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): concurrent connect() on a pooled connection leaks sockets and SSH tunnels, a failed attempt drops the working session #1214
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
Merge pull request #1220 from QueryaHub/fix/1214-concurrent-connect
ZhuchkaTriplesix commentedon Oct 9, 2026
Closed by #1220.