diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Artifacts/ExternalSystemArtifact.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Artifacts/ExternalSystemArtifact.cs index f93e6ebb..6e689145 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Artifacts/ExternalSystemArtifact.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Artifacts/ExternalSystemArtifact.cs @@ -5,4 +5,5 @@ public record ExternalSystemArtifact( string EndpointUrl, string AuthType, string? AuthConfiguration, - string? MethodDefinitionsJson); + string? MethodDefinitionsJson, + int TimeoutSeconds = 0); diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs index 8d3d310c..41f06bf8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs @@ -192,7 +192,7 @@ public class ArtifactDeploymentService })) : null; externalSystemArtifacts.Add(new ExternalSystemArtifact( - es.Name, es.EndpointUrl, es.AuthType, es.AuthConfiguration, methodsJson)); + es.Name, es.EndpointUrl, es.AuthType, es.AuthConfiguration, methodsJson, es.TimeoutSeconds)); } // Map database connections diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs index f4953b55..fbbcf723 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs @@ -1885,7 +1885,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers foreach (var es in command.ExternalSystems) { await _storage.StoreExternalSystemAsync(es.Name, es.EndpointUrl, - es.AuthType, es.AuthConfiguration, es.MethodDefinitionsJson); + es.AuthType, es.AuthConfiguration, es.MethodDefinitionsJson, es.TimeoutSeconds); } } diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReplicationActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReplicationActor.cs index fe8a3a64..f7d9d375 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReplicationActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReplicationActor.cs @@ -349,7 +349,7 @@ public class SiteReplicationActor : ReceiveActor if (command.ExternalSystems != null) foreach (var es in command.ExternalSystems) - await _storage.StoreExternalSystemAsync(es.Name, es.EndpointUrl, es.AuthType, es.AuthConfiguration, es.MethodDefinitionsJson); + await _storage.StoreExternalSystemAsync(es.Name, es.EndpointUrl, es.AuthType, es.AuthConfiguration, es.MethodDefinitionsJson, es.TimeoutSeconds); if (command.DatabaseConnections != null) foreach (var db in command.DatabaseConnections) diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs index a4c86003..60800328 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs @@ -174,6 +174,10 @@ public class SiteStorageService // Native-alarm display metadata (UA4) — restored on rehydration so a persisted condition // renders fully (type/category/message/values) before the first source snapshot arrives. await TryAddColumnAsync(connection, "native_alarm_state", "metadata_json", "TEXT"); + + // Per-external-system call timeout (ExternalSystemGateway Timeout) — carried through the + // artifact pipeline so site-side calls honor it; 0 = use the site default. + await TryAddColumnAsync(connection, "external_systems", "timeout_seconds", "INTEGER NOT NULL DEFAULT 0"); } private async Task TryAddColumnAsync(SqliteConnection connection, string table, string column, string type) @@ -716,22 +720,25 @@ public class SiteStorageService /// The authentication type (e.g., 'ApiKey', 'BasicAuth'). /// Authentication configuration JSON, if applicable. /// JSON representation of available method definitions, if any. + /// Per-system call timeout in seconds (0 = use the site default). /// A task that completes when the external system definition has been stored or updated. public async Task StoreExternalSystemAsync( - string name, string endpointUrl, string authType, string? authConfig, string? methodDefs) + string name, string endpointUrl, string authType, string? authConfig, string? methodDefs, + int timeoutSeconds = 0) { await using var connection = new SqliteConnection(_connectionString); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = @" - INSERT INTO external_systems (name, endpoint_url, auth_type, auth_configuration, method_definitions, updated_at) - VALUES (@name, @url, @authType, @authConfig, @methodDefs, @updatedAt) + INSERT INTO external_systems (name, endpoint_url, auth_type, auth_configuration, method_definitions, timeout_seconds, updated_at) + VALUES (@name, @url, @authType, @authConfig, @methodDefs, @timeoutSeconds, @updatedAt) ON CONFLICT(name) DO UPDATE SET endpoint_url = excluded.endpoint_url, auth_type = excluded.auth_type, auth_configuration = excluded.auth_configuration, method_definitions = excluded.method_definitions, + timeout_seconds = excluded.timeout_seconds, updated_at = excluded.updated_at"; command.Parameters.AddWithValue("@name", name); @@ -739,6 +746,7 @@ public class SiteStorageService command.Parameters.AddWithValue("@authType", authType); command.Parameters.AddWithValue("@authConfig", (object?)authConfig ?? DBNull.Value); command.Parameters.AddWithValue("@methodDefs", (object?)methodDefs ?? DBNull.Value); + command.Parameters.AddWithValue("@timeoutSeconds", timeoutSeconds); command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O")); await command.ExecuteNonQueryAsync(); diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs index 8b378390..efb8a48e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs @@ -36,7 +36,7 @@ public class SiteExternalSystemRepository : IExternalSystemRepository await using var command = connection.CreateCommand(); command.CommandText = @" - SELECT name, endpoint_url, auth_type, auth_configuration + SELECT name, endpoint_url, auth_type, auth_configuration, timeout_seconds FROM external_systems"; var results = new List(); @@ -68,7 +68,7 @@ public class SiteExternalSystemRepository : IExternalSystemRepository await using var command = connection.CreateCommand(); command.CommandText = @" - SELECT name, endpoint_url, auth_type, auth_configuration + SELECT name, endpoint_url, auth_type, auth_configuration, timeout_seconds FROM external_systems WHERE name = @name"; command.Parameters.AddWithValue("@name", name); @@ -263,7 +263,8 @@ public class SiteExternalSystemRepository : IExternalSystemRepository authType: reader.GetString(2)) { Id = GenerateSyntheticId(name), - AuthConfiguration = reader.IsDBNull(3) ? null : reader.GetString(3) + AuthConfiguration = reader.IsDBNull(3) ? null : reader.GetString(3), + TimeoutSeconds = reader.IsDBNull(4) ? 0 : reader.GetInt32(4) }; } diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs index c1a3e88e..14b1f08b 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs @@ -54,6 +54,26 @@ public class SiteRepositoryTests : IDisposable Assert.Equal("https://api.example.com", all[0].EndpointUrl); } + /// + /// ExternalSystemGateway (Timeout): the per-system TimeoutSeconds stored via + /// must round-trip through the + /// site SQLite store and back out via the repository, so site-side calls honor it. + /// + [Fact] + public async Task SiteStorage_ExternalSystem_TimeoutSeconds_RoundTrips() + { + var storage = NewStorage(); + await storage.InitializeAsync(); + await storage.StoreExternalSystemAsync( + "WeatherApi", "https://api.example.com", "ApiKey", "{\"key\":\"x\"}", null, 7); + + var repo = new SiteExternalSystemRepository(storage); + var found = await repo.GetExternalSystemByNameAsync("WeatherApi"); + + Assert.NotNull(found); + Assert.Equal(7, found!.TimeoutSeconds); + } + /// /// SiteRuntime-007: the synthetic ID for an external system must be identical when /// the storage service and repository are re-created (simulating a process restart).