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.
Use the transaction() method on DittoStore to run a given block in a
read-write transaction:
Copy
Ask AI
let completionAction = try await ditto.store.transaction(hint: "transaction example") { transaction in let queryResult1 = try await transaction.execute(/* query 1 */) let queryResult2 = try await transaction.execute(/* query 2 */) // ... let queryResultN = try await transaction.execute(/* query N */) // Complete by returning a `TransactionCompletionAction` return .commit // Or .rollback}
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:
Copy
Ask AI
let result: DittoQueryResult? = try await ditto.store.transaction(hint: "transaction returning a value") { transaction in let queryResult1 = try await transaction.execute(/* query 1 */) let queryResult2 = try await transaction.execute(/* query 2 */) // ... let queryResultN = try await transaction.execute(/* query N */) return queryResultN}
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.
Copy
Ask AI
// Read-only transaction with hint:try await ditto.store.transaction(hint: "Read-only transaction", isReadOnly: true) { transaction in // ... return .commit}
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.
Copy
Ask AI
try await ditto.store.transaction { transaction in let id = transaction.info.id let hint = transaction.info.hint let isReadOnly = transaction.info.isReadOnly // ... return .commit}
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.
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.
Copy
Ask AI
try await ditto.store.transaction { transaction in try await transaction.execute(/* query 1 */) // ✅ OK try await ditto.store.execute(/* query 2 */) // ⚠️ Avoid this return .commit}
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.
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.
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.
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.