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

Closed

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 3 commits that reference this issue on Oct 9, 2026
  3. ZhuchkaTriplesix commented on Oct 10, 2026

    @ZhuchkaTriplesix
    MemberAuthor

    Resolved and merged in PR #1260.

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