Skip to content

fix(db): concurrent connect() on a pooled connection leaks sockets and SSH tunnels, a failed attempt drops the working session #1214

Description

@ZhuchkaTriplesix

Part of #1212.

Problem

connect() on a pooled connection is not guarded against concurrent calls.

  • PostgresConnectionPool.acquire() reconnects a dropped entry with await entry.connection.connect() and then setSessionReadOnly(...) (lib/core/database/postgres_connection_pool.dart:92-95 and :128-131). Two callers that reuse the same dropped entry both get there.
  • PostgresConnection.connect() (lib/core/database/postgres_connection.dart:204) only checks _isConnected && _conn != null, which is false for both callers while the first is still connecting. Both:
    • read the secrets,
    • open an SSH tunnel when the connection uses one (_sshTunnelHandle = await SshTunnelManager.instance.openTunnel(...)),
    • open a socket (_conn = await Connection.open(...)).
  • The later assignment wins. The earlier Connection and tunnel are never closed (leak until the app exits).
  • If one attempt fails after the other succeeded, its catch sets _conn = null, _isConnected = false and releases _sshTunnelHandle — by then the other attempt's tunnel. The working session is dropped and the next statement fails with Not connected to PostgreSQL.

MysqlConnection.connect() (lib/core/database/mysql_connection.dart:167-265) has the same structure and the same races (socket, SSH tunnel, failure path).

SQLite (SqliteConnection.connect, lib/core/database/sqlite_connection.dart:88) has the same guard, but sqflite reuses one instance per path, so it is likely unaffected (not verified).

When it happens: any time a shared slot was dropped (tab dispose, timeout, network blip) and two views ask for it at once — e.g. the tree and a table tab refreshing together, or a table tab and an MCP call.

Scope

  • connect() keeps the in-flight attempt (Future<void>? _connecting); concurrent callers await the same future. _connecting is cleared when it completes.
  • On failure, clean up only what this attempt created (its socket, its tunnel), never fields set by another attempt.
  • The pool reconnects a dropped entry once (per-entry reconnect future or under PoolEntryLock), then runs setSessionReadOnly once.
  • Same for MySQL.

Acceptance

  • Unit test (fake connection): two concurrent connect() calls open one socket and one tunnel.
  • Unit test: a failing attempt does not close a session another attempt opened.
  • Pool test: two concurrent acquire() on a dropped entry → one reconnect, one SET default_transaction_read_only.

Activity

  1. added
    bugSomething isn't working
    stabilityTheme parser epic label: stability
    connectionsDatabase connections, URI parsing, pools
    P2Medium priority / Parity & Refactoring
    on Oct 9, 2026
  2. added a commit that references this issue on Oct 9, 2026
  3. ZhuchkaTriplesix commented on Oct 9, 2026

    @ZhuchkaTriplesix
    MemberAuthor

    Closed by #1220.

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

    P2Medium priority / Parity & RefactoringbugSomething isn't workingconnectionsDatabase connections, URI parsing, poolsstabilityTheme parser epic label: stability

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions