Next: Multi-repository Operation, Previous: Multi-threaded Applications, Up: User Guide
Transactions are dynamic contexts in which all side effects to persistent slots and other persistent objects such as BTrees are guaranteed to have the ACID properties: atomicity, consistency, isolation and durability. On a normal exit from context, the side effects are committed as a group. On a non-local exit, the transaction is aborted.
For most users, the tutorial section Using Transactions is the best introduction to transactions. This section adds to that by exposing some of the details of how it is implemented.
To reiterate, there are a few important restrictions to adhere to:
*current-transaction*
is reserved for use by the transaction system. Users should not override, manipulate or close over this variable.
with-transaction
internalsThe with-transaction
macro wraps the body expression with an
anonymous lambda expression. This closure is passed to a call to the
execute-transaction
generic function which is specialized to
the current data store.
The only bookkeeping done by the macro is ensuring that the
:parent
argument is checked for the current dynamic transaction
context. If it is not owned by the default or provided store
controller, then it is not passed to execute-transaction
. This
maintains a continuous dynamic stack transactions through the
with/ensure transaction macros, but allows for a single leaf
transaction to another store controller.
Be very careful about mixing transactions between store controller. This facility was only added to ensure that migrate worked correctly.
The macro processes keywords arguments :store-controller
(defaults to *store-controller*
), :parent
(defaults to
*current-transaction*
) and :retries
and passes the
remaining keywords to the call to execute-transaction
allowing
the user to pass data store specific transaction keywords to their
preferred data store. The consumed keywords are analyzed and then
passed on to execute-transaction
.
Any non-standard keywords for a given data store will be ignored by
other data store implementation of execute-transaction
so
portable programs should not use keywords that change the semantics of
the transaction.
ensure-transaction
only calls execute-transaction
if
it needs to create a fresh transaction. If the transaction in
*current-transaction*
exists and belongs to the store controller
passed to ensure-transaction
then it merely calls the transaction
closure, relying on the environment that created the transaction to
handle any exit procedures and determining whether to abort or commit.
*current-transaction*
contains transaction records during the
dynamic execution of a transaction. These records capture any data
store specific bookkeeping as well as the store-controller that the
transaction is associated with.
execute-transaction
internalsSee the Elephant Architecture section for details on how execute-transaction works. It will provide some deeper insight into the transaction system.
Data stores are required to implement three primitive transaction
methods: controller-start-transaction
,
controller-abort-transaction
and
controller-commit-transaction
. These are wrappers for the data
store's primitive transaction mechanism. If you use these, it is up
to you to make sure that you properly manage nested transactions,
maintain the state of *current-transaction*
handle any
automated retries you might want, and handle detecting
If you use these, you are on your own - it is easy to make mistakes with
transactions and create very complex bugs that are hard to track down.
Most users are much better off sticking with the two transaction macros
and the underlying execute-transaction
method.
You can trace elephant::execute-transaction
to see the sequence
of calls that occur dynamically and detect where and how many
transactions are and are not happening.