Rebus: Should IdempotencyData be persisted along with IdempotentSagaData instance? - c#

I'm trying to use the IdempotentSagas in Rebus with MongoDb as a storage.
I enable idempotency when configuring Rebus like this:
Configure
...
.Options( o => { o.EnableIdempotentSagas(); } )
...
.Sagas( s => { s.StoreInMongoDb( mongoDatabase ); } )
I can see in debug that (during handling the message) the property IdempotencyData in the IdempotentSagaData instance stores the handled message id.
But when the saga data gets persisted, the IdempotencyData is always stored as an empty document:
{
"_id" : NUUID("0aa63d69-f8f9-46bd-ab29-f1e46411a166"),
"Revision" : 1,
"IdempotencyData" : {},
...
}
and thus it always appears empty when the saga data is loaded from the storage to handle a message.
It seems that this neglects all the idempotency checks, and later redelivered messages will be handled as if they are completely new. But the IdempotencyData class seems to be designed in a way which prevents it from being serialized by the default MongoDb BsonSerializer (get-only properties, private backing fields).
Is it an intentional behavior? Maybe I'm missing some configuration step which would allow the idempotency data to be persisted?
Thanks in advance for your help.

Rebus (all versions prior to Rebus 5.0.0-b14) had a BSON serializer-unfriendly IdempotencyData, thus making it impossible to properly roundtrip this pretty important piece of data when using IIdempotentSagaData.
It has been fixed in Rebus.MongoDb 5.0.0-b02 and Rebus 5.0.0-b14.
Rebus' IdempotencyData now has proper constructors in place, allowing for serializers to initialize the entire state that way.
Rebus.MongoDb now registers appropriate class maps during initialization of the saga storage.

Related

Serilog TcpSink Not throwing exceptions

