
I’ve been able to talk and write a bit about Wolverine in the last couple weeks. This builds on the last two blog posts in this list:
- Wolverine on the JetBrains Webinar series — but watch out, the
ICommandBus
interface shown in that webinar was consolidated and changed toIMessageBus
in the latest release. The rest of the syntax and the concepts are all unchanged though. - Wolverine on DotNetRocks — a conversation about Wolverine with a bonus rant from me about prescriptive, hexagonal architectures
- Introducing Wolverine for Effective Server Side .NET Development
- How Wolverine allows for easier testing
Alright, back to the sample message handler from my previous two blog posts here’s the shorthand version:
[Transactional]
public static async Task Handle(
DebitAccount command,
Account account,
IDocumentSession session,
IMessageContext messaging)
{
account.Balance -= command.Amount;
// This just marks the account as changed, but
// doesn't actually commit changes to the database
// yet. That actually matters as I hopefully explain
session.Store(account);
// Conditionally trigger other, cascading messages
if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
{
await messaging.SendAsync(new LowBalanceDetected(account.Id));
}
else if (account.Balance < 0)
{
await messaging.SendAsync(new AccountOverdrawn(account.Id));
// Give the customer 10 days to deal with the overdrawn account
await messaging.ScheduleAsync(new EnforceAccountOverdrawnDeadline(account.Id), 10.Days());
}
}
and just for the sake of completion, here is a longer hand, completely equivalent version of the same handler:
[Transactional]
public static async Task Handle(
DebitAccount command,
Account account,
IDocumentSession session,
IMessageContext messaging)
{
account.Balance -= command.Amount;
// This just marks the account as changed, but
// doesn't actually commit changes to the database
// yet. That actually matters as I hopefully explain
session.Store(account);
if (account.Balance > 0 && account.Balance < account.MinimumThreshold)
{
await messaging.SendAsync(new LowBalanceDetected(account.Id));
}
else if (account.Balance < 0)
{
await messaging.SendAsync(new AccountOverdrawn(account.Id));
// Give the customer 10 days to deal with the overdrawn account
await messaging.ScheduleAsync(new EnforceAccountOverdrawnDeadline(account.Id), 10.Days());
}
}
To review just a little bit, that Wolverine style message handler at runtime is committing changes to an Account
in the underlying database and potentially sending out additional messages based on the state of the Account
. For folks who are experienced with asynchronous messaging systems who hear me say that Wolverine does not support any kind of 2 phase commits between the database and message brokers, you’re probably already concerned with some potential problems in that code above:
- Maybe the database changes fail, but there are “ghost” messages already queued that pertain to data changes that never actually happened
- Maybe the messages actually manage to get through to their downstream handlers and are applied erroneously because the related database changes have not yet been applied. That’s a race condition that absolutely happens if you’re not careful (ask me how I know 😦 )
- Maybe the database changes succeed, but the messages fail to be sent because of a network hiccup or who knows what problem happens with the message broker
Needless to say, there’s genuinely a lot of potential problems from those handful lines of code up above. Some of you reading this have probably already said to yourself that this calls for using some sort of transactional outbox — and Wolverine thinks so too!
The general idea of an “outbox” is to obviate the lack of true 2 phase commits by ensuring that outgoing messages are held until the database transaction is successful, then somehow guaranteeing that the messages will be sent out afterward. In the case of Wolverine and its integration with Marten, the order of operations in the message handler (in either version) shown above is to:
- Tell Marten that the
Account
document needs to be persisted. Nothing happens at this point other than marking the document as changed - The handler creates messages that are registered with the current
IMessageContext
. Again, the messages do not actually go out here, instead they are routed by Wolverine to know exactly how and where they should be sent later - The Wolverine + Marten
[Transactional]
middleware is calling the MartenIDocumentSession.SaveChangesAsync()
method that makes the changes to theAccount
document and also creates new database records to persist any outgoing messages in the underlying Postgresql application database in one single, native database transaction. Even better, with the Marten integration, all the database operations are even happening in one single batched database call for maximum efficiency. - When Marten successfully commits the database transaction, it tells Wolverine to “flush” the outgoing messages to the sending agents in Wolverine (depending on configuration and exact transport type, the messages might be sent “inline” or batched up with other messages to go out later).
To be clear, Wolverine also supports a transactional outbox with EF Core against either Sql Server or Postgresql. I’ll blog and/or document that soon.
The integration with Marten that’s in the WolverineFx.Marten
Nuget isn’t that bad (I hope). First off, in my application bootstrapping I chain the IntegrateWithWolverine()
call to the standard Marten bootstrapping like this:
using Wolverine.Marten;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarten(opts =>
{
// This would be from your configuration file in typical usage
opts.Connection(Servers.PostgresConnectionString);
opts.DatabaseSchemaName = "wolverine_middleware";
})
// This is the wolverine integration for the outbox/inbox,
// transactional middleware, saga persistence we don't care about
// yet
.IntegrateWithWolverine()
// Just letting Marten build out known database schema elements upfront
// Helps with Wolverine integration in development
.ApplyAllDatabaseChangesOnStartup();
For the moment, I’m going to say that all the “cascading messages” from the DebitAccount
message handler are being handled by local, in memory queues. At this point — and I’d love to have feedback on the applicability or usability of this approach — each endpoint has to be explicitly enrolled into the durable outbox or inbox (for incoming, listening endpoints) mechanics. Knowing both of those things, I’m going to add a little bit of configuration to make every local queue durable:
builder.Host.UseWolverine(opts =>
{
// Middleware introduced in previous posts
opts.Handlers.AddMiddlewareByMessageType(typeof(AccountLookupMiddleware));
opts.UseFluentValidation();
// The nomenclature might be inconsistent here, but the key
// point is to make the local queues durable
opts.Policies
.AllLocalQueues(x => x.UseDurableInbox());
});
If instead I chose to publish some of the outgoing messages with Rabbit MQ to other processes (or just want the messages queued), I can add the WolverineFx.RabbitMQ
Nuget and change the bootstrapping to this:
builder.Host.UseWolverine(opts =>
{
// Middleware introduced in previous posts
opts.Handlers.AddMiddlewareByMessageType(typeof(AccountLookupMiddleware));
opts.UseFluentValidation();
var rabbitUri = builder.Configuration.GetValue<Uri>("rabbitmq-broker-uri");
opts.UseRabbitMq(rabbitUri)
// Just do the routing off of conventions, more or less
// queue and/or exchange based on the Wolverine message type name
.UseConventionalRouting()
.ConfigureSenders(x => x.UseDurableOutbox());
});
I just threw a bunch of details at you all, so let me try to anticipate a couple questions you might have and also try to answer them:
- Do the messages get delivered before the transaction completes? No, they’re held in memory until the transaction completes, then get sent
- What happens if the message delivery fails? The Wolverine sending agents run in a hosted service within your application. When message delivery fails, the sending agent will try it again up to a configurable amount of times (100 is the default). Read the next question though before the “100” number bugs you:
- What happens if the whole message broker is down? Wolverine’s sending agents have a crude circuit breaker and will stop trying to send message batches if there are too many failures in a period of time, then resume sending after a periodic “ping” message gets though. Long story short, Wolverine will buffer outgoing messages in the application database until Wolverine is able to reach the message broker.
- What happens if the application process fails between the transaction succeeding and the message getting to the broker? The message will be recovered and sent by either another active node of the application if running in a cluster, or by restarting the single application process.
- So you can do this in a cluster without sending the message multiple times? Yep.
- What if you have zillions of stored messages and you restart the application, will it overwhelm the process and cause harm? It’s paged, distributes a bit between nodes, and there’s some back pressure to keep it from having too many outgoing messages in memory.
- Can I use Sql Server instead? Yes. But for the moment, it’s like the scene in Blues Brothers when Elwood asks what kinds of music they have and the waitress replies “we have both kinds, Country and Western.”
- Can I tell Wolverine to throw away a message that’s old and maybe out of date if it still hasn’t been processed? Yes, and I’ll show a bit of that in the next post.
- What about messages that are routed to a non-durable endpoint as part of an outbox’d transaction? Good question! Wolverine is still holding those messages in memory until the message being processed successfully finishes, then kicks them out to in memory sending agents. Those sending agents have their own internal queues and retry loops for maximum resiliency. And actually for that matter, Wolverine has a built in in memory outbox to at least deal with ordering between the message processing and actually sending outgoing messages.
Next Time
WordPress just cut off the last section, so I’ll write a short follow up on mixing in non-durable message queues with message expirations. Next week I’ll keep on this sample application by discussing how Wolverine & its friends try really hard for a “clone n’go” developer workflow where you can be up and running mere minutes with all the database & message broker infrastructure up and going after a fresh clone of the codebase.
The first two code snippets are almost exactly the same except for the comment “Conditionally trigger other, cascading messages”.
Copy/paste fail. Doesn’t actually impact the post’s contents much. The Outbox applies regardless of whether you explicitly call `IMessageContext` or use cascading messages