Expand description
Frontend sequencing for read-then-write operations.
This module implements INSERT […] SELECT FROM […], DELETE and UPDATE operations using a subscribe with optimistic concurrency control (OCC), sequenced from the session task rather than the Coordinator.
The motivation is correctness with concurrent writers, including writers in
different environmentd processes, which the coordinator’s in-process write
locks cannot provide. The OCC path also fixes a serializability defect of the
lock path, which reads at one timestamp and commits at a later one while
locking only the selection’s direct dependency items. See the design doc,
doc/developer/design/20260210_incremental_occ_read_then_write.md, and the
comment on the retry arm in run_occ_loop. Relieving the coordinator loop is
a side benefit, and only sequencing moves off it. The subscribe’s data path
still runs through the coordinator.
§Whether the write reads persisted state
Two predicates answer that one question, and they have to agree. Before
anything runs, SessionClient::try_frontend_read_then_write decides it
syntactically, from depends_on() on the planned selection, because inside
a transaction a read-dependent write has to be refused while refusing is
still possible. Once the dataflow runs, the subscribe answers it
dynamically: the channel closes on its own only once the sink’s output
frontier reaches the empty antichain, which means the selection can never
change again.
Reading nothing persisted is the common case for a clean close, not the
guarantee. An input whose frontier seals closes cleanly too, despite reading
persisted state, for example a REFRESH AT materialized view past its last
refresh, whose write frontier advances to the empty antichain. What holds in
either case is the property the write side actually needs: past the close,
the consolidated diffs are frontier-independent. The syntactic predicate is
correspondingly stricter than the dynamic one, since it refuses a sealed-MV
INSERT ... SELECT in a transaction that would technically be bufferable.
The two answers are used for different things, and that separation matters.
The syntactic answer decides whether the statement can belong to a transaction. A selection that reads nothing produces diffs that are valid at any timestamp, so they are staged as session write ops and land when the transaction commits, which is what makes the statement atomic with whatever surrounds it. A selection that reads persisted state cannot belong to a transaction, and we refuse it in an explicit one. An extended-protocol pipeline is an implicit transaction, so it must not quietly join one either: it ends its own transaction instead of spanning the rest of the pipeline, which is how PostgreSQL treats statements that cannot run in a transaction block. It is durable once it reports success, and a later failure in the pipeline does not undo it.
The dynamic answer only decides how the write is submitted, a timestamped
write from inside the loop or a blind submission after it. It cannot decide
transaction membership, because it is a property of the inputs rather than
of the statement. A sealed input closes the subscribe cleanly, so an
INSERT ... SELECT over a REFRESH AT materialized view past its last
refresh takes the blind exit while still reading persisted state. Its diffs
really are frontier-independent and staging them would be safe, and we still
do not stage them. Otherwise whether a statement’s rows survive a failure
later in the pipeline would depend on whether one of its inputs happened to
pass its last refresh, which no one reading the statement could predict.
Staging is also what earns the right to span a pipeline in the first place.
TransactionStatus::may_span_pipeline lets an implicit transaction stay open
only for writes, precisely because they are merely staged. A statement that
committed on its own has no business claiming it.
Disagreement is caught on both sides, and only one side can still refuse.
frontend_read_then_write re-checks the syntactic predicate before running a
dataflow, which catches a caller that skipped the gate. If the syntactic
predicate were laxer than the dynamic one, that check would pass and a write
meant for staging would commit on its own, so the loop asserts wherever the
two answers can be compared. The Committed arm catches a write timestamp
for a statement we meant to stage, and the zero-row arm catches a read
timestamp for one. Neither can undo anything by then, the write is already
durable in the first case and there was never anything to write in the
second, so all they do is make the disagreement loud.
§The frontier certifies, the oracle chooses
The target T comes from the oracle, and a progress message at F only
certifies completeness below F, so it gates the write rather than choosing
its timestamp. The design doc’s “The OCC loop” says why. Three invariants
hold for every write this path makes:
F >= Tbefore it submits, so the payload is a complete view ofT - 1.- The payload is every diff below
T, strictly. A diff atTis concurrent with the write and waits for a later target. T > as_of, so the snapshot, which arrives atas_of, is in the payload.
NOTE: F >= T does not make the two equal, because F is a minimum over the
selection’s inputs. Where F runs above T, a selection that reads the
target table makes the compare-and-append refuse, and retrying higher is the
design rather than a failure.
§A zero-row answer
A write linearizes itself, because group commit advances the oracle before it
acknowledges. An answer of “no rows” performs no write, so the loop reports
the timestamp its view is complete through and the caller waits for the
oracle to reach it. A selection empty at as_of is complete through as_of,
which the caller put behind the oracle before the subscribe started, so that
answer waits for nothing, while reporting the frontier the subscribe observed
would cost a group commit every time. The design doc’s “Linearization” argues
why the lower timestamp is not the weaker guarantee.
§Rollout note
The FRONTEND_READ_THEN_WRITE dyncfg is read once at process startup and
fixed for the lifetime of the environmentd process. This avoids a
mixed-mode window where both the lock-based coordinator path and this OCC
path are active concurrently. The coordinator path acquires write locks to
prevent concurrent writes between its read and write phases, but this OCC
path does not use write locks, so concurrent operation of both paths could
allow an OCC write to slip between a coordinator-path reader’s read and
write.
Structs§
- Frontend
Write 🔒Attempt State - State shared between an in-flight frontend write attempt and its
cancellation wrapper,
SessionClient::try_frontend_read_then_write_with_cancel. - OccState 🔒
- Accumulated state for the OCC loop in
run_occ_loop. - Subscribe
Handle 🔒 - A handle to an internal subscribe, meaning one that writes no
mz_subscriptionsrow. ADropimpl ensures the subscribe’s dataflow is cleaned up when dropped. - Validation
Result 🔒 - Result of validating a read-then-write operation.
Enums§
- Frontend
Write 🔒Cancellation - Reason a frontend write attempt is being torn down early.
- OccOutcome 🔒
- What the OCC loop produced.
- Process
Result 🔒 - Result of processing a single subscribe message in the OCC loop.
- Write
Outcome 🔒 - What the coordinator’s answer to a submitted write means for the statement.
Functions§
- apply_
mutation_ 🔒to_ mir - Transform a MIR expression to produce the appropriate diffs for a mutation.
- build_
no_ 🔒rows_ response - Build the response returned when no rows matched the selection.
- build_
success_ 🔒response - Builds the response for a write that is about to be submitted.
- classify_
write_ 🔒result - Maps a
WriteResultto the outcome the statement reports, or to the one conflict the OCC loop can retry. - contains_
mz_ 🔒now - Whether a read-then-write mentions
mz_now()anywhere. - empty_
as_ 🔒of - The timestamp an answer holds as of, given a view complete strictly below
complete_below. - end_
own_ 🔒transaction - Ends the implicit transaction that a statement which cannot run in a transaction block opened for itself.
- governing_
timeline 🔒 - The timeline whose oracle governs a read-then-write with the given read-side
TimelineContext. - process_
message 🔒 - Process one subscribe message, updating
statein place. - validate_
read_ 🔒then_ write - Validates a read-then-write and resolves the context the rest of the pipeline runs against.
- validate_
selection_ 🔒dependencies - Checks that a read-then-write may read what its selection depends on.