Skip to content

Commit 3f95cd4

Browse files
anth-volkclaude
andcommitted
Serialize local sqlite initialization under a file lock
With WEB_CONCURRENCY=2, both gunicorn workers import the app concurrently on a fresh instance; both could pass the sqlite exists-check and race initialize() — the losing worker died at boot on 'UNIQUE constraint failed: policy.id' (observed as 503 bursts during the first real Cloud Run scale-out, Stage 5 LB validation), and a worker could also observe a created-but-unseeded database and skip initialization entirely, serving with empty reference tables. An exclusive fcntl lock around the exists-check + initialize means exactly one process initializes while the others wait, then see the complete file and skip. Empirical verification (6 concurrent fresh boots x 3 rounds): unfixed code failed every round (boot crashes and row counts of 0/2/5 across workers); fixed code passes with all workers seeing exactly 5 seed rows. Fixes #3746 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 56d8c69 commit 3f95cd4

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Serialize local sqlite initialization under a file lock so concurrent gunicorn worker boots cannot race the seed inserts or observe a partially initialized database.

policyengine_api/data/data.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fcntl
12
import sqlite3
23
from policyengine_api.constants import REPO, COUNTRY_PACKAGE_VERSIONS
34
from policyengine_api.utils import hash_object
@@ -106,8 +107,20 @@ def __init__(
106107
if local:
107108
# Local development uses a sqlite database.
108109
self.db_url = REPO / "policyengine_api" / "data" / "policyengine.db"
109-
if initialize or not Path(self.db_url).exists():
110-
self.initialize()
110+
# Serialize the exists-check + initialize under an exclusive file
111+
# lock: with multiple gunicorn workers importing concurrently on a
112+
# fresh instance, both can otherwise pass the exists() check and
113+
# race initialize() (seed INSERTs collide -> worker dies at boot),
114+
# or one can observe a created-but-unseeded file and skip
115+
# initialization entirely.
116+
lock_path = str(self.db_url) + ".init.lock"
117+
with open(lock_path, "w") as lock_file:
118+
fcntl.flock(lock_file, fcntl.LOCK_EX)
119+
try:
120+
if initialize or not Path(self.db_url).exists():
121+
self.initialize()
122+
finally:
123+
fcntl.flock(lock_file, fcntl.LOCK_UN)
111124
else:
112125
self._create_pool()
113126
if initialize:

0 commit comments

Comments
 (0)