diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_2681_handler_type_naming_binds_all_exchanges.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_2681_handler_type_naming_binds_all_exchanges.cs index c89f2becb..4f50fc63f 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_2681_handler_type_naming_binds_all_exchanges.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_2681_handler_type_naming_binds_all_exchanges.cs @@ -46,7 +46,13 @@ public async ValueTask InitializeAsync() _runtime = _host.Services.GetRequiredService(); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: IHost.Dispose() does not run StopAsync, so a synchronous teardown left this + // class's Rabbit consumers attached to a SHARED, fixed queue name and stealing later + // tests' messages. Stop the hosts for real. + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_host != null) await _host.StopAsync(); + } [Fact] public void handler_queue_is_bound_to_every_handled_message_exchange() @@ -127,7 +133,13 @@ public async ValueTask InitializeAsync() _runtime = _host.Services.GetRequiredService(); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: IHost.Dispose() does not run StopAsync, so a synchronous teardown left this + // class's Rabbit consumers attached to a SHARED, fixed queue name and stealing later + // tests' messages. Stop the hosts for real. + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_host != null) await _host.StopAsync(); + } [Fact] public void user_custom_binding_is_not_double_added_by_the_convention() diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_3041_saga_and_handler_with_handler_type_naming.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_3041_saga_and_handler_with_handler_type_naming.cs index 8a18ecd02..6e5fa0ee0 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_3041_saga_and_handler_with_handler_type_naming.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/Bug_3041_saga_and_handler_with_handler_type_naming.cs @@ -39,7 +39,13 @@ public async ValueTask InitializeAsync() _runtime = _host.Services.GetRequiredService(); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: IHost.Dispose() does not run StopAsync, so a synchronous teardown left this + // class's Rabbit consumers attached to a SHARED, fixed queue name and stealing later + // tests' messages. Stop the hosts for real. + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_host != null) await _host.StopAsync(); + } [Fact] public void both_the_saga_and_the_regular_handler_get_listener_queues() diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/ConventionalRoutingContext.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/ConventionalRoutingContext.cs index b17ba0ebf..d9c303d10 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/ConventionalRoutingContext.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/ConventionalRoutingContext.cs @@ -14,7 +14,16 @@ public static class ConventionalRoutingTestDefaults } -public abstract class ConventionalRoutingContext : IDisposable +/// +/// GH-3965. Note the async disposal. Every conventionally routed +/// lands on one FIXED queue -- the type carries [MessageIdentity("routed")], so the queue is +/// literally routed -- and several classes in this namespace stand up listeners on it. +/// does NOT run IHostedService.StopAsync, so disposing synchronously +/// left those consumers attached to the broker after the test finished, and a later test's message was +/// delivered to a leaked consumer belonging to an already-completed class. That shows up as a tracked +/// session containing Sent and no Received at all. +/// +public abstract class ConventionalRoutingContext : IDisposable, IAsyncDisposable { private IHost _host = null!; @@ -55,6 +64,22 @@ public void Dispose() _host?.Dispose(); } + public ValueTask DisposeAsync() => DisposeHostAsync(); + + /// + /// Stops the host so its Rabbit consumers are actually cancelled, then disposes it. Derived classes + /// that implement IAsyncLifetime shadow the interface implementation above, so they must call + /// this from their own DisposeAsync rather than returning a completed ValueTask. + /// + protected async ValueTask DisposeHostAsync() + { + if (_host == null) return; + + await _host.StopAsync(); + _host.Dispose(); + _host = null!; + } + internal async Task ConfigureConventions(Action configure) { _host = await WolverineHost.ForAsync(opts => diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs index 4a79971d0..60a7d9613 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing.cs @@ -27,7 +27,14 @@ public async ValueTask InitializeAsync() }); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: IHost.Dispose() does not run StopAsync, so a synchronous teardown left this + // class's Rabbit consumers attached to a SHARED, fixed queue name and stealing later + // tests' messages. Stop the hosts for real. + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_sender != null) await _sender.StopAsync(); + if (_receiver != null) await _receiver.StopAsync(); + } public void Dispose() { diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_custom_exchange.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_custom_exchange.cs index fc5fd048d..97f6c6e68 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_custom_exchange.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_custom_exchange.cs @@ -57,7 +57,14 @@ public async ValueTask InitializeAsync() } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: IHost.Dispose() does not run StopAsync, so a synchronous teardown left this + // class's Rabbit consumers attached to a SHARED, fixed queue name and stealing later + // tests' messages. Stop the hosts for real. + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_sender != null) await _sender.StopAsync(); + if (_receiver != null) await _receiver.StopAsync(); + } public void Dispose() { diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs index fc6d5656d..90b56258c 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/end_to_end_with_conventional_routing_with_prefix.cs @@ -38,7 +38,14 @@ public async ValueTask InitializeAsync() }); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: IHost.Dispose() does not run StopAsync, so a synchronous teardown left this + // class's Rabbit consumers attached to a SHARED, fixed queue name and stealing later + // tests' messages. Stop the hosts for real. + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_sender != null) await _sender.StopAsync(); + if (_receiver != null) await _receiver.StopAsync(); + } public void Dispose() { diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs index d9436c9bd..cb485a958 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_all_defaults.cs @@ -17,7 +17,8 @@ public async ValueTask InitializeAsync() theEndpoint = (await theRuntime()).Endpoints.EndpointFor(theExpectedUri).ShouldBeOfType(); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: shadows ConventionalRoutingContext.DisposeAsync -- must stop the host itself. + ValueTask IAsyncDisposable.DisposeAsync() => DisposeHostAsync(); [Fact] public void endpoint_should_be_a_listener() diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs index 57f84ce69..788db92e3 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_listening_endpoint_with_overridden_queue_naming.cs @@ -22,7 +22,8 @@ await ConfigureConventions(c => theEndpoint = (await theRuntime()).Endpoints.EndpointFor(theExpectedUri).ShouldBeOfType(); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: shadows ConventionalRoutingContext.DisposeAsync -- must stop the host itself. + ValueTask IAsyncDisposable.DisposeAsync() => DisposeHostAsync(); [Fact] public void endpoint_should_be_a_listener() diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs index 83eededbd..2f47be2ae 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_discovering_a_sender_with_all_defaults.cs @@ -20,7 +20,8 @@ public async ValueTask InitializeAsync() theRoute = ((await PublishingRoutesFor()).Single() as MessageRoute)!; } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: shadows ConventionalRoutingContext.DisposeAsync -- must stop the host itself. + ValueTask IAsyncDisposable.DisposeAsync() => DisposeHostAsync(); [Fact] public void should_have_exactly_one_route() diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_using_handler_type_naming.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_using_handler_type_naming.cs index 7ef068883..00c75afd0 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_using_handler_type_naming.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/ConventionalRouting/when_using_handler_type_naming.cs @@ -31,7 +31,13 @@ public async ValueTask InitializeAsync() _runtime = _host.Services.GetRequiredService(); } - ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask; + // GH-3965: IHost.Dispose() does not run StopAsync, so a synchronous teardown left this + // class's Rabbit consumers attached to a SHARED, fixed queue name and stealing later + // tests' messages. Stop the hosts for real. + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_host != null) await _host.StopAsync(); + } [Fact] public void listener_endpoint_should_be_named_after_handler_type()