Hi, I have an app service(inheriting from the ProjectAppServiceBase), and have such method:
public async Task<int> DoSomething(InputDto input){ //do some db operations await myManager.Create(myEntity); await CurrentUnitOfWork.SaveChangesAsync();
//send long lasting job to rabbitMQ await masstransitJobSender.PublishAsync( myEntity.Id, "MessageNameHere", CancellationToken.None ); }
Here i do sent myEntity.Id as the argument and expect it to be present in db, right? the rabbitMq consumers complain about entity not being in db: "There is no such entity. entity type: xyz.abc.project.myEntity, id: 333
What am i missing here? isnt saving changes on current unit of work enough here? the consumer works just fine in its second try (which is five seconds later), and finds the entity. But in its first try it throws the mentioned error in most cases. Say i called the DoSomething method 500 times, i get "Entity can not be found" error in maybe 200 of them.
I also tried wrapping the myManager.Create code in a using(var uow = CurrentUnitofwork.begin()) {} but this doesnt seem to work either.
I was hoping that the simple await CurrentUnitOfWork.SaveChangesAsync(); call would be enough to commit the transaction.
Any help would greatly be appreciated.
thank you
1 Answer(s)
-
0
Hi @uenlkr4e
The issue you're facing is a classic race condition that frequently occurs when integrating transactional operations with asynchronous messaging systems like RabbitMQ, within frameworks like ASP.NET Boilerplate (ABP) that manage transactions for you.
Your intuition is correct, but you're missing a subtle detail about how the Unit of Work (UoW) transaction is committed.
In ASP.NET Zero, public methods of an AppService are wrapped in a database transaction by default. This is managed by the Unit of Work. Here’s the sequence of events in your
DoSomethingmethod:- When
DoSomethingis called, ABP automatically begins a database transaction. myManager.Create(myEntity)marks your new entity to be added to the DbContext in memory.await CurrentUnitOfWork.SaveChangesAsync()flushes these changes from memory to the database. However, the transaction is still open! The data has been written, but it is not yet visible to any other database connection or transaction.await masstransitJobSender.PublishAsync(...)sends your message to RabbitMQ immediately.- Your RabbitMQ consumer receives the message almost instantly and attempts to query the database for the entity using its Id.
- Meanwhile, your DoSomething method has not finished yet. Only after the method completes successfully (i.e., after the return statement), does ABP finally commit the transaction.
The race condition happens right here: The consumer's database query (Step 5) often runs before the main transaction is committed (Step 6). Since the consumer is operating in a new, separate transaction, it cannot see the uncommitted data, which results in the "Entity not found" error.
It works on the second try (5 seconds later) because by then, the original transaction from your DoSomething method has had plenty of time to be committed, making the data visible to all other connections.
The correct, framework approved way to solve this is to defer the message publishing until after the Unit of Work has successfully completed. ABP provides the
CurrentUnitOfWork.Completedevent for exactly this purpose. This event is triggered immediately after the transaction has been successfully committed.You should refactor your code like this:
public async Task<int> DoSomething(InputDto input) { await myManager.Create(myEntity); await CurrentUnitOfWork.SaveChangesAsync(); CurrentUnitOfWork.Completed += (sender, args) => { // Note: This event handler doesn't natively support async-await, so this is often // treated as a "fire-and-forget" call. Be mindful of how you handle potential errors here. _masstransitJobSender.PublishAsync( myEntity.Id, "MessageNameHere", CancellationToken.None ); }; return myEntity.Id; }Reliability: It guarantees that your message is only sent after the data has been durably persisted in the database. Atomicity: If any part of your
DoSomethingmethod fails after you save changes but before it completes, the transaction will be rolled back. The Completed event will never fire, and no incorrect message will be sent to RabbitMQ, which keeps your system consistent.In short,
SaveChangesAsync()saves the data but does not commit the transaction. To perform an action after the transaction is committed, you must use theCurrentUnitOfWork.Completedevent.Hope this helps clarify the issue and solve your problem. Thank you.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) - When