Summary
In transaction pooling mode, when a client dies abruptly (e.g. SIGKILL, container stop) while holding a session-scoped advisory lock, the pinned server connection is returned to the pool still holding the lock. The lock is never released until that backend happens to be closed, and every other client polling pg_try_advisory_lock on the same key spins for minutes (or forever on a quiet pool).
The happy path works correctly: after SELECT pg_try_advisory_lock(a, b) the client is pinned to one backend, subsequent statements stay on it, and pg_advisory_unlock(a, b) releases and unpins. The leak only happens on abrupt disconnect mid-hold.
Version / config
- pgdog
v0.1.53 (official Docker image), also relevant on current main as far as I can tell
pooler_mode = "transaction", passthrough_auth = "enabled_plain", min_pool_size = 5, max_pool_size = 40, single primary database, no sharding
Repro
-
Connect a client through pgdog and take a session advisory lock:
psql "postgres://user:pass@pgdog:6432/db" \
-c "SELECT pg_try_advisory_lock(999125, 0)" \
-c "SELECT pg_sleep(30)"
-
While it sleeps (lock held), kill -9 the psql process.
-
On a direct connection to Postgres:
SELECT * FROM pg_locks WHERE locktype = 'advisory';
The lock is still granted to the pooled backend, which is back in the general pool serving other clients. It stays leaked indefinitely; in a busy pool we observed dozens of leaked locks accumulating on long-lived backends (28 locks on a single ~1h-old server connection).
Expected
When a client that pgdog knows is holding session advisory locks (it tracks them for pinning) disconnects without releasing them, the server connection should either run the dirty cleanup (SELECT pg_advisory_unlock_all() — it already exists in src/backend/pool/cleanup.rs as the DIRTY query set) or be closed so Postgres frees the session's locks.
Impact
After a deploy SIGKILLed app processes mid-hold, every request for the affected lock keys polled pg_try_advisory_lock in a loop (the common Ruby with_advisory_lock gem pattern) for 5–10 minutes per request until backends were recycled. SHOW POOLS / SHOW STATS look completely healthy while this is happening — cl_waiting 0, maxwait 0 — since the waiters are actively polling, which makes it quite hard to spot from the pgdog side.
Happy to test a fix or provide more detail.
Summary
In transaction pooling mode, when a client dies abruptly (e.g. SIGKILL, container stop) while holding a session-scoped advisory lock, the pinned server connection is returned to the pool still holding the lock. The lock is never released until that backend happens to be closed, and every other client polling
pg_try_advisory_lockon the same key spins for minutes (or forever on a quiet pool).The happy path works correctly: after
SELECT pg_try_advisory_lock(a, b)the client is pinned to one backend, subsequent statements stay on it, andpg_advisory_unlock(a, b)releases and unpins. The leak only happens on abrupt disconnect mid-hold.Version / config
v0.1.53(official Docker image), also relevant on currentmainas far as I can tellpooler_mode = "transaction",passthrough_auth = "enabled_plain",min_pool_size = 5,max_pool_size = 40, single primary database, no shardingRepro
Connect a client through pgdog and take a session advisory lock:
While it sleeps (lock held),
kill -9the psql process.On a direct connection to Postgres:
The lock is still granted to the pooled backend, which is back in the general pool serving other clients. It stays leaked indefinitely; in a busy pool we observed dozens of leaked locks accumulating on long-lived backends (28 locks on a single ~1h-old server connection).
Expected
When a client that pgdog knows is holding session advisory locks (it tracks them for pinning) disconnects without releasing them, the server connection should either run the dirty cleanup (
SELECT pg_advisory_unlock_all()— it already exists insrc/backend/pool/cleanup.rsas theDIRTYquery set) or be closed so Postgres frees the session's locks.Impact
After a deploy SIGKILLed app processes mid-hold, every request for the affected lock keys polled
pg_try_advisory_lockin a loop (the common Rubywith_advisory_lockgem pattern) for 5–10 minutes per request until backends were recycled.SHOW POOLS/SHOW STATSlook completely healthy while this is happening —cl_waiting 0,maxwait 0— since the waiters are actively polling, which makes it quite hard to spot from the pgdog side.Happy to test a fix or provide more detail.