-
Notifications
You must be signed in to change notification settings - Fork 21
Anatomy of a transaction
Josip Bakić edited this page Aug 20, 2016
·
2 revisions
This text will lay out the steps involved in running a transaction. This is useful to understand, to have a clear picture of what runs when.
The outline of a transaction looks like this:
- Acquire a reading ticket, and prepare internal tracking structures
- Execute the transaction delegate
- If no writes were made anywhere, release everything and we're done
- All registered commit side effects get executed before returning, incl. sync ones.
- Run the PreCommit subscriptions that are interested in the fields that are about to be changed
- Run the commutes, if any
- Run the commit check:
- If successful, lock written fields and acquire a new version number for writing
- If not, roll back and start from 1.
- NB that only this step runs under a global lock
- If this is a RunToCommit call, we stop here. The caller receives a callback object, using which they can continue this process from the next step, from any thread in the system, or they can later choose to roll it all back.
- Run the sync side-effects, if any
- Run WhenCommitting subscriptions
- Commit
A commit is composed of these steps:
- Commit the fields, making the changes visible
- Release the write stamp lock and the reader ticket, and dispose of all transactional tracking structures and local data storage
- Run all non-sync commit side effects, and all Conditionals that are interested in the fields that were just written
And a rollback:
- Rollback all fields, clearing their local storage
- Release the reader ticket, and dispose of all transactional tracking structures and local data storage
- Run all rollback side effects
Some points worth noting:
- The PreCommit subscriptions get injected inside transactions, and they run before the check. This means they will run even in repetitions which will eventually fail and get retried. They may expand the footprint of the transaction they are injected into, and also note that they cause commutes over the fields they are interested in to degenerate! This is because commutes run under a strict regimen, they are not allowed to touch anything except the fields which they registered to commute. If your subscription could jump into the commute sub-transaction, it could see inconsistent state, and it therefore forces the commutes to run before, inside the main transaction. But this only happens for those fields that have a PreCommit interested in them.
- The Conditionals run after the main transaction has fully committed, and they each open their own transaction. This means other threads may manage to execute transactions between our transaction and its Conditionals. This effectively means that the conditionals are not a mechanism that "promptly" reacts to every change. You are only guaranteed that your conditional will run some short time after a change happens. But of course, the conditional is consistent with itself, i.e. the test function is guaranteed to hold true whenever the main conditional delegate is running!
- WhenCommitting subscriptions do react to every change. Since the written fields remain locked when they run, these subscriptions are guaranteed to run synchronously to every change. For example, if you add things into a queue from these subscriptions, items will be added in this queue in exactly the same order as they were committed. But it's also important to understand that they run under a field-lock, not the global lock! That means they may run concurrently if the writing sets of their transactions do not overlap. Generally, these subscriptions are very "low level", and should be written carefully.
- Exception handling:
- Any unhandled exceptions coming from the transaction delegates (those passed to InTransaction or RunToCommit) cause an immediate rollback, and the exception bubbles out of the InTransaction or RunToCommit call.
- Exceptions in PreCommit delegates are wrapped into an AggregateException, and bubble out of the InTransaction or RunToCommit call. It's important to note that all interested PreCommits will get to run, even if some throw!
- WhenCommitting exceptions are also wrapped in an AggregateException, and they bubble out of InTransaction calls, but never from RunToCommit calls. If using RunToCommit, the WhenCommitting subscriptions execute when you call Commit on the continuation object. So, if they throw at that point, the AggregateException will bubble out of the Commit call and the transaction will have been rolled back. In either case, they too will all get the chance to run, even if some of them throw.
- Put more simply, do not let any exceptions bubble out of your WhenCommitting subscriptions!
- A possible future change is that exceptions from WhenCommitting subscriptions will not be able to cause a commit to fail. They all get a chance to run, and if just one of them throws, and others succeed and leave their effects elsewhere, it seems more appropriate to keep the committed data inside Shielded fields.
- Exceptions in non-sync side effects or conditionals (so, all the mechanisms triggered after a transaction is complete) are also aggregated together in an AggregateException, and bubble out of InTransaction calls, or Commit calls on a continuation, but in this case the main transaction has already been committed. Likewise, they all get to run, even if some throw. (NB that this means that a transaction that enlists a throwing side effect cannot cause the conditionals to get skipped!)
- ThreadAbort exceptions - the library is written to be safe against them, but this would currently need more testing to confirm. All critical steps are performed in finally blocks, and the CLR guarantees not to interrupt those with ThreadAborts. Most notably, the main transaction running method, Shield.TransactionRun will try to do a full rollback in it's finally block, if it is reached and the transaction still remains open. However, they may jump into unexpected parts of the process, and interrupt many things. E.g. they may be thrown in between two side effects, or between them and conditionals. We protect against them during a write, however, because otherwise we could leave part of the writes visible, and part lost.