Repository navigation
fix(database): skip idempotent schema statements and honour a lock timeout at init - #87
Merged
Merged
Conversation
…meout at init Postgres takes a table's lock before it evaluates IF NOT EXISTS, so every start's ALTER TABLE ... ADD COLUMN IF NOT EXISTS for a column that already exists still asks for ACCESS EXCLUSIVE on the table, and CREATE INDEX IF NOT EXISTS still asks for SHARE. When a long statement holds the table, the request waits and every later query on the table queues behind it: a routine start of a second instance stalls redirects until the long statement ends. - schema-guard.ts: guardSchemaClient answers idempotent statements whose effect already exists (a column, an index, an absent constraint, a settled NOT NULL) from the catalog, so they are never sent and never ask for a lock; anything else passes through untouched - initializeDatabase runs on a guarded client and accepts lockTimeoutMs: set after the schema advisory lock is held, reset before the client returns to the pool; a statement that still waits longer fails the start with SQLSTATE 55P03 and an operator-readable message so the host can retry - README: the option and the rule for new schema statements - tests: the guard's decisions; a second start completes while a transaction holds ACCESS SHARE on links and the setting does not leak; a bare ALTER that does need the table fails fast with 55P03
…ymlinked node_modules The ignore rule was `node_modules/`, which matches a directory but not a symlink of that name, so a worktree set up with a linked node_modules put the link itself into the previous commit. Ignore the name whatever it points at.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Why
initializeDatabase()runs its schema statements on every start. Postgres acquires a table's lock before it evaluatesIF NOT EXISTS, soALTER TABLE links ADD COLUMN IF NOT EXISTS …for a column that has existed for a year still requests ACCESS EXCLUSIVE onlinks, andCREATE INDEX IF NOT EXISTSfor an existing index still requests SHARE. If a long statement holds the table at that moment (a retention job, a large export), the request waits, and Postgres queues every later request on the table behind the waiting one. Redirects readlinks, so a routine start of a second instance can stall every redirect for as long as the long statement runs.What changes
src/lib/schema-guard.ts(new):guardSchemaClient(client)answers idempotent statements from the catalog when their effect already exists, so they are never sent and never ask for a lock:ALTER TABLE … ADD COLUMN IF NOT EXISTS(every column of a multi-action ALTER must exist; commas inside types and defaults do not split actions; a mixed ALTER passes through),CREATE [UNIQUE] INDEX [CONCURRENTLY] IF NOT EXISTS,DROP CONSTRAINT IF EXISTS,ALTER COLUMN … SET/DROP NOT NULL. Anything else, any multi-statement string and any parameterised query pass through untouched.initializeDatabase()runs on a guarded client and accepts a newlockTimeoutMsoption. It is applied after the schema advisory lock is held (waiting for another instance's initialisation is wanted; waiting for a table lock is not) and reset before the client returns to the pool. A statement that still waits longer fails the start with SQLSTATE55P03and an operator-readable message, so a host can retry with backoff or let its orchestrator restart. Default unset: behaviour unchanged for anyone not opting in.Tests
schema-guard.test.ts: the guard's decisions for each shape, multi-column and mixed ALTERs, parameterised queries untouched, SQLSTATE recognition.database.test.ts(Postgres,DATABASE_URL-gated like the existing cases): with a transaction holding ACCESS SHARE onlinks, a secondinitializeDatabase({ lockTimeoutMs: 2000 })completes in ~2 s, the reader is never blocked, andSHOW lock_timeouton a fresh pool client is0; a bare ALTER that does need the table fails with55P03after the timeout instead of queueing it.npm run buildclean.