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)));
}