In my best effort to avoid Concurrency issues, concurrency errors still popup even after implementing this way of locking.
The payment provider sends out updates very quickly sometimes (even 3 times within a second sometimes), which makes it hard for the application to catch up because we want to do more operations. Then the user also comes back from the payment provider, executing the same code. In the end we have multiple requests coming from different sessions and this piece of code should make sure that there should only be one update at a time.
My code:
public InvoiceModel UpdateInvoicePaymentStatus(PaymentModel payment)
{
InvoiceModel invoice = _invoiceRepository.GetByMspId(payment.ExternalOrderId);
//lock per invoice Id, so we can have multiple different invoice updates, but single updates for one invoice
object miniLockInvoice = MiniUpdateLock.GetOrAdd(invoice.InvoiceId, new object());
LoggingManagement.AddOwnErrorObjects("outside lock, for id:" + invoice.InvoiceId, Log.LogLevel.INFO,
miniLockInvoice);
lock (miniLockInvoice)
{
Log.AddOwnErrorObjects("inside lock, for id:" + invoice.InvoiceId, Log.LogLevel.INFO,
miniLockInvoice);
invoice = _invoiceRepository.GetByIdWithCrudRelations(invoice.InvoiceId);//get again inside lock
//do a lot of logic, send out emails (we don't want to send multiple of course)
Log.AddOwnErrorObjects("UpdateInvoicePaymentStatus", Log.LogLevel.INFO, invoice, payment);
base.Update(invoice.InvoiceId, invoice);
object temp;
if (MiniUpdateLock.TryGetValue(invoice.InvoiceId, out temp) && temp == miniLockInvoice)
MiniUpdateLock.TryRemove(invoice.InvoiceId, out temp);
Log.AddOwnErrorObjects("inside end of lock, for id:" + invoice.InvoiceId + " removed item", Log.LogLevel.INFO,
temp);
}
Log.AddOwnErrorObjects("outside end of lock, for id:" + invoice.InvoiceId, Log.LogLevel.INFO);
return invoice;
}
Now inside the repository, eventually the following code gets executed (generic repo). Now keep in mind that the entity that is being retrieved has a timestamp
variable to avoid concurrent updates. Inside ModelToEntity
the timestamp is not changed or altered.
public virtual void Update(int id, TModel model)
{
TEntity entity = GetEntityById(id);
entity = ModelToEntity(model, entity);
DbContext.Entry(entity).State = EntityState.Modified;
DbContext.SaveChanges();
}
So as you can see I have put a lot of logs inside and outside of the lock to track down what happens.
+-----------------------------------------+-----------+----------+
| Event | session1 | session2 |
+-----------------------------------------+-----------+----------+
| outside lock, for id | 3:55:54 | 3:55:55 |
| inside lock, for id: | 3:55:54 | 3:56:00 |
| UpdateInvoicePaymentStatus | 3:56:00 | 3:56:08 |
| inside end of lock, for id: remove item | exception | 3:56:09 |
| outside end of lock, for id: | exception | 3:56:09 |
+-----------------------------------------+-----------+----------+
For this purpose I might be better of with recording milliseconds, but session2
enters the lock after session1
has passed the UpdateInvoicePaymentStatus
event but BEFORE throwing the exception, that also occurred on 3:56:00. So before calling the update database method.
Either the lock is not working properly or something else is going on what I do not understand, the exception trace shows that it occurred while updating the entities from within this code block (or path). I tracked down two sessions before these 2 and they nicely waited on each other by another lock on a higher level. That lock was only one static object.