Transactions
Transactions are a way to group multiple operations into a single database commit.
Introduction
DQL transactions allow developers to execute multiple DQL statements in a single atomic operation. Using transactions ensures either all statements are executed successfully or none are executed at all, and provides consistency guarantees so that consecutive statements within a transaction operate on the same snapshot of the data. DQL read-write transactions are executed at serializable isolation level, which makes them suitable for use cases that require strong consistency guarantees.
Usage
Use the transaction()
method on DittoStore
to run a given block in a
read-write transaction:
The transaction is committed if the block returns .commit
and rolled back if
it returns .rollback
. The transaction will also be implicitly rolled back if
an error is thrown at any point inside the block.
You can alternatively return a value from a transaction using the second variant of the method. Returning any value will commit the transaction and throwing an error will roll it back:
Concurrency and Read-Only Transactions
Transactions are initiated as read-write transactions by default, so only a
single read-write transaction is being executed at any given time. Any other
read-write transaction started concurrently will wait until the current
transaction has been committed or rolled back. Therefore, it is crucial to make
sure a transaction finishes as early as possible so other read-write
transactions aren’t blocked for a long time. We recommend setting the hint
parameter for transactions, which improves the readability of log output related
to transactions and is especially helpful for debugging conflicts between
multiple transactions. Also see section Deadlock
Prevention for more information.
A transaction can be configured to be read-only using the options
parameter.
Multiple read-only transactions can be executed concurrently, including
concurrently with a read-write transaction. However, executing a mutating DQL
statement in a read-only transaction will throw an error.
Transaction Info
The closure passed to transaction()
receives a DittoTransaction
object. In
addition to the execute()
method, it also provides an info
property that
returns a DittoTransactionInfo
object. This object contains information about
the transaction, such as the transaction’s unique ID, the transaction’s hint,
and the transaction’s options.
Closing Ditto with Pending Transactions
Ditto waits for all pending transactions to complete before closing. Ditto is
closing when the close()
method is called, when the instance is garbage
collected, or when it goes out of scope.
The Flutter SDK does not support waiting for pending transactions to complete. Make sure to await all transactions’ completion before closing the Ditto instance.
Nested Non-Transaction Operations
Be aware that Ditto does not prevent running operations inside a transaction block without using the transaction object. This can lead to unexpected behavior and deadlocks and must be avoided.
Deadlock Prevention
Read-write transactions should only be used when necessary, as they can lead to deadlocks if not implemented correctly. This section provides some information on how to avoid and debug deadlocks and other performance issues when using read-write transactions. Note that the following does not apply to read-only transactions.
When a read-write transaction is started, a lock on the Ditto store is acquired and held until the transaction is committed or rolled back. Any attempt to start a new read-write transaction concurrently will wait until the current transaction is completed. It is important to avoid long-running operations inside a transaction block, as these prevent other transactions from starting which can lead to performance issues.
The transaction’s lock can also lead to deadlocks if the current transaction is waiting for the new transaction to complete. In particular, a read-write transaction must never be initiated from within another read-write transaction block. In this case the inner transaction will not start until the outer transaction is completed and the outer transaction will never complete because it is waiting for the inner transaction.
This caveat also applies to executing mutating DQL statements in a transaction block without using the transaction object. As each mutating DQL statement is run in its own transaction, this can lead to a deadlock. See the previous section Nested Non-Transaction Operations for an example.
Ditto keeps track of how long a transaction has been running and will start
logging warnings annotated with the transaction’s hint
every 5 seconds if a
transaction runs for more than 10 seconds. These time intervals can be
configured using the following system parameters:
transaction_duration_before_logging_ms
: The amount of time in milliseconds that a transaction can be running for before logging should begin.transaction_trace_interval_ms
: The interval of time in milliseconds between progressive trace statements
These warnings will be emitted with a progressively escalating log level,
starting at Debug
.
Replication of Changes from Transactions
In a Ditto mesh where peers share identical subscriptions, transaction atomicity and causal consistency is ensured by the replication process. However, if a peer subscribes to only a subset of the changes within a transaction, that peer will receive only the subscribed subset. This can lead to unexpected consequences if the peer later broadens its subscriptions or replicates these partial changes to other peers with broader subscriptions.
For the first scenario, consider a mesh with two peers. If Peer 1 modifies documents A and B, but Peer 2 subscribes only to changes on document A, then Peer 2 will receive only the changes to document A. If Peer 2 subsequently expands its subscriptions to include document B, it will not immediately have access to changes to B from the transaction; these changes will only become available once replication catches up with the extended subscription set.
A similar situation arises when replication occurs in stages across peers with varying subscription scopes. Suppose Peer 1 modifies both A and B, and Peer 2 subscribes only to document A. Peer 2 receives only changes to A. If Peer 3 is connected only to Peer 2 but subscribes to both A and B, it will still receive only the changes to A from the transaction since Peer 2 did not have access to changes on B. This can lead to unexpected behavior because Peer 3’s subscriptions includes both documents, but changes to B are missing due to Peer 2’s limited subscriptions. This issue can be mitigated by ensuring sufficient connectivity with peers that have broader subscription sets.
Error Handling
When an error is thrown at any point inside the transaction block or while
committing the transaction, the transaction is implicitly rolled back and the
error is propagated to the caller of transaction()
.
If errors occur in an execute()
call within a transaction block and the error
is caught and handled within the block, the transaction will continue to run and
not be rolled back.
When a transaction is configured to be read-only, the execution of mutating DQL
statements (UPDATE
, DELETE
, etc.) will throw an error. Unless this error is
caught, it will be propagated to the caller of transaction(:)
and the
transaction will be rolled back.
Limitations
Concurrent read-write transactions are not supported. If a read-write transaction is already in progress, any attempt to start a new transaction will wait until the current transaction is completed. See section Deadlock Prevention for more information. Transaction atomicity may be temporarily compromised if a mesh is set up such that changes are replicated to a peer with subscriptions covering only a portion of a transaction’s changes, and that peer then replicates to other peers with broader subscriptions or full coverage of the transaction. Additionally, if a peer initially subscribes to only part of a transaction’s changes and later extends its subscriptions, it won’t immediately receive the remaining changes from the original transaction. These additional changes will only become available once replication aligns with the new subscription scope. For more details, refer to Replication of Changes from Transactions section.