Repository navigation
Description
An InterruptException that lands while a task is blocked reading a response arrives bare on a plain-TCP connection. On a TLS connection it surfaces as ProtocolError("TLS transport failure (read): unexpected TLS failure"), and the interrupt is lost. ssl_mode=:preferred is the default, and MySQL 8 offers TLS out of the box, so this is the common case. A caller can't tell a Ctrl-C from a broken TLS record.
Repro
MySQL.jl 2.0.0, Julia 1.12, MySQL 8.4.11:
repro_tls_interrupt.jl
# docker run -d --rm --name mysql84 -p 127.0.0.1:3307:3306 -e MYSQL_ROOT_PASSWORD=pw -e MYSQL_DATABASE=f1 mysql:8.4
# julia -t 2 repro_tls_interrupt.jl
using MySQL, DBInterface
conn(; kw...) = DBInterface.connect(MySQL.Connection, "127.0.0.1", "root", "pw"; db = "f1", port = 3307, kw...)
holder = conn()
DBInterface.execute(holder, "DROP TABLE IF EXISTS driver")
DBInterface.execute(holder, "CREATE TABLE driver (driverid INT PRIMARY KEY, surname VARCHAR(50))")
DBInterface.execute(holder, "INSERT INTO driver VALUES (1, 'Senna')")
for ssl_mode in (:disabled, :preferred)
victim = conn(; ssl_mode)
# block `victim` in a read: it waits on a row lock held by `holder`
DBInterface.execute(holder, "START TRANSACTION")
DBInterface.execute(holder, "SELECT * FROM driver WHERE driverid = 1 FOR UPDATE")
t = Threads.@spawn try
DBInterface.execute(victim, "SELECT * FROM driver WHERE driverid = 1 FOR UPDATE")
nothing
catch e
e
end
sleep(0.5)
schedule(t, InterruptException(); error = true)
e = fetch(t)
println("ssl_mode=$ssl_mode: ", typeof(e), " => ", sprint(showerror, e))
DBInterface.execute(holder, "ROLLBACK")
endssl_mode=disabled: InterruptException => InterruptException:
ssl_mode=preferred: MySQL.Protocol.ProtocolError => ProtocolError: TLS transport failure (read): unexpected TLS failure
Cause
fault! receives a Reseau.TLS.TLSError whose cause is the InterruptException. I confirmed this by instrumenting fault! in the repro above. The authenticated-TLS branch then turns it into a ProtocolError, which keeps only err.op and err.message:
err isa Reseau.TLS.TLSError && return ProtocolError("TLS transport failure ($(err.op)): $(err.message)")The same function already looks through a TLSError for an EOFError cause a few lines up, so the plain-TCP and TLS paths agree on EOF but not on interrupts.
Suggested fix
Unwrap the interrupt the same way as the EOF, before the TLSError → ProtocolError mapping:
err isa Reseau.TLS.TLSError && err.cause isa InterruptException && return err.causeThe session is still marked BROKEN and the transport closed, exactly as on the plain-TCP path. Only the exception the caller sees changes.
Two alternatives, if returning the interrupt is not wanted:
- keep the
TLSError(or itscause) as acausefield onProtocolError; - unwrap any
causegenerically.
Context
This came out of evaluating MySQL.jl 2.0 as the MySQL backend for PormG, an async-first Julia ORM that already runs on Postgres.jl and SQLite.jl. Its pool treats a bare InterruptException as an abandoned await: it cancels the server-side statement and then recovers or discards the slot. Under TLS that path is never taken. If a PR along these lines would be welcome, I'm glad to write it.
Activity
quinnj commentedon Oct 27, 2026
[comment generated via AI]
Fixed upstream in Reseau.jl 1.6.2 (JuliaServices/Reseau.jl#207): Reseau's TLS read, write, and handshake paths now rethrow an
InterruptExceptioninstead of wrapping it in aTLSError, so it reaches MySQL.jl unchanged. Your repro against MySQL 8.4 with MySQL.jl 2.0.0 and Reseau 1.6.3 now printsInterruptExceptionfor bothssl_mode=:disabledandssl_mode=:preferred. MySQL.jl's compat already allows these Reseau versions, soPkg.update()picks it up. Closing; please reopen if you still see it.