Bedrock.ControlPlane.Distributor.Transactions (bedrock v0.7.0)

View Source

The distributor's fenced system transactions.

Every mutating commit is built the FDB way: read the lock state at a pinned version, evaluate the Lock fence, and commit the fence's mutations WITH read conflicts on the lock keys at that version — so a concurrent take conflicts with this commit inside the pipeline itself. A commit abort is therefore an authoritative supersession verdict (the replacement for phase-a's director-side delta rejection); transient read/commit failures are surfaced as themselves and are not verdicts.

Reads resolve through the same channel clients use: a commit proxy's covering entry names the system-shard materializer, and the versioned read happens there. The keyspace is the channel, for the distributor too.

Summary

Types

The injectable dependency set. deps_for/4 builds the production wiring; tests script the three seams directly.

Functions

A CHECK-fenced mutating commit, honoring the Lock runner obligations: the owner key is read (and read-conflicted) always; the write key is read only when the owner is not ours — an unconditional write-key read would make every pair of concurrent same-owner distributor transactions mutually conflict and serialize.

Production dependencies: read versions from the epoch's sequencer, reads resolved by-key through a commit proxy to the covering materializer, commits through a random proxy.

The read-only poll-to-die verdict: reads both lock keys at a fresh version and mirrors Lock.poll/3. A failed read is :unavailable — not a verdict; the poll loop simply tries again on its next tick.

Reads the distributor's snapshot of the durable mapping families — shard layout and materializer refs — at one pinned version (a single-version multi-page read cannot tear: the families are written transactionally). Read AFTER the lock is taken, under FDB's lock-first-snapshot-second startup order.

Takes the distributor lock: reads both lock keys at a pinned version (FDB's take reads both — the remembered write UID is the unobserved-take evidence), claims the owner key, and commits with read conflicts on both keys at that version.

Types

deps()

@type deps() :: %{
  epoch: Bedrock.epoch(),
  proxies: [Bedrock.DataPlane.CommitProxy.ref()],
  next_read_version_fn: (-> {:ok, Bedrock.version()} | {:error, term()}),
  get_fn: (Bedrock.key(), Bedrock.version() ->
             {:ok, binary()}
             | {:error, :not_found | term()}
             | {:failure, term(), term()}),
  commit_fn: (Bedrock.DataPlane.CommitProxy.ref(),
              Bedrock.epoch(),
              binary(),
              keyword() ->
                {:ok, Bedrock.version(), non_neg_integer()} | {:error, term()}),
  get_range_fn: (Bedrock.key(), Bedrock.key(), Bedrock.version() ->
                   {:ok, {[{Bedrock.key(), binary()}], boolean()}}
                   | {:error, term()}
                   | {:failure, term(), term()})
}

The injectable dependency set. deps_for/4 builds the production wiring; tests script the three seams directly.

Functions

commit_checked(lock, deps, payload_mutations)

@spec commit_checked(Bedrock.ControlPlane.Distributor.Lock.t(), deps(), [
  Bedrock.ControlPlane.Distributor.Lock.mutation()
  | {:set, Bedrock.key(), binary()}
  | {:clear, Bedrock.key()}
]) ::
  :ok
  | {:error, :superseded}
  | {:error,
     {:read_version_failed | :lock_read_failed | :lock_commit_failed, term()}}

A CHECK-fenced mutating commit, honoring the Lock runner obligations: the owner key is read (and read-conflicted) always; the write key is read only when the owner is not ours — an unconditional write-key read would make every pair of concurrent same-owner distributor transactions mutually conflict and serialize.

Verdict semantics mirror FDB exactly: supersession is the READ verdict (Lock.check's refusal — FDB's movekeys_conflict), and it is authoritative. A commit ABORT is not: it retries with a fresh read version (re-evaluating the fence, so a genuine usurper is caught by the read on the retry); exhausted retries surface as a transient commit failure.

deps_for(cluster, epoch, sequencer, proxies)

Production dependencies: read versions from the epoch's sequencer, reads resolved by-key through a commit proxy to the covering materializer, commits through a random proxy.

poll_verdict(lock, deps)

@spec poll_verdict(Bedrock.ControlPlane.Distributor.Lock.t(), deps()) ::
  :ok | :superseded | :unavailable

The read-only poll-to-die verdict: reads both lock keys at a fresh version and mirrors Lock.poll/3. A failed read is :unavailable — not a verdict; the poll loop simply tries again on its next tick.

read_snapshot(deps)

@spec read_snapshot(deps()) ::
  {:ok,
   %{
     shard_layout: %{
       required(Bedrock.key()) => {Bedrock.range_tag(), Bedrock.key()}
     },
     materializer_refs: %{
       required(Bedrock.range_tag()) => %{required(String.t()) => String.t()}
     }
   }}
  | {:error, term()}

Reads the distributor's snapshot of the durable mapping families — shard layout and materializer refs — at one pinned version (a single-version multi-page read cannot tear: the families are written transactionally). Read AFTER the lock is taken, under FDB's lock-first-snapshot-second startup order.

take_lock(deps)

@spec take_lock(deps()) ::
  {:ok, Bedrock.ControlPlane.Distributor.Lock.t()}
  | {:error,
     {:read_version_failed | :lock_read_failed | :lock_commit_failed, term()}}

Takes the distributor lock: reads both lock keys at a pinned version (FDB's take reads both — the remembered write UID is the unobserved-take evidence), claims the owner key, and commits with read conflicts on both keys at that version.

An abort at TAKE time is not a verdict — FDB's takeMoveKeysLock retries not_committed/too_old with a fresh read version, because an abort here can also mean the read version fell below the resolver's pruning floor. Take semantics are last-take-wins: the re-take reads the interleaved winner as the new previous owner and claims over it; supersession is delivered where it is authoritative — by the CHECK fence on mutating transactions and the poll loop. Exhausted retries surface as a transient commit failure for the director's recruit-retry.