diff --git a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs
index b510e326ec..93614c2dfb 100644
--- a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs
+++ b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs
@@ -13,6 +13,7 @@
///
[TestFixture]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
+ [CancelAfter(180_000)]
public abstract partial class NServiceBusAcceptanceTest
{
[SetUp]
diff --git a/src/ServiceControl.AcceptanceTesting/TestTimeoutScenario.cs b/src/ServiceControl.AcceptanceTesting/TestTimeoutScenario.cs
new file mode 100644
index 0000000000..c86d6d01bc
--- /dev/null
+++ b/src/ServiceControl.AcceptanceTesting/TestTimeoutScenario.cs
@@ -0,0 +1,109 @@
+namespace ServiceControl.AcceptanceTesting;
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using NServiceBus.AcceptanceTesting;
+using NServiceBus.AcceptanceTesting.Support;
+using NUnit.Framework;
+using NUnit.Framework.Internal;
+
+public static class TestTimeoutScenarioExtensions
+{
+ ///
+ /// Bounds the scenario by the test's own cancellation token instead of the framework's fixed 90 second
+ /// Done limit. The token comes from , which
+ /// applies to every test and which a test can override.
+ ///
+ public static IScenarioWithEndpointBehavior WithTestTimeout(this IScenarioWithEndpointBehavior scenario)
+ where TContext : ScenarioContext =>
+ new TestTimeoutScenario(scenario);
+}
+
+class TestTimeoutScenario(IScenarioWithEndpointBehavior inner) : IScenarioWithEndpointBehavior
+ where TContext : ScenarioContext
+{
+ public IScenarioWithEndpointBehavior WithEndpoint() where T : EndpointConfigurationBuilder, new()
+ {
+ inner = inner.WithEndpoint();
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior WithEndpoint(Action> behavior) where T : EndpointConfigurationBuilder, new()
+ {
+ inner = inner.WithEndpoint(behavior);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior WithEndpoint(EndpointConfigurationBuilder endpointConfigurationBuilder, Action> defineBehavior)
+ {
+ inner = inner.WithEndpoint(endpointConfigurationBuilder, defineBehavior);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior WithComponent(IComponentBehavior componentBehavior)
+ {
+ inner = inner.WithComponent(componentBehavior);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior WithServices(Action configureServices)
+ {
+ inner = inner.WithServices(configureServices);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior WithServices(Action configureServices)
+ {
+ inner = inner.WithServices(configureServices);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior WithServiceResolve(Func resolve, ServiceResolveMode resolveMode = ServiceResolveMode.BeforeStart)
+ {
+ inner = inner.WithServiceResolve(resolve, resolveMode);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior WithServiceResolve(Func resolve, ServiceResolveMode resolveMode = ServiceResolveMode.BeforeStart)
+ {
+ inner = inner.WithServiceResolve(resolve, resolveMode);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior Done(Func func)
+ {
+ inner = inner.Done(func);
+ return this;
+ }
+
+ public IScenarioWithEndpointBehavior Done(Func func)
+ {
+ inner = inner.Done(func);
+ return this;
+ }
+
+ public Task Run(CancellationToken cancellationToken = default) => Run(new RunSettings(), cancellationToken);
+
+ public async Task Run(RunSettings settings, CancellationToken cancellationToken = default)
+ {
+ // The framework only lifts its fixed 90 second Done limit when handed a token that can actually be
+ // cancelled, so a caller that passes nothing gets the test's own token instead.
+ if (!cancellationToken.CanBeCanceled)
+ {
+ cancellationToken = TestContext.CurrentContext.CancellationToken;
+ }
+
+ try
+ {
+ return await inner.Run(settings, cancellationToken);
+ }
+ catch (Exception e) when (e is TimeoutException or OperationCanceledException && cancellationToken.IsCancellationRequested)
+ {
+ // Done wraps the cancellation in a TimeoutException naming the framework's own limit, which is
+ // infinite for a cancellable token and prints as a negative number of seconds.
+ throw new TimeoutException($"The scenario did not complete within the test's CancelAfter budget of {TestExecutionContext.CurrentContext.TestCaseTimeout} ms.", e);
+ }
+ }
+}
diff --git a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs
index a445576e0a..e86af5c88c 100644
--- a/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs
+++ b/src/ServiceControl.AcceptanceTests/TestSupport/AcceptanceTest.cs
@@ -90,7 +90,8 @@ protected void ExecuteWhen(Func execute, Func action,
protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() =>
Scenario.Define(contextInitializer)
- .WithComponent(serviceControlRunnerBehavior);
+ .WithComponent(serviceControlRunnerBehavior)
+ .WithTestTimeout();
protected Action CustomConfiguration = _ => { };
protected Action SetSettings = _ => { };
diff --git a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs
index 133e7e805a..5c295cb104 100644
--- a/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs
+++ b/src/ServiceControl.Audit.AcceptanceTests/AcceptanceTest.cs
@@ -65,7 +65,8 @@ public Task Teardown()
protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() =>
Scenario.Define(contextInitializer)
- .WithComponent(serviceControlRunnerBehavior);
+ .WithComponent(serviceControlRunnerBehavior)
+ .WithTestTimeout();
protected Action CustomConfiguration = _ => { };
protected Action SetSettings = _ => { };
diff --git a/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs
index aaf1849fd2..baf11934be 100644
--- a/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs
+++ b/src/ServiceControl.Monitoring.AcceptanceTests/AcceptanceTest.cs
@@ -55,7 +55,8 @@ public void Teardown()
protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() =>
Scenario.Define(contextInitializer)
- .WithComponent(serviceControlRunnerBehavior);
+ .WithComponent(serviceControlRunnerBehavior)
+ .WithTestTimeout();
protected Action CustomConfiguration = _ => { };
protected Action SetSettings = _ => { };
diff --git a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs
index f618503256..fd5c5301ac 100644
--- a/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs
+++ b/src/ServiceControl.MultiInstance.AcceptanceTests/AcceptanceTest.cs
@@ -71,7 +71,8 @@ public void Teardown()
protected IScenarioWithEndpointBehavior Define(Action contextInitializer) where T : ScenarioContext, new() =>
Scenario.Define(contextInitializer)
- .WithComponent(serviceControlRunnerBehavior);
+ .WithComponent(serviceControlRunnerBehavior)
+ .WithTestTimeout();
protected Action CustomPrimaryEndpointConfiguration = c => { };
protected Action CustomAuditEndpointConfiguration = c => { };