Repository navigation
fix(db): a timeout or a server-side cancel force-closes a shared session, and a queued statement can cancel another caller's #1216
Copy link
Copy link
Closed
Labels
P1High priority / Core capabilityHigh priority / Core capabilitybugSomething isn't workingSomething isn't workingconnectionsDatabase connections, URI parsing, poolsDatabase connections, URI parsing, poolscoreCore library logic and servicesCore library logic and servicesstabilityTheme parser epic label: stabilityTheme parser epic label: stability
Description
Part of #1212.
Problem
One caller's timeout, or a cancel the server answered correctly, closes a session that other callers share. And with postgres 3.5.17, a statement waiting in line can cancel another caller's statement.
1. Every TimeoutException force-closes the connection.
PostgresConnection.executeandexecuteWithTimeoutcallforceClose()onTimeoutException(lib/core/database/postgres_connection.dart:382-394,:397-409).- Same in
MysqlConnection(lib/core/database/mysql_connection.dart:437,:497) andSqliteConnection(lib/core/database/sqlite_connection.dart:218,:252). - On a pooled
readOnlysession this drops the tree, stats, table tabs, Relations view and MCP calls together.
2. A graceful server-side cancel is treated as a broken connection.
- In
postgres3.5.17 a cancelled statement (SQLSTATE57014, "canceling statement due to user request") is_PgQueryCancelledException, which implementsTimeoutException(postgres/lib/src/exceptions.dart:226). - The package itself keeps the connection in that case and closes only on a bare
TimeoutException, when the protocol is out of sync (postgres/lib/src/v3/connection.dart,_waitForResult, around:1185-1225). - Our wrapper catches it as a
TimeoutExceptionand force-closes a healthy session.
3. The statement timer counts the time spent waiting for the connection.
postgres3.5.17 serialises statements on oneConnectionwith_operationLock = Pool(1)(v3/connection.dart:46); a statement waits in_scheduleStatement→_withResourceuntil the previous one is done.- The timeout timer starts in
_waitForResult, i.e. before the statement gets the lock. When it fires, the package callscancelPendingStatement(), which sends aCancelRequestfor the backend — and the backend cancels the statement running now, which belongs to another caller. - Timeouts in play: 30 s driver default (
queryTimeout,postgres_connection.dart:196), 15 s for MCP (lib/core/mcp/mcp_query_service.dart:44), the SQL editor's statement timeout. - Example: an MCP
run_querywaits 15 s behind a slow table-browserSELECTon the read-only session → theSELECTis cancelled → its caller gets57014→ our wrapper (point 2) force-closes the session → everyone else on it fails. - Example: the Diagram tab loads its catalog on the editor session while a long editor query runs; after 30 s the catalog statement cancels the editor's query and the editor's open transaction is lost (fix(erd): the Diagram tab reads the catalog through the SQL editor session (implicit BEGIN, aborted transactions, LIMIT 5000) #1218).
4. MySQL waits for the previous query by polling, with a 10 s cap.
mysql_clienthas no queue:executepolls_stateevery 100 µs (third_party/mysql_client/lib/src/mysql_client/connection.dart:1169-1182) and gives up after_timeoutMs= 10 s (:471,:771,:890) with aTimeoutException.- Our wrapper then force-closes the session (point 1). A query that merely waited 10 s behind another one kills the connection for both.
- The 100 µs polling also burns CPU while a long query runs.
Scope
- Do not force-close on a server-side cancel: treat
PgException/ SQLSTATE57014as "statement cancelled, session healthy". Force-close only on a bareTimeoutException(protocol out of sync). - Serialise statements per pooled session in our own layer (a future-based lock per connection) and start the statement timeout only when the statement actually starts. A statement that waits too long for the lock fails with its own error ("waited too long for the connection") and cancels nothing.
- MySQL: queue in our layer instead of relying on the client's polling and 10 s cap; report waiting and running timeouts separately.
- Prefer server-side statement timeouts (
SET statement_timeoutfor PostgreSQL,max_execution_timefor MySQL SELECTs) so a runaway statement is stopped by the server and the session stays usable. - Error texts that tell the user which one happened: "Query timed out after N s" vs "Waited N s for the connection".
Acceptance
- Unit test (fake postgres connection): a statement cancelled with
57014leaves the session connected; the next statement runs on it. - Unit test: two statements on one session, the first runs longer than the second's timeout → the first is not cancelled; the second's timeout counts from its own start.
- Unit test (fake MySQL connection): a second query waiting more than 10 s behind the first does not close the session.
- Unit test: a bare
TimeoutExceptionstill force-closes (protocol out of sync), and only that session.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P1High priority / Core capabilityHigh priority / Core capabilitybugSomething isn't workingSomething isn't workingconnectionsDatabase connections, URI parsing, poolsDatabase connections, URI parsing, poolscoreCore library logic and servicesCore library logic and servicesstabilityTheme parser epic label: stabilityTheme parser epic label: stability
Activity
test(db): pass the connection id in the statement queue probes, drive…
Merge pull request #1222 from QueryaHub/fix/1216-timeouts-shared-session
Merge pull request #1260 from QueryaHub/issue/1216-session-timeouts
ZhuchkaTriplesix commentedon Oct 10, 2026
Resolved and merged in PR #1260.