Repository navigation
Conversation
pg_locks is cluster-wide while an advisory lock is per database, so a lock session dialled into another database of the same server confirmed the lock it held there and the run went ahead with a lock that excluded nothing on the working database's table.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TableLockSession.ConfirmandLookupTableLockHoldernow readpg_locksscoped to the connection's own database, so a table lock held in another database of the same server no longer confirms as this database's lock.Why
An advisory lock excludes only sessions of the database it was taken in, but
pg_lockslists every database's grants. The holder lookup behindConfirmhad nodatabasepredicate, so a working transaction in database A found the lock a session held in database B — same server, same key — andConfirmreturned nil. The run then executed with a lock that kept nothing off the table in A; a second run correctly wired to A would take its own lock there without contention. Across clusters the lookup finds nothing and the run already refused, so this is specifically the same-server, other-database wiring.pkg/migratealready stops that wiring at the front door (CopySwapConnectionMismatchError), but anything that reaches the lock through another door —schemachange.NewRunner(...).Run, orAcquireTableLockplusConfirmdirectly, all exported — was still confirmed against a lock in any database. The predicate closes the gap at its source and makesshadow-lock-unconfirmedmean what it says.What
lookupTableLockHolderaddsl.database = (SELECT oid FROM pg_database WHERE datname = current_database()). It servesConfirm,LookupTableLockHolder, and the contention holder report inAcquireTableLock, so all three now describe only this database's grant.sessionHoldsAdvisoryLockwas already correct: it filters onpid = pg_backend_pid(), and one backend holds locks in one database.TestTableLockConfirmIgnoresTheSameKeyHeldInAnotherDatabase: lock taken from database B,Confirmfrom a transaction on database A isErrTableLockNotHeldand not aTableLockHeldError;LookupTableLockHolderon A finds nobody while on B it reports the session's PID. Fails onmainat both assertions (Confirmreturned nil; A "found" B's holder).docs/invariants.mdLK-1 Enforced: one clause for the database scope.Before / after
Same wiring on both sides: the lock session is dialled into database
b, the pool writes to databasea, both on one server. The table isapp.orders; the key is(0x70677370, hashtext('app.orders')).Before, the working transaction on
aaskspg_locksfor that key with no database filter, sees the grant inb, andConfirmsays the lock is held, so the write proceeds unprotected. After, the same query is scoped toa's OID, finds no grant, andConfirmreturnsErrTableLockNotHeld; the caller fails closed before its first write. Nothing changes for the correctly wired case, where lock and work share one database.The front-door check in
pkg/migrateis unchanged and still stops this wiring before the preflight; this PR is the safety net underneath it for every other entry point.🤖 Generated with Amp (Claude Opus 4.6)