Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ gha-creds-*.json

## vscode settings
/.vscode
*.db.init.lock
1 change: 1 addition & 0 deletions changelog.d/local-db-init-race.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
17 changes: 15 additions & 2 deletions policyengine_api/data/data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fcntl
import sqlite3
from policyengine_api.constants import REPO, COUNTRY_PACKAGE_VERSIONS
from policyengine_api.utils import hash_object
Expand Down Expand Up @@ -106,8 +107,20 @@ def __init__(
if local:
# Local development uses a sqlite database.
self.db_url = REPO / "policyengine_api" / "data" / "policyengine.db"
if initialize or not Path(self.db_url).exists():
self.initialize()
# Serialize the exists-check + initialize under an exclusive file
# lock: with multiple gunicorn workers importing concurrently on a
# fresh instance, both can otherwise pass the exists() check and
# race initialize() (seed INSERTs collide -> worker dies at boot),
# or one can observe a created-but-unseeded file and skip
# initialization entirely.
lock_path = str(self.db_url) + ".init.lock"
with open(lock_path, "w") as lock_file:
fcntl.flock(lock_file, fcntl.LOCK_EX)
try:
if initialize or not Path(self.db_url).exists():
self.initialize()
finally:
fcntl.flock(lock_file, fcntl.LOCK_UN)
else:
self._create_pool()
if initialize:
Expand Down
Loading