Repository navigation
Rust #325: send MySQL statement closes with the next command (#714) - #717
Conversation
COM_STMT_CLOSE has no reply. The published sqlx-mysql flushed it on its own write after a cache eviction or an uncached execute, and flushed it again before draining results. The next command then waited behind Nagle's algorithm for the server's delayed acknowledgement, about 40 ms. Across the 30-48 statements of a transition that held the global lock for 0.3-1 s under load, and a due timer behind it fired more than the 1 s bound late (#714). The vendored driver now queues each close with sequence 0 and leaves a buffer of replyless closes unflushed until the next command, so both go out in one write. A proxy-recorded qualification test fails with the published driver and passes with the change on MySQL 8.0 and MariaDB.
|
Clean release A/B, all five families. Disposable VM
Neither build carries instrumentation. The native tree is unchanged from
On main, the six MySQL-family failures took three forms:
All three are gone with the change. PostgreSQL and SQLite are unchanged. The 1.000 s bound, spec, fixtures and frozen actors are untouched. Local qualification on this head (
The VM was deleted and verified absent, at about $0.10. |
|
Security source review at exact f31c4c3: the causal batching is well supported, the 1.000 s bound is preserved, and the five-family A/B is relevant. One connection-lifecycle regression still blocks acceptance. Connection::flush still delegates to MySqlStream::wait_until_ready. With this patch, when the buffer contains only queued COM_STMT_CLOSE packets, wait_until_ready returns success without flushing them. should_flush simultaneously remains true because the buffer is nonempty. clear_cached_statements also queues every close and returns success without another command. That no longer satisfies the SQLx Connection contract that clear_cached_statements removes cached statements and closes them on the server, and an explicit flush can report success while leaving all bytes buffered. An idle pooled connection can therefore retain the complete server-side statement cache until later reuse or close; the current close-packet regression does not exercise either public lifecycle. Please separate the internal next-command readiness path from the actual Connection flush contract. Preserve batching for ordinary eviction/uncached execution, but make an explicit Connection::flush force all buffered bytes and leave should_flush false, and make clear_cached_statements complete its documented server-side closes before returning (or force the same real flush). Add bounded MySQL and MariaDB controls proving both outcomes without a subsequent business command. Preserve the existing close/next-command counterfactual and timer A/B. One evidence wording correction is also warranted: record_client_writes observes TCP read chunks, not caller write syscall boundaries; TCP is a byte stream. The published-driver counterfactual is useful, but describe/assert the network-visible chunk behavior accurately or instrument the socket write boundary directly. #717 remains behind main, so the corrected head will also need current-main integration and fresh normal qualification. |
Security's review of f31c4c3: Connection::flush delegated to wait_until_ready, which now leaves a buffer of queued closes unsent, and clear_cached_statements only queued its closes. Both report success with the bytes still buffered. Explicit flushes and cache clears now send every buffered byte before returning (MySqlStream::flush_queued); only the path before the next command keeps closes to share that command's write. New MySQL and MariaDB controls prove both calls without a later command; they fail on f31c4c3. The close test now describes what it observes: TCP read chunks at a proxy, not write calls.
|
Rust Server (Claude): the lifecycle regression found at
The timer A/B is unaffected, because this change touches only the explicit lifecycle calls. Fresh normal qualification is running on this head. |
|
|
Security re-review at exact
The two new no-later-command controls cover explicit flush and cache clear on both MySQL and MariaDB, with the MySQL performance-schema observation verifying server-side statement removal. Both fail against the first revision. The recorder and evidence now accurately claim TCP read chunks rather than client write syscalls. No Security source blocker remains on this exact head. Fresh normal qualification is still running, so this is source acceptance, not a terminal merge or release qualification. Preserve any failed cells and current-main identity through the final disposition. |
|
Hosted evidence at |



Fixes #714 (#325). The native timer path was running more than 1 s late on MariaDB/MySQL under the CPU qualification load.
Cause
COM_STMT_CLOSEhas no server reply. The publishedsqlx-mysql0.9.0 flushes it in its own write in two cases:Before the next command, it also flushes anything still buffered. That next command is therefore a second small write while the close is unacknowledged, so Nagle's algorithm holds it until the server's delayed ACK, about 40 ms. SQLx never sets
TCP_NODELAY; PHP's mysqlnd does. PostgreSQL's driver already batches its closes with the next request.Inside the global transition lock, a transaction issues 30 to 48 statements. These stalls stretched lock holds to 0.3 to 1 s. A timer that came due behind a sweep waiting on such a hold fired 1.1 to 1.2 s late.
How it was attributed (disposable VM, local-only instrumentation)
Change
All of it is in the vendored
sqlx-mysql(patch record:rust/vendor/sqlx-mysql/DURABLE-PATCH.md):MySqlStream::queue_packetbuffers each statement close with sequence number 0.wait_until_readydoes not flush a buffer that holds only replyless closes, so they leave in the same write as that command.f31c4c361.Connection::flushsends every buffered byte, queued closes included, and leavesshould_flushfalse.clear_cached_statementssends its closes before returning.The server receives the same packets in the same order. The 1.000 s lateness bound, the spec and all fixtures are unchanged.
Verification
runtime::mysql:: --ignoredstep.qualification_statement_close_leaves_with_the_next_commandcovers both close paths. It passes with this change on MySQL 8.0 and MariaDB 10.11. It fails with the published driver on both: every close arrives as a chunk of its own, and the same 14 statements take 0.60 s against 0.01 s with the fix.qualification_explicit_flush_sends_queued_statement_closesandqualification_clearing_the_statement_cache_closes_them_nowprove both lifecycle calls without a later command. On MySQL 8.0 with the performance schema on, the cache clear also drops the server's statements for the connection from three to none. Both fail withf31c4c361and pass now.wait_until_ready:Checkpoint:
docs/rust-server-port-checkpoints/2026-10-23-mysql-statement-close.md.