Is there any way to track if end-point is available for Tcp Sink logging ?
For example locally on my machine I do not have FileBeat setup, while its working on Staging machine.
The way I initialize Logger
private readonly ILogger _tcpLogger;
public TcpClient(IOptions<ElasticSearchConfig> tcpClientConfig)
{
var ip = IPAddress.Parse(tcpClientConfig.Value.TcpClientConfig.IpAddress);
_tcpLogger = new LoggerConfiguration()
.WriteTo.TCPSink(ip, tcpClientConfig.Value.TcpClientConfig.Port, new TcpOutputFormatter())
.CreateLogger();
}
and simple method just to submit log
public void SubmitLog(string json)
{
_tcpLogger.Information(json);
}
And in my case when its submitting json string locally, it just goes nowhere and I would like to get an exeption/message back.
ideally on json submit, but during initialization is Ok.
Writing to a Serilog logger is meant to be a safe operation and never throw exceptions, and that's by design. Thus any exceptions that happen when sending those messages would only appear in the SelfLog - if you enable it.
e.g.
// Write Serilog errors to the Console
Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine(msg));
The example above is, of course, just to illustrate the SelfLog feature... You'll choose where/how to display or store these error messages.
Now, if the operation you're logging is important enough that you'd want to guarantee it succeeds (or throws exception if it doesn't) then you should use Audit Logging, i.e. Use .AuditTo.TCPSink(...) instead of .WriteTo.TCPSink(...)

Is it possible to add dynamic data to an MassTransit courier/routing slip custom event?

I have a MassTransit routing slip configured and working. For reference, the routing slip takes in an ID of an item in a MongoDB database and then creates a "version" of that document in a SQL database using EF Core. The activities (as commands) are:
Migrate document to SQL
Update audit info in MongoDB document
Update MongoDB document status (i.e. to published)
All of the above are write commands.
I have added a new 1st step which runs a query to make sure the MongoDB document is valid (e.g. name and description fields are completed) before running the migration. If this step fails it throws a custom exception, which in turns fires a failed event which is then picked up and managed by my saga. Below is a snippet of my activity code followed by the routing slip builder code:
Activity code
var result = await _queryDispatcher.ExecuteAsync<SelectModuleValidationResultById, ModuleValidationResult>(query).ConfigureAwait(false);
if (!result.ModuleValidationMessages.Any())
{
return context.Completed();
}
return context.Faulted(new ModuleNotValidException
{
ModuleId = messageCommand.ModuleId,
ModuleValidationMessages = result.ModuleValidationMessages
});
Routing slip builder code
builder.AddActivity(
nameof(Step1ValidateModule),
context.GetDestinationAddress(ActivityHelper.BuildQueueName<Step1ValidateModule>(ActivityQueueType.Execute)),
new SelectModuleValidationResultById(
context.Message.ModuleId,
context.Message.UserId,
context.Message.LanguageId)
);
builder.AddSubscription(
context.SourceAddress,
RoutingSlipEvents.ActivityFaulted,
RoutingSlipEventContents.All,
nameof(Step1ValidateModule),
x => x.Send<IModuleValidationFailed>(new
{
context.Message.ModuleId,
context.Message.LanguageId,
context.Message.UserId,
context.Message.DeploymentId,
}));
Whilst all of this works and the event gets picked up by my saga I would ideally like to add the ModuleValidationMessages (i.e. any failed validation messages) to the event being returned but I can't figure out how or even if that's possible (or more fundamentally if it's right thing to do).
It's worth noting that this is a last resort check and that the validation is checked by the client before even trying the migration so worse case scenario I can just leave it has "Has validation issues" but ideally I would like to include the derail in the failed response.
Good use case, and yes, it's possible to add the details you need to the built-in routing slip events. Instead of throwing an exception, you can Terminate the routing slip, and include variables - such as an array of messages, which are added to the RoutingSlipTerminated event that will be published.
This way, it isn't a fault but more of a business decision to terminate the routing slip prematurely. It's a contextual difference, which is why it allows variables to be specified (versus Faulted, which is a full-tilt exception).
You can then pull the array from the variables and use those in your saga or consumer.

Publishing to multiple Rebus queues with different stores

We have one application that publishes messages - PublishApp.
The messages (MessageA and MessageB) are picked up by two different consumer apps (ConsumerA and ConsumerB).
All applications use SQL Server as a transport and Windsor configuration, but the two Consumers have different databases in SQL Server.
How can we configure PublishApp to publish MessageA for ConsumerA and MessageB for ConsumerB?
I have tried using DetermineMessageOwnership as described here, but that doesn't seem to actually be called (no breakpoints hit). I'm a little mystified as to what the string endpoints returned should be.
I was hoping that I could set up an IBus component in Windsor with a specific name, then reference that by name when setting up my MessageB-publishing class. However it's not clear how to set up an IBus in Windsor outside of the magic box that does it all for me.
Fiddling with Windsor configuration leads me to a Windsor error if I try to call Configure.With(new WindsorContainerAdapter(container)) twice, as it is interpreted as registering the IBus interface twice. I can't see an extension point here to give one of the IBus instances a name, and hence differentiate them in Windsor.
Alternatively, trying to reuse the Configure.With... call throws an error telling me I have called .Transport() on the configurer twice, which is also not allowed (but which would let me use a different connection string...)
Adding XML configuration will let me specify different endpoints for my different messages, but not different SQL connection strings.
What I would really like to end up with is something like:
// Set up Bus A
var busA = Configure.With(new WindsorContainerAdapter(container))
.Transport(tc => tc.UseSqlServerInOneWayClientMode("ConnectionStringA"))
.Subscriptions(sc => sc.StoreInSqlServer("ConnectionStringA", "RebusSubscriptions"))
.CreateBus()
.Start();
// Set up Bus B
var busB = Configure.With(new WindsorContainerAdapter(container))
.Transport(tc => tc.UseSqlServerInOneWayClientMode("ConnectionStringB"))
.Subscriptions(sc => sc.StoreInSqlServer("ConnectionStringB", "RebusSubscriptions"))
.CreateBus()
.Start();
// Register Bus A in Windsor
container.Register(Component.For<IBus>()
.Named("BusA")
.Instance(busA));
// Register a class that depends on IBus, and set it to use Bus A
container.Register(Component.For<IPublishA>()
.ImplementedBy<PublishA>()
.DependsOn(Dependency.OnComponent(typeof(IBus), "BusA"));
// And a registration also for IBus B, and for IPublishB to reference named "BusB"
Note: I do not want to listen to multiple buses, only publish events to them. Other applications are monitoring the queues, and each application only listens for one event on one queue.
We resolved this in the end by dropping the WindsorContainerAdaptor. Since we're not handling any messages, only publishing/sending, we don't need any of the 'handler' stuff in the container adaptor and we can switch the registration of the IBus component around to happen outside of the configuration/starting, rather than inside it. This gives us the control to name the IBus registration.
public static void ConfigureAndStartBus(IWindsorContainer container)
{
_RegisterBus(container, "ConnectionStringA" "BusA");
_RegisterBus(container, "ConnectionStringB" "BusB");
}
private static void _RegisterBus(IWindsorContainer container, string connectionString, string busName)
{
var bus = Configure.With(new BuiltinContainerAdapter())
.Transport(tc => tc.UseSqlServerInOneWayClientMode(connectionString))
.Subscriptions(sc => sc.StoreInSqlServer(connectionString, "RebusSubscriptions"))
.CreateBus()
.Start();
container.Register(
Component.For<IBus>()
.Named(busName)
.LifestyleSingleton()
.Instance(bus));
}
Then in class PublishA, we can register it with a dependency on BusA, and PublishB can be registered with a dependency on BusB. The messages go to separate databases, and are picked up by separate subscribers to do work in those different databases.
First off: There's no way (at least at the moment) to ship messages between two SQL Server databases. In order for messages to be sent/published between endpoints, you need to use one single table in one shared database.
Your setup hints at something being off there, since you're using "ConnectionStringA" and "ConnectionStringB" for the transports.
It's not clear to me whether you actually want/need to do pub/sub messaging - pub/sub is what you would usually use when you want multiple recipients of each message, which would usually be some kind of event (i.e. a message whose name is in the past tense, as in: "this and that happened").
If you want one specific recipient for a message, you want to bus.Send that message, and that is when your endpoint mappings will be hit in order to get a destination for the message.
If you tell me some more about exactly what you're trying to achieve, I am sure I can help you :)

How to log if nservicebus saga is already started

I have a saga:
public class MySaga : Saga<MySagaEntity>,
IAmStartedByMessages<Message1>,
IAmStartedByMessages<Message2> {
}
In general I need to see easily from logs which of the messages starts which saga.
What I need is to log something like :
Recieved message Message1 with ... which starts a new saga
recieved message Message2 with ... for exsisting saga wiht Id=...
As alternatives i have following ways:
1. check if log file if that saga was not started
2. check if correlationid of saga is empty (so as it will be filled within handlers which start the saga)
if (Data.CorrelationId == default_value)
_log.DebugFormat("message starts saga CorrelationId={0}", message.CorrelationId)
Does anyone knew better ways for this?
There isn't currently a way in NServiceBus to get notified if a saga has been created or if a existing instance was loaded. (I've opened up a github issue for further discussion)
That said if the fact that the saga was created by a given message has a business meaning you're probably better off setting a boolean flag on your saga data to record this explicitly.
if(Data.SagaWasStartedByAOnlineCustomer)
Bus.Send(new VerifySomethingForOnlineCustomersCommand);

MassTransit binary serialized messages are not handled correctly

I've been using MassTransit for handling e-mail messages. Using this code: http://meandaspnet.blogspot.com/2009/10/how-to-binary-serialize-mailmessage-for.html I'm able to binary serialize my e-mails and publish them to my service bus. They're handled correctly too.
Bus.Initialize(
sbc =>
{
sbc.EnableMessageTracing();
sbc.ReceiveFrom("msmq://localhost/MyQueue");
sbc.UseMsmq();
sbc.VerifyMsmqConfiguration();
sbc.UseMulticastSubscriptionClient();
sbc.UseBinarySerializer();
sbc.Subscribe(subs => subs.Instance(new MessageHandler()));
});
Now I added a new type and handler:
// Check out the sequence of the Consumes<> !
public class MessageHandler :
Consumes<SerializeableMailMessage>.All,
Consumes<AangifteOmzetbelasting>.All
{
public void Consume(AangifteOmzetbelasting message)
{
// Some code - method will NOT be called
}
public void Consume(SerializeableMailMessage mailMessage)
{
// Some code - this method is called by Mass Transit
}
}
The weird thing is that this works if I Publish a SerializableMailMessage - but not for AangifteOmzetbelasting. If I change the interface order - it works for AangifteOmzetbelasting and not for SerializableMailMessage. Like so:
// Check out the sequence of the Consumes<> !
public class MessageHandler :
Consumes<AangifteOmzetbelasting>.All,
Consumes<SerializeableMailMessage>.All
In the latter case, the SerializedMailMessges do not appear on the service bus either. Both are published using:
Bus.Instance.Publish(object)
What am I doing wrong here?
Publishing messages without type information is a real struggle; the type information is hugely important for routing.
What I would look at doing here, if you must publish as object, is we have FastActivator helpers you can take a peek at (should be in the referenced Magnum library) that would be like Bus.Instance.FastActivator("Publish", message, { message.GetType() }). I might have the order of the parameters wrong, but you need the method name, parameters, and generic type parameters.
Additionally, I'd suggest joining the MT mailing list to help with this issue further if you need it. https://groups.google.com/forum/#!forum/masstransit-discuss

Categories