You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
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.execute and executeWithTimeout call forceClose() on TimeoutException (lib/core/database/postgres_connection.dart:382-394, :397-409).
Same in MysqlConnection (lib/core/database/mysql_connection.dart:437, :497) and SqliteConnection (lib/core/database/sqlite_connection.dart:218, :252).
On a pooled readOnly session 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 postgres 3.5.17 a cancelled statement (SQLSTATE 57014, "canceling statement due to user request") is _PgQueryCancelledException, which implements TimeoutException (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 TimeoutException and force-closes a healthy session.
3. The statement timer counts the time spent waiting for the connection.
postgres 3.5.17 serialises statements on one Connection with _operationLock = Pool(1) (v3/connection.dart:46); a statement waits in _scheduleStatement → _withResource until the previous one is done.
The timeout timer starts in _waitForResult, i.e. before the statement gets the lock. When it fires, the package calls cancelPendingStatement(), which sends a CancelRequest for 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_query waits 15 s behind a slow table-browser SELECT on the read-only session → the SELECT is cancelled → its caller gets 57014 → our wrapper (point 2) force-closes the session → everyone else on it fails.
4. MySQL waits for the previous query by polling, with a 10 s cap.
mysql_client has no queue: execute polls _state every 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 a TimeoutException.
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 / SQLSTATE 57014 as "statement cancelled, session healthy". Force-close only on a bare TimeoutException (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_timeout for PostgreSQL, max_execution_time for 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 57014 leaves 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 TimeoutException still force-closes (protocol out of sync), and only that session.
Part of #1212.
Problem
One caller's timeout, or a cancel the server answered correctly, closes a session that other callers share. And with
postgres3.5.17, a statement waiting in line can cancel another caller's statement.1. Every
TimeoutExceptionforce-closes the connection.PostgresConnection.executeandexecuteWithTimeoutcallforceClose()onTimeoutException(lib/core/database/postgres_connection.dart:382-394,:397-409).MysqlConnection(lib/core/database/mysql_connection.dart:437,:497) andSqliteConnection(lib/core/database/sqlite_connection.dart:218,:252).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.
postgres3.5.17 a cancelled statement (SQLSTATE57014, "canceling statement due to user request") is_PgQueryCancelledException, which implementsTimeoutException(postgres/lib/src/exceptions.dart:226).TimeoutException, when the protocol is out of sync (postgres/lib/src/v3/connection.dart,_waitForResult, around:1185-1225).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._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.queryTimeout,postgres_connection.dart:196), 15 s for MCP (lib/core/mcp/mcp_query_service.dart:44), the SQL editor's statement timeout.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.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.Scope
PgException/ SQLSTATE57014as "statement cancelled, session healthy". Force-close only on a bareTimeoutException(protocol out of sync).SET statement_timeoutfor PostgreSQL,max_execution_timefor MySQL SELECTs) so a runaway statement is stopped by the server and the session stays usable.Acceptance
57014leaves the session connected; the next statement runs on it.TimeoutExceptionstill force-closes (protocol out of sync), and only that session.