Unsure if I am understanding pessimistic locking functionality correctly

I am using NHibernate persistence which I understand utilizes pessimistic locking. I am fairly new to distributed systems and am unsure if my understanding of this behavior is correct from the docs.

My understanding is basically that in multiple host/thread scenarios, a handler in a given saga can only be invoked by one thread/host at a time, and is “locked” before it begins to run the handler method, insuring that no data is edited concurrently/with conflicts. Messages attempting to run the same handler while one is locked are sent to retry queue. This is the default behavior with no configurations set for NHibernate persistence.

Is this understanding correct?

Please and thank you,
Matt

Hi @MattN42

Yes, the saga row in a database is locked before the handler executed. In most cases (happy cases) this guarantees that only one thread in one process on one host can execute the message handling logic. This is not a 100% guarantee, though. It might happen that this thread that started executed saga logic happens to be on a machine that loses connectivity with the DB server. Once the DB detects lost connection it may release the locks held on that row and another thread on another machine can start executing.

This is why the NHibernate persister uses optimistic concurrency (in addition to pessimistic) to make sure that only one of these threads can successfully persist the state change.

You can think about is like this: optimistic concurrency is for correctness, pessimistic concurrency is for performance. It is important because when a saga row is locked for processing one message, other messages for that saga wait on that lock. They are not sent for retries but wait in-memory for their turn. This makes allows the NHibernate saga persister to handle 100s of messages per second in a single saga instance. If pessimistic concurrency was not in place, all these other messages would fail and would be subject to retries, grinding the system to halt.

2 Likes

I am a little confused. Isn’t optimistic for performance and pessimistic for correctness? I thought when there is low data contention, a system might not rely on locks but rather checks to see if the data has been modified before committing the transaction, and therefore it would be considered optimistic. And then when you expect there to be more contention, or you are being pessimistic, you might choose to some sort of locking mechanism to ensure only one process has write access at a time. So it seems like everything about your answer makes sense if I swap the terms optimistic and pessimistic. Is there something I am not understanding? It seems that the handling of messages by a saga uses optimistic concurrency, but the creation of sagas uses pessimistic concurrency, since the uniqueness of a correlation ID is enforced. Is this fair to say?

Yes, that is true

And then when you expect there to be more contention, or you are being pessimistic, you might choose to some sort of locking mechanism to ensure only one process has write access at a time

That is also true. I think I was actually wrong when I mentioned that pessimistic concurrency control in NH persistence is not 100% reliable. After looking at the code I realized it does use the same connection/transaction to load and then store the saga so if that connection is broken and DB releases the lock, the thread that uses it would not be able to commit its transaction.

So to summarize, both optimistic and pessimistic concurrency controls in NServiceBus NHibernate saga persister provide mutual exclusion guarantees.

Regarding the saga creation, this a bit blurry (at least to me). As you mentioned, NServiceBus relies on the uniqueness of the correlation ID. If you assume that the difference between optimistic and pessimistic concurrency control is the fact that pessimistic will block the thread until it can safely proceed while optimistic will always let you go but then might fail, the creation is a mixture of these.

Assuming ReadCommitted defualt transaction isolation mode, having two threads trying to insert a row with the same correlation ID results in one of them waiting for the row lock until the other is done committing the insert. Then the first thread in unblocked and proceeds with its attempt to insert which results in a failure. So it looks like the creation combines weak points of both concurrency approaches. That means that if you expect a high contention situation in your saga, a better design would be to create the saga before.