fix(plugin-mysql): stop blocking the main thread on Stop, and never replay a read with side effects - #2448
Merged
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…eplay a read with side effects Claude-Session: https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ
datlechin
force-pushed
the
fix/mysql-cancel-and-replay
branch
from
August 26, 2026 09:02
75d38ac to
dd00e41
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two MySQL driver defects found while investigating #2427. Neither is caused by that work and neither depends on it, so they are here rather than in #2447.
Stop blocks the main thread for up to five seconds
DatabaseManageris@MainActor, andcancelRunningQuery(reach: .userStop)callsdriver.cancelQuery()synchronously on purpose. Its own comment says why: "Stop stays synchronous because the user is waiting on it", and it contrasts that against a navigation supersede, which is dispatched off the main thread precisely because "a PostgreSQL cancel opens a second connection to deliver the request, which through an SSH tunnel costs 70-160ms".MySQL's cancel is not a
PQcancel.MariaDBPluginConnection.cancelCurrentQueryflips the cancellation gate, which is the fast part, and then callskillQueryOnServer, which opens a whole new connection: TCP, TLS, auth. Measured with the shippedLibs/libmariadb_arm64.aagainst a host that drops packets, which is exactly the situation that makes somebody press Stop:That 5,001 ms is the
MYSQL_OPT_CONNECT_TIMEOUTthe function sets, and it is spent on the main thread with the UI frozen.The half the caller needs is already synchronous. The gate is what makes the in-flight read give up, and it is a lock and a flag. The kill is server-side cleanup, and it carries only a thread id, no handle, so it can finish on its own queue. It moves to one: serial, so a user pressing Stop repeatedly opens one connection at a time rather than one per press.
A read with side effects is re-run after a dropped connection
executeWithReconnectretries a statement when the connection was lost mid-flight, gated onmysqlStatementIsReadOnly, which looks at the leading keyword alone. EverySELECTpasses.A
SELECTis not automatically repeatable. On MariaDB,SELECT NEXTVAL(order_seq)advances the sequence; the connection drops before the row arrives, the driver reconnects and runs it again, and the second value is the one the user sees while the first is gone. Nothing reports it: the retry looks like a connection that healed itself.SELECT GET_LOCK(...)re-acquires,SELECT ... FOR UPDATEre-locks,SELECT ... INTO OUTFILEre-writes the file.The retry now goes through
mysqlStatementIsSafeToReplay, which keeps the leading-keyword test and adds a scan for the markers that make a read non-repeatable: sequence functions, the lock functions,INTO OUTFILE/INTO DUMPFILE/INTO @, row-locking clauses, user-variable assignment, and the functions whose value moves on its own.It is deliberately crude, and it only ever errs toward "do not replay". A false positive costs the user a connection error where they would have got a transparent retry, which is the safe direction. It cannot see inside a stored function, so
SELECT my_function()that writes is still replayable; that would need the server's own read-only tracking.Verification
verify.sh buildverify.sh test MySQLStatementClassificationTests MySQLReplaySafetyTestsverify.sh lint Plugins/MySQLDriverPlugin TableProTests/PluginsMySQLReplaySafetyTestscovers each marker class plus the whitespace forms, sinceNEXT VALUE FORandFOR UPDATEare multi-word and a line break between them must not hide them.No UI automation for the Stop path: reproducing it needs a server that accepts a connection and then stops answering, which no deterministic fixture in the repo provides. The 5,001 ms figure above is from a compiled probe against the real library rather than through the app.
https://claude.ai/code/session_01KsqHrFwJxUW6eWozYjT8JZ