5.7.1 Scope and Lifetime

A transaction groups registry operations so that they commit together or not at all. Installing a role writes service definitions, defaults and registry entries; a transaction is what makes that one event rather than a sequence of partially-applied ones.

reg_begin_transaction takes no arguments and returns a transaction fd. It contacts no source: it allocates an id, creates an anonymous inode, starts the lifetime timer, and returns. A transaction begins in the state REG_TXN_ACTIVE_UNBOUND.

5.7.1.1 Binding #

A transaction binds to a source on its first mutating operation. There are seven: writing a value, deleting a value entry, setting or removing a blanket tombstone, creating a key, deleting a key's path entry, hiding a key, and changing a Security Descriptor.

Reads never bind. A read passed an unbound transaction fd is sent to the source with a transaction id of zero and behaves as an ordinary non-transactional read.

Once bound, every operation on that fd must target the same hive. One that does not fails with EXDEV, and it fails in the kernel, before anything reaches a source — a source never sees a cross-hive operation inside a transaction. Cross-source atomicity would require two-phase commit and is not supported.

Binding identity is the pair of source and hive root GUID carried on the transaction object, which identifies the hive exactly.

5.7.1.2 Sources that do not support transactions #

Because reg_begin_transaction chooses no source, it cannot fail for lack of transaction support. The failure surfaces on the operation that would have bound: LCS sends RSI_BEGIN_TRANSACTION, the source answers RSI_TXN_NOT_SUPPORTED, and the operation returns ENOTSUP. The transaction stays ACTIVE_UNBOUND, its source binding untouched, and the caller can use the fd against a different hive.

There is no advance check of whether a source supports transactions. The answer comes from the source, on the attempt.

If the source is Down before the first bind, the binding operation fails EIO under the ordinary rules and the transaction likewise remains unbound. If a source goes Down after binding, the transaction becomes REG_TXN_SOURCE_DOWN (§5.8.5).

5.7.1.3 States #

StateValueMeaning
REG_TXN_ACTIVE_UNBOUND0Active, no source chosen.
REG_TXN_ACTIVE_BOUND1Active, bound to a source.
REG_TXN_COMMITTED2Commit completed.
REG_TXN_ABORTED3Explicitly or implicitly aborted.
REG_TXN_TIMED_OUT4The lifetime timer fired.
REG_TXN_SOURCE_DOWN5The bound source went Down.

REG_IOC_TXN_STATUS reports the state and a terminal_errno: 0 for COMMITTED, EINVAL for ABORTED, ETIMEDOUT for TIMED_OUT, EIO for SOURCE_DOWN, and 0 while active.

For COMMITTED, terminal_errno is not the errno that a further operation would return. A committed transaction reports 0, but using its fd again returns EINVAL.

5.7.1.4 Reaching a terminal state #

  • Commit. REG_IOC_COMMIT on the transaction fd. The source applies everything atomically, watch events fire, and the object becomes COMMITTED. Later use of the fd returns EINVAL. The fd should be closed.
  • Explicit abort. close() without committing. The source is told to discard, and the object becomes ABORTED during release. No events.
  • Implicit abort. Process death closes the fd, which aborts it. There are no orphaned transactions.
  • Timeout. The lifetime timer fires. See below.

The object stays addressable after reaching a terminal state, until the fd is closed. That is what makes REG_IOC_TXN_STATUS useful.

Transaction fds are pollable. Any terminal transition wakes poll waiters with POLLERR | POLLHUP. A caller that needs a race-free reason for the wakeup queries the status.

5.7.1.5 Timeout #

The lifetime timer starts when the fd is created — not at the first operation — and runs for TransactionTimeoutMs, default 30 seconds.

When it fires, the object becomes TIMED_OUT, poll waiters are woken, and further use of the fd returns ETIMEDOUT. If the transaction was bound and no commit is already in flight, RSI_ABORT_TRANSACTION is sent to the source. The fd is not removed from the caller's table; close() still releases it normally.

The timeout is not a convenience. Sources serialise writers — loregd does so through SQLite's WAL — so a stalled transaction blocks every other write to that source. The timer is what bounds the starvation window, and MaxBoundTransactionsPerSource (default 16) is what stops colluding processes from extending it indefinitely by binding fresh transactions at each timeout boundary. An operation that would bind past that cap returns EBUSY.

The cap is tested before the source is contacted, but after the transaction's mutation-log entry has been allocated; that entry is freed on the way out.

5.7.1.6 Constraints #

  • No nesting. Transactions are flat: no savepoints, no sub-transactions. The transaction fd accepts only REG_IOC_COMMIT and REG_IOC_TXN_STATUS; every other ioctl is ENOTTY.
  • One transaction per fd. A process may hold many transaction fds at once, but each is exactly one transaction.
  • Reads are permitted. A transaction is not write-only, which is what makes verify-then-write possible.

Edit this page