feat(control): native SQL in-txn read-your-own-writes (U3) + cross-cluster txn_id groundwork#176
feat(control): native SQL in-txn read-your-own-writes (U3) + cross-cluster txn_id groundwork#176pizofreude wants to merge 3 commits into
Conversation
A native (MessagePack) client that sends BEGIN as its first post-auth frame previously no-oped the transaction: handle_begin -> run_begin -> SessionStore::begin silently returned Ok without setting InBlock when no session yet existed for the peer address (ensure_session was only called lazily from the SQL statement path). The first in-block write then autocommitted instead of buffering into the staging overlay. Call sessions.ensure_session(peer_addr) at the top of handle_begin so BEGIN transitions the session to InBlock regardless of frame ordering, matching pgwire which creates the session at connection startup. Guarded by native_first_frame_begin_does_not_buffer_probe.
…RYOW Native SQL reads inside an explicit transaction route through sql_gateway.rs::dispatch_task_via_gateway, which built a GatewayQueryContext without the connection's txn_id before calling gateway.execute(). That dropped txn_id ahead of the Data Plane, so the per-transaction staging overlay was never resolved and a transaction could not read its own writes. The pgwire path was unaffected because its local in-block dispatch stamps task.txn_id straight onto the Request envelope. Add QueryContext.txn_id and thread it end to end: gateway::execute -> dispatch_route -> dispatch_local -> dispatch_to_data_plane_with_txn. The dispatch_route arguments are bundled into a new DispatchRouteParams struct (mirroring DispatchRouteStreamParams) to stay under the too_many_arguments threshold. The real txn_id is carried at the in-txn read seams (sql_gateway, direct_ops, gather, native streaming); None is used at non-interactive entry points (HTTP/RESP/ILP/CDC/topic/ws_rpc/promql/calvin), the four remote graph-dispatch callers, and the two pgwire gateway sites (remote-forward is cross-node, streaming is autocommit-only). Cross-node in-transaction RYOW remains a tracked gap: the RPC envelope does not yet carry txn_id. Guarded by native_in_txn_sql_select_reads_own_write_probe.
…OW to U4/U5 Add ExecuteRequest.txn_id (WIRE_VERSION 2->3, coordinated upgrade) and thread the session txn id through dispatcher/stream/receiver/all-cores. Mint globally-unique ids via TxnId::from_origin(node_id, seq). Add cross_node_ryow_probe.rs, #[ignore]d: cross-node in-txn RYOW is confirmed to require the cross-shard-transaction machinery (U4/U5); U3 is single-node only per LOCKED design section 8. Root cause: the pgwire remote-leader forward path returns before the staging gate and hardcodes QueryContext.txn_id = None, so a non-owner's in-txn write ships as a plain committed write and its read carries no txn id. Remove RYOW debug instrumentation.
|
Single-node native RYOW looks right. Two asks before merge:
Commits 1–2 are fine to land independently if you want to split the wire groundwork out. |
What & why
Verifies and closes design unit U3 (in-transaction read-your-own-writes) on the SQL paths, and lands the cluster-wire groundwork needed for the cross-node continuation (U4/U5).
Three focused commits:
fix(control): ensure native session on first-frame BEGIN— aBEGINarriving on the very first native frame no longer races session creation, so the transaction has a session to attach to.fix(control): propagate txn_id through the gateway for native in-txn RYOW— threads the sessiontxn_idthrough the native gateway dispatch so an in-block read resolves this transaction's staging overlay. Single-node native in-txn RYOW now works.feat(control): thread txn_id across cluster wire; defer cross-node RYOW to U4/U5— addsExecuteRequest.txn_id(WIRE_VERSION2→3, coordinated upgrade), threads the id through dispatcher/stream/receiver/all-cores, and mints globally-unique ids viaTxnId::from_origin(node_id, seq).Scope decision: U3 closed as single-node; cross-node deferred to U4/U5
Per the LOCKED design §8, U3's validation surface is single-node only ("single-node-verifiable: U1, U2, U3 (RYOW), U7"; cross-node is "cluster-bound (stage-2): U4, U5, U6, U8"). A 3-node probe (
cross_node_ryow_probe.rs) is included but#[ignore]d as the executable spec for U4/U5.Confirmed root cause of the cross-node gap (live trace): the per-transaction machinery — staging gate, COMMIT buffer, COMMIT/ROLLBACK overlay flush — is local-only. On a non-owner node the pgwire dispatcher takes the remote-leader forward short-circuit (
routing/execute.rsshould_forward_via_gateway→dispatch_tasks_via_gateway) and returns beforedispatch_task_loop, soroute_in_tx_writenever runs. The in-txn write ships to the owner as a plain replicable write (Raft-proposed = committed at statement time, never staged), andgateway_dispatch.rshardcodesQueryContext.txn_id = None, so the read arrives with no txn id. The wiretxn_idfield here is a necessary-but-not-sufficient prerequisite for the U4/U5 fix.How to test
Also verified locally:
cargo fmt --allclean;cargo check -p nodedbclean (zero warnings);cargo clippyintroduces no new warnings in any file this branch touches.Tradeoffs / alternatives considered
WIRE_VERSION2→3 — the field is additive but mixed-version clusters are out of scope for this branch.TxnId::from_origin(node_id, seq)kept as groundwork so ids are globally unique before cross-node txn coordination exists (all non-txn call sites passNone/identity).Notes for review
WIRE_VERSION— flagging per the "wire protocol changes" note inCONTRIBUTING.md.run-cilabel. Heads-up: the workspace currently has pre-existing clippy lints under current stable (iter_kv_map,question_mark,unneeded_wildcard_pattern) in files unrelated to this branch; with-D warningsthey would trip the lint job independently of this change.