Report #68381
[bug\_fix] Application hang or indefinite wait when using pg\_advisory\_lock with external connection pooling \(PgBouncer, AWS RDS Proxy, or application-side pools\)
Replace session-level advisory locks \(\`pg\_advisory\_lock\`, \`pg\_advisory\_unlock\`\) with transaction-level advisory locks \(\`pg\_advisory\_xact\_lock\`\) which automatically release when the transaction commits or rolls back. Alternatively, ensure explicit \`pg\_advisory\_unlock\` is called in a \`finally\` block before returning the connection to the pool. Root cause: Session-level advisory locks persist until explicitly unlocked or the database session \(backend process\) terminates. Connection pools reuse persistent TCP connections \(sessions\), so when App Request A acquires a lock and returns the connection to the pool, the lock remains held. When App Request B obtains the same connection from the pool and tries to acquire the same lock, it hangs indefinitely because it cannot release the lock held by the previous logical request using the same physical session.
Journey Context:
Your Ruby on Rails API uses Sidekiq background jobs to process webhooks. To prevent duplicate processing of the same event ID, you implement distributed locking using \`ActiveRecord::Base.connection.execute\("SELECT pg\_advisory\_lock\(\#\{event\_id.hash\}\)"\). You place the corresponding unlock in an \`ensure\` block. In development \(single process\), this works perfectly. In production with Puma \(multi-threaded\) and PgBouncer in transaction pooling mode, jobs randomly hang indefinitely. You check \`pg\_stat\_activity\` and see multiple queries in \`active\` state waiting on \`ClientRead\` or \`AdvisoryLock\`. You realize that when a job completes, the advisory lock is unlocked, but the connection returns to PgBouncer's pool. The next job that gets that same physical connection from PgBouncer finds the session still has the advisory lock if the previous \`unlock\` wasn't reached \(e.g., exception thrown past ensure block\), or worse, the new job tries to lock a different ID while holding a lock from a previous logical session. You switch to \`pg\_advisory\_xact\_lock\` which automatically releases when the transaction commits, ensuring locks never leak across pooled connections. The indefinite hangs disappear immediately.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-20T21:15:39.039625+00:00— report_created — created