Skip to content

fix(db): a timeout or a server-side cancel force-closes a shared session, and a queued statement can cancel another caller's #1216

Description

@ZhuchkaTriplesix

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.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.
  • 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_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.

Activity

  1. added
    bugSomething isn't working
    stabilityTheme parser epic label: stability
    coreCore library logic and services
    connectionsDatabase connections, URI parsing, pools
    P1High priority / Core capability
    on Oct 9, 2026
  2. added 2 commits that reference this issue on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priority / Core capabilitybugSomething isn't workingconnectionsDatabase connections, URI parsing, poolscoreCore library logic and servicesstabilityTheme parser epic label: stability

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions