Repository navigation
fix: MySQL QUERY_TIMEOUT kill + parseable NDJSON (#15) - #17
Conversation
MySQL MAX_EXECUTION_TIME does not interrupt SLEEP(); add a runner wall-clock watchdog that destroys the client socket at timeoutMs. Demux stdout/stderr as complete lines so kill-path stderr cannot splice into NDJSON. Return truncated results without waiting on container.wait() (#16). Map runner timeout errors to code TIMEOUT. Co-authored-by: zhangjing <zhangjing798@qq.com>
ChuTingzj
left a comment
There was a problem hiding this comment.
Verdict
Fixes #15 correctly: MySQL wall-clock watchdog (SLEEP), per-stream line demux (JSON splice), TIMEOUT mapping. #16 abort-hang subset fixed (return truncated without waiting on wait()); do not close #16 for UNION/CTE / live e2e follow-ups.
Findings
- Minor — truncate path:
destroy()beforefinish()could theoretically reject if error is sync; prefer finish-first or ignore post-truncate errors. - Ops — runner image / digest bump still required;
@0.1.4consumers will not get the watchdog until republish.
No must-fix. Do not merge from this bot.
| }); | ||
| try { | ||
| const query = connection.query({ sql, rowsAsArray: true }); | ||
| await streamMysqlQuery(query, connection, limiter, writeEvent); |
There was a problem hiding this comment.
#15 root cause match. MAX_EXECUTION_TIME does not abort SLEEP(); wall-clock destroy() is the right MySQL lever. Tests lock destroy-within-timeoutMs and hung end() → destroy.
Residual: if connection.destroy() sync-emits error before finish(), the Promise can reject after a truncated end was already written. Prefer finish() before destroy(), or ignore errors once truncate is intended. Low likelihood with mysql2.
| } | ||
| if (data.truncated || killedForLimit) { | ||
| return successResult(data); | ||
| } |
There was a problem hiding this comment.
#15 parse fix looks right — complete lines per stream so stderr cannot splice mid-JSON. Both streams still feed one consumer; junk stderr lines are skipped by parseRunnerEvent. Good.
#16 partial: returning on data.truncated || killedForLimit without awaiting wait() is the abort-hang fix. Keep #16 open for UNION/CTE unclean failures + live large-result e2e after runner republish.
There was a problem hiding this comment.
Cursor Bugbot did not reach a terminal state within the 8-minute wait, so this run is not approving. Human review is needed; no reviewers were assigned because the only GitHub contributor on these paths is the PR author. Tests and Cursor Security Agent already passed.
Sent by Cursor Approval Agent: Pull Request Router and Approver
Pin DEFAULT_SANDBOX_IMAGE to the GHCR digest from Publish runner after #17 (MySQL watchdog + demux). Bump npm package to 0.1.5.


Issues tracked
SLEEPnot killed + NDJSON/mux parse errortruncated:trueSLEEP). Do not auto-close #16.Fixes #15
#16 is not
Fixeshere. The abort-hang subset is included; UNION/CTE 400s and live large-result e2e remain follow-up.#15 (primary) — timeout + parse
e2e against
ffp-sql-sandbox@0.1.4withSANDBOX_TIMEOUT_MS=10000:SELECT SLEEP(15)was not killed; the query returned success.Unexpected non-whitespace character after JSON at position 70(mux/NDJSON corruption).Root cause
SET SESSION MAX_EXECUTION_TIMEis not astatement_timeoutequivalent and does not interruptSLEEP(). The runner had no wall-clock abort.Fix
timeoutMsdestroys the MySQL socket (SLEEPanalog).QUERY_TIMEOUT/Query execution was interrupted/ Postgresstatement timeouttocode: 'TIMEOUT'.Postgres
statement_timeoutdoes cancelpg_sleep. MySQLMAX_EXECUTION_TIMEdoes not cancelSLEEP(). Dual timeout is watchdog attimeoutMsplus host container kill attimeoutMs + 2000ms.#16 (related) — truncated:true hang
Included (same abort-hang cause as the timeout/kill path): after
maxRows/maxBytesabort, the host usedPromise.all([consumed, wait]), so a hungcontainer.wait()/ mysql2end()delayed{ truncated: true }until the client timed out. This PR returns truncated as soon as the consumer hits the cap and capsend()(~200ms) thendestroy().Follow-up (not this root cause, keep #16 open):
Tests
test/mysql-query-stream.test.ts— non-returning query is destroyed withintimeoutMs; hungend()does not block close.test/query-watchdog.test.ts— watchdog fires/clears; timeout message matching.test/demux.test.ts/test/execute-sql.test.ts— stderr cannot splice a stdout NDJSON line;QUERY_TIMEOUTmaps toTIMEOUT; maxRows abort returns truncated without waiting onwait().Verified locally:
pnpm test(75 pass),pnpm typecheck,pnpm build.Follow-up (not in this PR)
DEFAULT_SANDBOX_IMAGE.SLEEP(15)+ large result) against the new runner digest.Reviewer notes
Start with
sandbox/mysql-query-stream.js(#15 watchdog),src/docker-types.ts(demuxStdout, #15 parse), andsrc/docker-executor.ts(timeout mapping + #16 return-on-truncate). Runner image consumers still on@0.1.4digest will not pick up the watchdog until a runner republish.