adapter: make statement logging single-owner for frontend sequencing - #37920
Conversation
ed3e203 to
723f456
Compare
4d880b4 to
ec70668
Compare
ec70668 to
0e70c18
Compare
0e70c18 to
70eb002
Compare
70eb002 to
74cb16e
Compare
74cb16e to
834d7e2
Compare
|
hey aj, you're saying in the PR description that logging is wrong today, but we don't add any tests that show what's wrong, or anything that is fixed by our changes? |
834d7e2 to
d5b9c0e
Compare
|
Fair hit, and checking it properly turned up that two of the four claims were wrong.
Two claims were wrong and I have rewritten them. "A statement that failed before the frontend took it over produced no row" does "End events were sent with Two claims are real but not reachable from SQL today, so they get no test |
d5b9c0e to
cd3d6d1
Compare
cd3d6d1 to
4f12ec4
Compare
|
@ggevay we once again change statement logging, but this one actually looks quite decent 😅 |
ggevay
left a comment
There was a problem hiding this comment.
LGTM, nice cleanup of the logging-obligation handling.
| { | ||
| session | ||
| .metrics() | ||
| .subscribe_outputs(&[session_type, metrics::subscribe_output_label_value(output)]) |
There was a problem hiding this comment.
FYI, this closes a real gap in passing: before this commit, frontend-sequenced SUBSCRIBEs never bumped mz_subscribe_outputs. The only bump site in the tree was in handle_execute, which a taken-over statement never reaches, and SessionMetrics had no accessor for it. Worth a line in the PR description, since it is a small behavior change beyond restructuring.
| } | ||
| return Err(err); | ||
| } | ||
| // Extract things from the portal. A failed verification is not logged, |
There was a problem hiding this comment.
nit: "is not logged" threw me briefly. The statement's own entry is never begun, but an adopted outer entry still gets its Errored end from the retirement site in SessionClient::execute. Maybe "does not begin an entry, mirroring the coordinator"?
| #[must_use = "StatementLoggingGuard must be explicitly retired or handed off; \ | ||
| otherwise `Drop` will log the statement as Aborted"] | ||
| pub(crate) struct StatementLoggingGuard { | ||
| struct StatementLoggingGuard { |
There was a problem hiding this comment.
Optional, and fine as a follow-up after the whole stack has landed: ExecuteContextGuard and StatementLoggingGuard are the same concept in two flavors, the armed end-of-execution obligation with a coordinator channel vs a session-task channel, but the names do not show the symmetry. Something like CoordStatementLoggingGuard vs FrontendStatementLoggingGuard would make it visible at a glance. We filed SQL-600 for a broader consolidation of these types (deferred until the stack lands), which would subsume this rename.
4f12ec4 to
7eca208
Compare
|
(This is aj, Aljoscha's agent, acting on his behalf. He's away, so replies here Both taken.
Comment reworded. Your reading is right and mine was sloppy, since "is not On the guard naming, agreed on the substance: |
A statement sequenced from the session task has its end-of-execution logging obligation handed between several places, and each of them can be the last one to hold it. Whether a log row appears, and what reason it carries, depends on which path the statement takes out of the session task. That is survivable while the only such statement is a peek. It stops being survivable with the next part of this stack. Read-then-write is a long-running operation, so it is bounded by cancellation and `statement_timeout` in a `tokio::select!` that drops the operation's future when either fires. The obligation must not live inside that future. If it does, dropping the future runs the guard's `Drop`, which reports `aborted` with no message, and the user who just saw "canceling statement due to statement timeout" finds a log row that records neither the error nor its text. Hoisting the obligation into the frame beside the `select!` is what makes that drop safe, and it is why this lands before the path that needs it rather than with it. `ExecutionLogging` is a slot that owns the obligation for one `SessionClient::execute` call, with a single retirement site that maps the call's outcome to an end reason. Handing the statement back to the coordinator passes the obligation as data in `Command::Execute`, and `handle_execute` arms it again on receipt, which is safe because the command travels a channel only the coordinator loop drains. Three defects follow from the structure and go away with it. An `EXECUTE` that unrolled to its inner statement got a guard whose retire channel was already dropped, so if that guard ever ran its `Drop` the end event went nowhere and the entry stayed open forever. A statement whose future is dropped mid-flight was recorded as `aborted` rather than carrying its error. And a plan that passes the AST allowlist but then fails the plan-kind check bailed out after logging had begun, closing the frontend's entry as errored while the coordinator started its own from scratch, so one statement produced two log rows and counted `query_total` twice. None of the three is reachable from SQL on the peek path. The first needs a drop in the few microseconds between installing the guard and the takeover, or a shutdown. The second needs an inner future for something to drop, and the peek path has none. The third sits behind `soft_panic_or_log`. They are latent, which is the point: the caller that reaches the second one arrives in part 6. Frontend-sequenced subscribes also start counting towards `mz_subscribe_outputs`. The only site bumping it was `handle_execute`, which a statement the frontend took over never reaches, and `SessionMetrics` had no accessor for it, so those subscribes went uncounted. End events also move from `Client::send`, which panics when the coordinator is gone, to a `try_send` that drops them. A guard can outlive the coordinator during shutdown, and losing one end event is cheaper than panicking in `Drop` and taking the connection with it. Every other logging event keeps the asserting send. `end_statement_execution` takes the end timestamp from its caller. The frontend already recorded when execution finished and the handler discarded it, so `finished_at` and the `ExecutionFinished` lifecycle event charged the statement for however long its message waited in the coordinator's queue. The client never waited for that queue, because the response leaves the session task and the end event is a fire-and-forget send on an unbounded channel. `began_at` already came from the frontend's clock, so the two ends of a statement's duration were being read from different clocks.
`server.rs` had grown to 7585 lines, and the statement logging tests were a contiguous 994-line block of it with their own harness. Moving them to `statement_logging.rs` leaves both files at a size a reader can hold, and it keeps later additions to either group from churning the other's diff. This is a pure move. Every moved item is byte-identical to its previous form, and the only edits are the imports each file now needs.
The end timestamp of a statement the session task retires itself is taken in that task, so a coordinator busy with other work cannot inflate it. Stalling the coordinator inside `Catalog::transact` and then measuring a constant `SELECT` from a warmed session makes the difference observable: the logged `finished_at` has to fall before the client saw the statement return.
7eca208 to
1d91d76
Compare
NOTE: This looks big, but a large chunk is moving tests around, in a separate commit.
Motivation
Part 3 of 7 in a stack that moves
DELETE,UPDATEandINSERT ... SELECToff the coordinator onto the session task, using optimistic concurrency
control.
This part is a restructuring, not a bugfix. A statement sequenced from the
session task has its end-of-execution logging obligation handed between
several places, and each of them can be the last one to hold it. Whether a log
row appears, and what reason it carries, depends on which path the statement
takes out of the session task. That is survivable while the only such
statement is a peek. It stops being survivable in part 6.
Read-then-write is a long-running operation, so part 6 bounds it with
cancellation and
statement_timeoutin atokio::select!that drops theoperation's future when either fires. The obligation must not live inside that
future. If it does, dropping the future runs the guard's
Drop, which reportsabortedwith no message, and a user who just saw "canceling statement due tostatement timeout" finds a log row recording neither the error nor its text.
Hoisting the obligation into the frame beside the
select!is what makes thatdrop safe, which is why this lands before the path that needs it rather than
with it.
Closes SQL-589
Description
ExecutionLoggingis a slot that owns the obligation for oneSessionClient::executecall, with a single retirement site that maps thecall's outcome to an end reason. Handing the statement back to the coordinator
passes the obligation as data in
Command::Execute, andhandle_executearmsit again on receipt, which is safe because the command travels a channel only
the coordinator loop drains.
Three defects follow from the old structure and go away with it:
EXECUTEthat unrolled to its inner statement got a guard whose retirechannel was already dropped, so if that guard ever ran its
Dropthe endevent went nowhere and the entry stayed open forever
abortedrather than carrying its error
bailed out after logging had begun, closing the frontend's entry as errored
while the coordinator started its own from scratch, so one statement produced
two log rows and counted
query_totaltwiceNone of the three is reachable from SQL on the peek path, which is why none of
them gets a test here. The first needs a drop in the few microseconds between
installing the guard and the takeover, or a shutdown. The second needs an inner
future for something to drop, and the peek path has none. The third sits behind
soft_panic_or_log. They are latent, which is the point: the caller thatreaches the second one arrives in part 6, which carries its tests.
Frontend-sequenced subscribes also start counting towards
mz_subscribe_outputs. The only site bumping it washandle_execute, which astatement the frontend took over never reaches, and
SessionMetricshad noaccessor for it, so those subscribes went uncounted. That is a behavior change
beyond the restructuring.
End events also move from
Client::send, which panics when the coordinator isgone, to a
try_sendthat drops them. A guard can outlive the coordinatorduring shutdown, and losing one end event is cheaper than panicking in
Dropand taking the connection with it. Every other logging event keeps the
asserting send.
end_statement_executiontakes the end timestamp from its caller. The frontendalready recorded when execution finished and the handler discarded it, so
finished_atand theExecutionFinishedlifecycle event charged the statementfor however long its message waited in the coordinator's queue. The client
never waited for that queue, because the response leaves the session task while
the end event is a fire-and-forget send on an unbounded channel.
began_atalready came from the frontend's clock, so the two ends of a statement's
duration were being read from different clocks.
Verification
test_statement_logging_finished_at_excludes_coordinator_queuecovers thefinished_atchange, which is the one thing here observable from SQL today. Itstalls the coordinator inside
Catalog::transactand then runs a constantSELECTfrom a session whose catalog snapshot is already warm, so thestatement needs nothing from the coordinator. The logged
finished_athas tofall before the client saw the statement return. On the old code it lands about
three seconds later.
All existing
test_statement_logging_*tests are unchanged and pass. Theremaining tests for these paths are DML-specific and arrive with part 6, which
adds a harness asserting the frontend and coordinator paths produce the same
mz_statement_execution_historyrows for the same statement, plus thecancellation and statement-timeout tests that reach the latent defects above.
The second commit moves the existing tests out of
server.rsintostatement_logging.rs. It is a pure move: every moved item is byte-identicalto its previous form, and the only other edits are the imports each file now
needs.
server.rswas 7585 lines, of which the statement logging tests andtheir harness were a contiguous 994-line block.