Multi-mount transactions#

A single client Patch can touch paths owned by different owners at once — some base (logd), some under one mount, some under another. Each owner must write its own subtree, yet the whole write must land atomically. docd achieves this by decomposing the patch and applying the parts as one multi-participant transaction.

Static decomposition#

docd splits a client patch, by path, into:

  • a per-mount sub-patch for each mount the patch touches, rooted at that mount's path and sent to its controller; and
  • base writes for everything left over, applied by docd over its own logd link.

The split is static — computed from the patch's structure without reading current state. Each base write sits at a path that is not an ancestor of any mount, so no two participants' paths overlap and the transaction merge (which rejects a patch whose path is a prefix of another's) is never triggered.

Some tags sitting on a node above a mount boundary would make a purely structural split ambiguous — the tag's effect could depend on the mounted content docd does not have. Such a patch is rejected rather than mis-split, keeping decomposition correct-by-construction.

Multi-participant transactions#

logd transactions are counted by participants: a transaction id is allocated for a known number of writers, and logd commits it only once every participant has written. docd uses this to coordinate a spanning patch:

  1. Allocate a transaction id for N participants (one per mount part plus base).
  2. Hand each participant the id; each writes its part against it.
  3. logd commits atomically when the last participant's write arrives.

If any participant fails, the transaction does not commit — the client sees a single failure rather than a partial write.

The transaction is created on logd for the write, with logd's tx.timeout, and no participant names a timeout of its own: each is answered when the transaction resolves, so the client is told exactly what the transaction did. (docd once kept a pool of transaction ids fetched in advance; a transaction's timeout runs from its creation, so a pooled id was a transaction dying in the hand.)

A single-owner write needs no transaction id at all — it is forwarded straight to its owner. A client's own newtx goes to logd on the client's connection.

Scopes#

A client's copy-on-write scope, set once in Hello, is threaded through every operation so that reads, writes, and watches all see the same isolated view:

  • base operations carry the scope to logd; and
  • controller operations carry the scope in the request, so one multiplexed mount connection serves every client scope without a connection per scope.

Scoped writes go to the scope without touching baseline, and scoped reads see the scope's data overlaid on baseline (copy-on-write). DeleteScope — dropping a scope and all its data — is a baseline-only operation; a scoped session cannot delete scopes.

Authors#

A write's author — the author on the patch, else the one on the client's Hello — is resolved by docd wherever the write leaves the client's own logd link, for the same reason scope is: a controller's write, and the transaction a split write becomes, commit on sessions whose hello is not the client's. A stand-alone patch routed to a controller carries it, and the controller relays it to its logd write. A split write's transaction is created with it, and its participants inherit it as any transaction's do, so the one commit is recorded under the client's author whichever mounts it crossed. A client's own transaction is written by the author its newtx named. See Who wrote it.