rebus-org / Rebus.ServiceProvider

:bus: Microsoft Extensions Dependency Injection container adapter for Rebus
https://mookid.dk/category/rebus
Other
67 stars 34 forks source link

Transaction behaviour with different busses #78

Closed mattwcole closed 1 year ago

mattwcole commented 1 year ago

According to the documentation on transactions "if you receive a message with one bus instance and in its message handler you use ANOTHER bus instance to send/publish a message, that message will be sent/published immediately". This is not what I observe when using Rebus.ServiceProvider.

Here is a demo app showcasing the problem. I have configured 2 busses like so:

// Subscribing bus
services.AddRebus(
    configurer => configurer
        .Transport(transport => transport.UseRabbitMq(rabbitConnection, "MyApp")),
    isDefaultBus: false,
    async bus => await bus.Subscribe<InboundMessage>());

// Publishing bus
services.AddRebus(
    configurer => configurer
        .Transport(transport => transport.UseRabbitMqAsOneWayClient(rabbitConnection)),
    isDefaultBus: true);

services.AddRebusHandler<MyHandler>();

If the handler transaction fails, no outbound message is published, even though a different bus is used for publishing. My assumption is that the publishing bus is injected because it is the default bus. Have I misunderstood the documentation?

public class MyHandler : IHandleMessages<InboundMessage>
{
    private readonly IBus _bus;

    public MyHandler(IBus bus)
    {
        _bus = bus;
    }

    public async Task Handle(InboundMessage message)
    {
        await _bus.Publish(new OutboundMessage
        {
            Bar = message.Foo
        });

        // Uncommenting this prevents the outbound message from sending
        // throw new Exception("Failure after publish.");
    }
}
mookid8000 commented 1 year ago

Sorry, but this is actually expected behavior!

I know it's a little bit confusing, and this is one of the reasons why I have been an avid opponent of supporting multiple bus instances in the same container instance: It's confusing!

BUT here's how it works:

(1) Rebus message handlers (and their transitive dependencies resolved either with a scoped or a transient lifetime) ALWAYS get the handling bus instance injected. Always 🙂 This is what happens to you.(*)

(2) Everything else (i.e. everything resolved outside of a clearly defined "bus context"), will have the default bus resolved.

(3) Via IBusRegistry it's possibly to interrogate the "bus registry" about which instances it holds and possibly resolve a specific instance.


() And unfortunately, this is where it gets really F#KED, because if the dependency resolution ends up resolving a singleton, that singleton would get the bus currently handling a message injected, REGARDLESS of whether it was the default bus or not.

So if it's not the default bus, then the bus instance that gets to handle a message first will be the one injected into the singleton, and it would then be subjected to the infamous "transient caught by singleton"-situation (can't remember a better name for it right now), which is almost always NOT what you want.

I am sorry that it works this way. Given that The People (and myself included, I am 55% for and 45% against now 😉 ) want multiple bus instances in the same container instance, this is the best I could come up with.

mattwcole commented 1 year ago

Thank you for the clear explanation. So the "publishing bus" in my example above is never used. Maybe this could be mentioned in the readme secion on the primary/default bus?