fix(database): serialise schema initialisation with an advisory lock - #45
Open
onamfc wants to merge 2 commits into
Open
fix(database): serialise schema initialisation with an advisory lock#45onamfc wants to merge 2 commits into
onamfc wants to merge 2 commits into
Conversation
`initializeDatabase()` is safe to call from one process and unsafe to call from several at once against an empty database. `CREATE TABLE IF NOT EXISTS` is not atomic: the existence check and the creation are separate steps, so two connections can both find a table absent and both try to create it. The loser does not quietly do nothing — it fails with `duplicate key value violates unique constraint "pg_type_typname_nsp_index"`. The `CREATE INDEX IF NOT EXISTS` and `ADD COLUMN IF NOT EXISTS` statements have the same property. Reproduced by running four initialisations concurrently against one empty database: three of the four crashed, and the survivor produced the correct schema. Against an already-initialised database all four succeed, because every statement short-circuits — which is why this only shows up on the first boot of a new environment that starts more than one instance, and why it is invisible in ordinary use. A session-level advisory lock around the initialisation makes the second caller wait rather than race. Postgres releases it automatically when the connection ends, so a process that dies mid-initialisation cannot wedge the others, and waiting is the desired behaviour anyway: another instance is building the schema this one is about to use. The key is exported so a host application taking its own advisory locks on the same database can avoid colliding with it. Adds a database-backed test covering both halves of the contract — that initialisation blocks while another session holds the lock, and that the lock is released once it completes. The behaviour under test is a property of Postgres locking, so a mocked client would accept any sequence of queries and prove nothing; it needs a real server. The test skips itself when DATABASE_URL is absent, so `npm test` stays green without one, and a CI job now supplies a Postgres service so it actually runs.
…ents
Code comments carried issue ids from a private tracker, and one test header
named a file path in a downstream private repository. Neither means anything to
a reader of this repository, and the file path described private structure.
Every comment keeps its explanation; only the identifiers are gone. Two needed
rewording rather than deletion, because the id was carrying the sentence:
- a regression test described itself by ticket; it now describes the
behaviour it reproduces
- a note about which consumer must not misread a null column named that
consumer by ticket; it now names it by what it does
The preview-scraper suite no longer points at a downstream file. It says what
is actually true for this repository: a host application may register its own
preview handler ahead of these routes, so this list governs standalone
deployments.
Generated changelog entries are stripped of the same ids. The underlying commit
subjects still contain them and are not practically rewritable; this only stops
the rendered changelog repeating them.
Two references to the hosted product remain and look deliberate rather than
leaked — the README link to it, and a comment marking an endpoint as a premium
feature. Left alone.
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 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.
Two commits: the schema-initialisation fix, and a comment cleanup.
1. Serialise schema initialisation with an advisory lock
initializeDatabase()is safe to call from one process and unsafe to call from several at once against an empty database.CREATE TABLE IF NOT EXISTSis not atomic. The existence check and the creation are separate steps, so two connections can both find a table absent and both attempt to create it. The loser does not quietly do nothing — it fails:The
CREATE INDEX IF NOT EXISTSandADD COLUMN IF NOT EXISTSstatements have the same property.Reproduction
Four initialisations run concurrently against one empty database:
Against an already-initialised database all four succeed even without the fix, because every statement short-circuits. That is why this only surfaces on the first boot of a new environment that starts more than one instance — or a test harness initialising in parallel — and why it stays invisible in ordinary use.
The fix
A session-level advisory lock around the initialisation, so the second caller waits instead of racing.
SCHEMA_INIT_LOCK_KEYis exported so an embedding application taking its own advisory locks on the same database can avoid colliding with it.Testing
A database-backed test covers both halves of the contract: that initialisation blocks while another session holds the lock, and that the lock is released once it completes. A leaked lock would block every subsequent boot, so the second half matters as much as the first.
The behaviour under test is a property of Postgres locking — a mocked client would accept any sequence of queries and prove nothing — so it needs a real server. The test skips itself when
DATABASE_URLis absent, keepingnpm testgreen without one, and a new CI job supplies a Postgres service so it actually runs rather than silently skipping.Verified by mutation: with the lock removed, both cases fail.
2. Remove internal tracker ids and downstream references from comments
Code comments carried issue ids from a private tracker, and one test header named a file path in a downstream private repository. Neither means anything to a reader of this repository, and the file path described private structure.
Every comment keeps its explanation; only the identifiers are gone. Two needed rewording rather than deletion, because the id was carrying the sentence:
The preview-scraper suite no longer points at a downstream file. It states what is actually true here: a host application may register its own preview handler ahead of these routes, so this list governs standalone deployments.
Generated changelog entries are stripped of the same ids. The underlying commit subjects still contain them and are not practically rewritable; this only stops the rendered changelog repeating them.
Two references to the hosted product remain and look deliberate rather than leaked — the README link to it, and a comment marking an endpoint as a premium feature. Left alone; happy to change either if you would rather they went.
Full suite: 254 passed / 2 skipped without a database, 256 passed with one. Type check and build clean.