Skip to content

adapter: let the group committer write at a caller-chosen timestamp - #37921

Open
aljoscha wants to merge 1 commit into
mainfrom
aljoscha/occ-04-group-commit
Open

adapter: let the group committer write at a caller-chosen timestamp#37921
aljoscha wants to merge 1 commit into
mainfrom
aljoscha/occ-04-group-commit

Conversation

@aljoscha

@aljoscha aljoscha commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Motivation

Part 4 of 7 in a stack that moves DELETE, UPDATE and INSERT ... SELECT
off the coordinator onto the session task, using optimistic concurrency
control. Design doc:
20260210_incremental_occ_read_then_write.md
(lands in part 6).

The group committer picks a write timestamp from the oracle, which is right
for a write whose diffs are valid whenever they land. A write computed
against a snapshot is not like that. Its diffs are only correct at the
frontier they were read at, so the timestamp has to come from the caller and
the committer has to be able to refuse it.

Closes SQL-590

Description

TableWriteCmd::TimestampedWrite carries a caller-chosen timestamp and gets
exactly one attempt. An InvalidUppers means another writer holds the upper
at or past that timestamp, so the snapshot the diffs were computed from is
stale, and the answer is TimestampPassed. Never a retry at a fresh
timestamp: retrying stale diffs later is precisely the lost update this
exists to prevent. Deciding what to do about a conflict is the caller's job,
and it needs a new snapshot to do it.

The request also names the GlobalId it was computed against, and the
committer refuses it if the table's latest generation has moved on. That is
how a concurrent ALTER TABLE is caught rather than applied to rows of the
wrong arity.

Both txns-shard write paths now share attempt_write_to_txns, so the catalog
upper advance, the metric, the InvalidUppers classification and the oracle
apply exist once. The two differ only in what they do with a conflict, which
is the difference worth seeing.

The result travels back through a responder whose Drop reports
Indeterminate. Without that, a dropped sender on shutdown would panic the
waiting session task instead of failing its statement.

Temporary scaffolding. Nothing calls this until part 6, so a
dead_code allowance on the unreached items keeps the crate warning-free.
Part 6 removes it in the same commit that adds the callers.

Verification

No caller yet, so this part is carried by the tests in parts 6 and 7, and by
existing coverage showing the shared attempt_write_to_txns did not change
ordinary group commit behavior.

@linear-code

linear-code Bot commented Jul 29, 2026

Copy link
Copy Markdown

SQL-590

@aljoscha
aljoscha force-pushed the aljoscha/occ-04-group-commit branch 2 times, most recently from 6e84c8e to b92c253 Compare July 29, 2026 14:43
@aljoscha
aljoscha force-pushed the aljoscha/occ-04-group-commit branch from b92c253 to 2b7f53b Compare July 30, 2026 06:02
@aljoscha
aljoscha force-pushed the aljoscha/occ-04-group-commit branch from 2b7f53b to 8f93d7b Compare July 30, 2026 06:24
@aljoscha
aljoscha force-pushed the aljoscha/occ-04-group-commit branch from 8f93d7b to b3630c1 Compare July 30, 2026 06:30
@aljoscha
aljoscha force-pushed the aljoscha/occ-04-group-commit branch 2 times, most recently from 57395d7 to 41ef569 Compare August 5, 2026 12:13
Base automatically changed from aljoscha/occ-03-statement-logging to main August 5, 2026 13:05
@aljoscha
aljoscha requested a review from a team as a code owner August 5, 2026 13:05
The group committer picks a timestamp from the oracle, which is right for
a write whose diffs are valid whenever they land. A write computed against
a snapshot is not like that: its diffs are only correct at the frontier
they were read at, so the timestamp has to come from the caller.

`TableWriteCmd::TimestampedWrite` carries that timestamp and gets exactly
one attempt. An `InvalidUppers` means another writer holds the upper at or
past it, so the snapshot the diffs were computed from is stale and the
answer is `TimestampPassed`, never a retry at a fresh timestamp. Retrying
stale diffs later is precisely the lost update this exists to prevent.

