Repository navigation
Distinguish sent from unsent commands on connection loss (2013 vs 2006) - #251
Conversation
A peer EOF after a command was written was reported as 2006 "MySQL server has gone away" whenever no answer byte had arrived, the same error the driver raises when it refuses a command before sending anything. The server may have executed and committed such a command. Classify the loss by whether the command was written instead: 2006 when writing it failed (phase READY), 2013 once it was written, as MySQL.jl 1.x (MariaDB Connector/C) did. Remove the received-bytes counter that only fed the old rule. Refs #248 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connection reset (ECONNRESET/EPIPE, or a TLSError caused by one) escaped as a raw SystemError, or as a ProtocolError over TLS, whether or not the command had been written. Report it as EOF from transport_read_some! and transport_write so fault! applies the same rule: 2006 if writing the command failed, 2013 once it was written. The socket error stays attached as the exception's cause. Refs #248 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The peer could update both rows and commit before the main transaction acquired its first row, so the test sometimes ran without a deadlock. Wait for both first-row locks before either transaction requests the other row. Ten transaction workflow runs on each of MySQL 8.4 and MariaDB 11.4 passed all 640 checks.
Normalizing socket errors to EOF discarded the TLS exception type before authentication. A TLS certificate rejection could therefore report ProtocolError, depending on whether its alert or socket reset arrived first. Use the session transport to classify a pre-authentication TLS close as TLSNegotiationError. Authenticated connection-loss codes are unchanged. Add deterministic TLS 1.2 and 1.3 reset tests that fail before this fix and pass after it.
There was a problem hiding this comment.
No remaining correctness findings after reviewing f939830...e2126bc (merge base f939830). All review changes were made in an isolated checkout and pushed to this PR.
Fixed during review:
- Socket-reset normalization erased the TLS error type before authentication. A client-certificate rejection could then report
ProtocolError, depending on whether its alert or reset arrived first. The session now preservesTLSNegotiationErrorfor a TLS close before authentication completes. Deterministic TLS 1.2/1.3 tests fail before this fix and pass after it. - The existing live deadlock test could let the peer commit before the other transaction held its first row. Both first-row locks are now coordinated before either transaction requests the other row. Ten transaction workflow runs on each real server passed all 640 checks.
Added authenticated TLS reset coverage (2013 for the written command, then 2006 for the refused next command) and strengthened LOCAL INFILE coverage so local file SystemErrors cannot be mistaken for socket failures.
Validation: the final Julia 1.13.3/macOS suite passed 1,936 checks, including the --trim=safe executable. Julia 1.10.12 with minimum Reseau 1.4.1 passed 1,787 checks. Full MySQL/MariaDB integration and Docker-backed performance/allocation gates passed during review. All 15 active checks on final commit e2126bc are green: Julia 1.10/latest/nightly on Linux, macOS, and Windows; Docker integration; coverage; and documentation. The two schedule/manual-only jobs are correctly skipped. CI run.
Version 2.2.0 is prepared because callers that match only error 2006 need to account for 2013 once a command was written. The execution outcome after 2013 or a response timeout remains unknown; retrying can duplicate a write.
I recommend merging and releasing 2.2.0 after the required independent approval. The PR author is this app, so this review is a comment rather than the separate approval GitHub requires.
Inference does not narrow `err` through the `||` in `fault!`'s EOF branch, so on Julia 1.12 `TLSNegotiationError(msg, err)` compiled to a dynamic call and the `--trim=safe` verifier rejected the trim workload. Julia 1.13 union-splits the constructor's `convert`, so CI, which runs the trim build only on the latest release, passed. `err::Exception` makes the call static on both. Also describe `TLSNegotiationError` as covering a TLS failure or close before authentication completes, not only a failed handshake. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the follow-up through 3095913. The err::Exception assertion is valid on both branches of the EOF/TLSError condition and keeps the TLS setup error constructor static on Julia 1.12. The updated error summary matches the implemented behavior. A fresh Julia 1.12.6/macOS suite passed all 1,794 checks, including the trim executable; all 15 active CI checks for this exact commit are green. No additional findings.
Fixes #248.
What changes for users
When a connection drops, the error now says whether the command reached the server:
was already closed or broken (and
reconnectcould not be used), or writing the commandfailed.
server may have executed and committed it, even if no answer arrived.
Until now, a command that was written but got no answer byte back also raised 2006, the same
error as a command the driver refused without sending. A caller could not tell "never ran"
from "may have committed". This restores what the two codes meant in MySQL.jl 1.x (MariaDB
Connector/C). The 2.x migration guide already says the codes are unchanged.
Connection resets now follow the same rule. Until now they surfaced as a raw
SystemError(plaintext) or as
ProtocolError("TLS transport failure …")(TLS), whether or not thecommand had been sent.
No new types, fields, or exports.
What 2006 does not promise: it covers only the command that raised it. A transaction
open on the lost connection is gone, and an earlier command that failed with 2013 or a
timeout may have committed. The Errors docs now say so.
Evidence
Setup: MySQL 8.4.13 behind a proxy that drops the server's reply. Row counts were read back
over a separate connection.
execute(conn, sql, params), EXECUTE reply droppedDBInterface.transaction, COMMIT reply droppedCompositeException[2006, 2006]CompositeException[2013, 2006]SystemError(read)SystemError(write)Against the same faults, MySQL.jl 1.6.0 reported 2013 for every loss after a successful write.
It reported 2006 for the refusal and for a failed write.
Compatibility
written. A
MySQL.ping-based pool check is one example. Such code should match 2006, 2013,4031 and
TimeoutError.reconnects with
reconnect=true.ProtocolError("connection closed by the server …")instead of a rawSystemError. Over TLS, a reset or close beforeauthentication completes is
TLSNegotiationError, the same error as a TLS alert at thatpoint. A TLS reset there already gave
TLSNegotiationError; a clean TLS close gaveProtocolError.Changes
fault!chooses 2006 vs 2013 by phase: inREADYthe write itself failed, otherwise thecommand was written. It no longer uses "no answer bytes". The
received_bytescounter,which only fed the old rule, is deleted.
transport_read_some!andtransport_writereport a socket error, or aTLSErrorcausedby one, as EOF and keep the original as the cause. This is done there rather than in
fault!, becausefault!also receives errors from reading a LOCAL INFILE source file.fault!reports a TLS close or reset asTLSNegotiationError. ATLS 1.3 server that rejects the client certificate may reset the socket before its alert
arrives, so the error no longer depends on which one the client sees first.
and the
TLSNegotiationErrorsummary.and COMMIT inside
DBInterface.transaction): 2013, then 2006 for the refused command.EPIPE: 2006.TLSNegotiationError.SystemErrorkeeps that error; it is not mistaken fora lost connection.
peer could take both rows and commit, so the test could see no deadlock.
Verification
--trim=safeexecutable. Theerr::Exceptionassertion keeps that build static on Julia 1.12.Release
Published as MySQL v2.2.0. General registration #173773 is merged. The registered source tree and annotated tag both match merge commit
ddc8df6.Co-authored by Codex
Original implementation generated with Claude Code.