diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs index 62cd6700..9c44f042 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs @@ -32,6 +32,31 @@ public static class ManagementEndpoints return DefaultAskTimeout; } + private static readonly TimeSpan DefaultLongRunningTimeout = TimeSpan.FromMinutes(5); + + /// Transport/deploy commands legitimately run for minutes (the CLI already + /// uses a 5-minute client timeout for bundles); a 30 s server-side Ask 504s while the + /// piped ProcessCommand keeps applying — the caller then retries and double-applies + /// (arch-review S1). These command types get LongRunningCommandTimeout instead. + private static readonly HashSet LongRunningCommands = + [ + typeof(ImportBundleCommand), typeof(PreviewBundleCommand), typeof(ExportBundleCommand), + typeof(MgmtDeployArtifactsCommand), typeof(MgmtDeployInstanceCommand), + ]; + + /// + /// Resolves the ManagementActor Ask timeout for a specific command type. Long-running + /// transport/deploy commands use + /// (default 5 min); all other commands fall through to . + /// + /// The management service options, or null if not configured. + /// The runtime type of the command being dispatched. + /// The resolved Ask timeout for the command. + public static TimeSpan ResolveAskTimeout(ManagementServiceOptions? options, Type commandType) + => LongRunningCommands.Contains(commandType) + ? (options is { LongRunningCommandTimeout.Ticks: > 0 } o ? o.LongRunningCommandTimeout : DefaultLongRunningTimeout) + : ResolveAskTimeout(options); + /// Registers the POST /management endpoint on the given route builder. /// The route builder to add the endpoint to. /// The same instance for chaining. @@ -99,7 +124,8 @@ public static class ManagementEndpoints var envelope = new ManagementEnvelope(authenticatedUser, command, correlationId); var askTimeout = ResolveAskTimeout( - context.RequestServices.GetService>()?.Value); + context.RequestServices.GetService>()?.Value, + command.GetType()); object response; try diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementServiceOptions.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementServiceOptions.cs index d21150f2..4aa8829d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementServiceOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementServiceOptions.cs @@ -4,4 +4,7 @@ public class ManagementServiceOptions { /// Maximum time to wait for a management command to complete before returning a timeout error; default 30 seconds. public TimeSpan CommandTimeout { get; set; } = TimeSpan.FromSeconds(30); + + /// Maximum time to wait for a long-running transport/deploy command (import/preview/export bundle, deploy artifacts/instance) before returning a timeout error; default 5 minutes. + public TimeSpan LongRunningCommandTimeout { get; set; } = TimeSpan.FromMinutes(5); } diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementEndpointsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementEndpointsTests.cs index 5aec495a..965e72d1 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementEndpointsTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementEndpointsTests.cs @@ -99,4 +99,30 @@ public class ManagementEndpointsTests Assert.Equal(TimeSpan.FromSeconds(30), timeout); } + + // ======================================================================== + // Per-command long-running Ask timeout (arch-review S1) + // + // Transport/deploy commands legitimately run for minutes; they must use the + // LongRunningCommandTimeout (default 5 min) instead of the 30 s default. + // ======================================================================== + + [Theory] + [InlineData(typeof(ImportBundleCommand))] + [InlineData(typeof(ExportBundleCommand))] + [InlineData(typeof(PreviewBundleCommand))] + [InlineData(typeof(MgmtDeployArtifactsCommand))] + [InlineData(typeof(MgmtDeployInstanceCommand))] + public void ResolveAskTimeout_LongRunningCommand_UsesLongTimeout(Type cmd) + => Assert.Equal(TimeSpan.FromMinutes(5), ManagementEndpoints.ResolveAskTimeout(null, cmd)); + + [Fact] + public void ResolveAskTimeout_OrdinaryCommand_UsesDefault() + => Assert.Equal(TimeSpan.FromSeconds(30), ManagementEndpoints.ResolveAskTimeout(null, typeof(ListTemplatesCommand))); + + [Fact] + public void ResolveAskTimeout_ConfiguredLongTimeout_Honored() + => Assert.Equal(TimeSpan.FromMinutes(10), ManagementEndpoints.ResolveAskTimeout( + new ManagementServiceOptions { LongRunningCommandTimeout = TimeSpan.FromMinutes(10) }, + typeof(ImportBundleCommand))); }