The write also names the `GlobalId` it was computed against, and the
committer refuses it if the table's latest generation has moved on, which
is how a concurrent `ALTER TABLE` is caught rather than applied to rows of
the wrong arity.

Both txns-shard write paths now share `attempt_write_to_txns`, so the
catalog upper advance, the metric, the `InvalidUppers` classification and
the oracle apply exist once. The two differ only in what they do with a
conflict, which is the difference worth seeing.

The result travels back through a responder whose `Drop` reports
`Indeterminate`. Without that, a dropped sender on shutdown would panic
the waiting session task instead of failing its statement.

Callers arrive later in the stack. Until they do, a `dead_code` allowance
on the unreached items keeps the crate warning-free, and it goes away with
the commit that adds them.
@aljoscha
aljoscha force-pushed the aljoscha/occ-04-group-commit branch from 41ef569 to a07c57b Compare August 5, 2026 13:05

@ggevay ggevay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, very clean scaffolding. One description question, plus some small inline comments (mostly optional doc/comment nits).

Description accuracy: "The request also names the GlobalId it was computed against, and the committer refuses it if the table's latest generation has moved on." As far as I can tell, this refusal exists only for the blind path (WriteTarget on UserWriteResponder::Internal) and happens during commit staging on the coordinator, not in the committer. TimestampedWriteRequest carries pre-resolved GlobalIds and relies on its (part 6) submitter's catalog check plus committer-channel FIFO instead. If that reading is right, could you tweak the paragraph? Precision here matters for reviewing parts 6 and 7.

}

/// An OCC write whose diffs are valid only at `target_timestamp`.
pub(crate) struct TimestampedWriteRequest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the doc also state the submission contract? These GlobalIds reach the table-write worker unvalidated, and a non-empty append for an id whose handle a Forget already dropped fails an assert in persist_handles.rs and takes the process down. The safety argument seems to be: validate the target against the current catalog on the coordinator loop and send with no await in between, so channel FIFO keeps the append ahead of any Forget. Part 6's handle_attempt_write does exactly that, but a sentence here would keep a future submitter honest.

appends
.entry(target.item_id)
.or_default()
.extend(writes.into_values().flatten());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, optional: this relies on the single-key invariant that only part 6's handle_attempt_write enforces, and a violation would silently append rows to the wrong table. A one-line assert that every key of writes equals target.item_id would make this self-defending. Fine to fold into part 6 instead.

/// Attempts an OCC write exactly once at its requested timestamp.
///
/// Unlike blind writes, an `InvalidUppers` conflict must return to the
/// subscribe loop. Retrying the same diffs at a fresh timestamp would apply

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this doc reaches into its caller's internals ("the subscribe loop", "the OCC semaphore on the frontend"), coupling the committer's doc to one caller's implementation. Could it stay at the level of this function's contract? E.g. "a conflict is reported as TimestampPassed and is the caller's to resolve with a new snapshot", and the throttle/permit bullets as preconditions on the caller: the requested timestamp must not run the write timeline ahead of the clock, and the caller bounds how many requests are in flight. (Still fine to mention what callers do as an example of how to use this function, the ask is just that the contract not be stated in terms of it.)

writes,
write_locks: None,
responder: UserWriteResponder::Session(pending_txn),
responder,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, optional: with the old header comment gone (rightly, "otherwise defer the write" no longer holds for the internal path), a corrected one-liner would help skimming this now ~115-line arm. E.g.: "Without handed-off locks, acquire just-in-time. On a miss, session writes defer, internal writes re-queue."

ControlFlow::Continue(())
}

/// Writes at a fresh oracle timestamp, retrying an upper conflict at a new

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, optional: the old doc also said "and applies a successful write to the oracle". That still holds (via attempt_write_to_txns, whose Applied implies the oracle apply), and commit relies on it, so consider keeping the clause here so the contract stays complete at this boundary too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants