Fork of patx/hglab.

enable sqlite wal mode for saftey with three gunicorn workers and local persistant filesystem

Commit 4dcf089a2cb1 · Harrison Erd · 2026-05-04 22:54 -0400

Changeset
4dcf089a2cb1fff93ab3fa15aee2d48bb61c42d6

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -36,3 +36,5 @@
 - `PORT`: HTTP port. Defaults to `8080`.
 
 This v1 stores repositories on local disk. Do not deploy it to ephemeral filesystems unless repository storage is mounted persistently.
+
+SQLite is configured with WAL mode and a busy timeout so a small multi-worker deployment can share one database file. Keep `HG_HOST_DB` on a local persistent filesystem used by one host; network or synced filesystems can break SQLite locking semantics and should use a server database instead.
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -38,6 +38,7 @@
 SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-change-me")
 DEBUG = os.environ.get("HG_HOST_DEBUG", "").lower() in {"1", "true", "yes", "on"}
 PASSWORD_ITERATIONS = 260_000
+SQLITE_BUSY_TIMEOUT_MS = 30_000
 SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{1,62}$")
 REV_RE = re.compile(r"^(null|[0-9a-fA-F]{1,40})$")
 REF_TYPE_BRANCH = "branch"
@@ -153,16 +154,24 @@
     REPO_ROOT.mkdir(parents=True, exist_ok=True)
 
 
+def configure_db_connection(conn):
+    conn.execute("PRAGMA foreign_keys = ON")
+    conn.execute(f"PRAGMA busy_timeout = {SQLITE_BUSY_TIMEOUT_MS}")
+    conn.execute("PRAGMA synchronous = NORMAL")
+
+
 def db_connect():
-    conn = sqlite3.connect(DB_PATH)
+    conn = sqlite3.connect(DB_PATH, timeout=SQLITE_BUSY_TIMEOUT_MS / 1000)
     conn.row_factory = sqlite3.Row
-    conn.execute("PRAGMA foreign_keys = ON")
+    configure_db_connection(conn)
     return conn
 
 
 def init_db():
     ensure_dirs()
     with db_connect() as conn:
+        conn.execute("PRAGMA journal_mode = WAL")
+    with db_connect() as conn:
         conn.executescript(
             """
             CREATE TABLE IF NOT EXISTS users (
diff --git a/tests/test_app.py b/tests/test_app.py
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -4,6 +4,8 @@
 from io import BytesIO, StringIO
 import os
 import shutil
+import subprocess
+import sys
 import tempfile
 from pathlib import Path
 from urllib.parse import urlencode, urlsplit, unquote
@@ -345,6 +347,87 @@
     assert {"target_ref_type", "target_ref_name", "source_ref_type", "source_ref_name"}.issubset(pr_columns)
 
 
+def test_db_connect_configures_sqlite_for_worker_contention(isolated_app):
+    isolated_app.init_db()
+
+    with isolated_app.db_connect() as conn:
+        foreign_keys = conn.execute("PRAGMA foreign_keys").fetchone()[0]
+        busy_timeout = conn.execute("PRAGMA busy_timeout").fetchone()[0]
+        synchronous = conn.execute("PRAGMA synchronous").fetchone()[0]
+        journal_mode = conn.execute("PRAGMA journal_mode").fetchone()[0]
+
+    assert foreign_keys == 1
+    assert busy_timeout == hglab.SQLITE_BUSY_TIMEOUT_MS
+    assert synchronous == 1
+    assert journal_mode.lower() == "wal"
+
+
+def test_sqlite_accepts_concurrent_worker_writes(isolated_app):
+    isolated_app.init_db()
+    repo_root = Path(__file__).resolve().parents[1]
+    env = os.environ.copy()
+    existing_pythonpath = env.get("PYTHONPATH")
+    env["PYTHONPATH"] = str(repo_root) if not existing_pythonpath else f"{repo_root}{os.pathsep}{existing_pythonpath}"
+
+    worker_script = """
+import os
+import sys
+import time
+
+os.environ["HG_HOST_DB"] = sys.argv[1]
+os.environ["HG_HOST_REPO_ROOT"] = sys.argv[2]
+os.environ["SECRET_KEY"] = "test-secret"
+
+import app as hglab
+
+with hglab.db_connect() as conn:
+    conn.execute("BEGIN IMMEDIATE")
+    conn.execute(
+        "INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)",
+        (sys.argv[3], "hash", hglab.utcnow()),
+    )
+    time.sleep(0.2)
+"""
+
+    processes = [
+        subprocess.Popen(
+            [
+                sys.executable,
+                "-c",
+                worker_script,
+                str(isolated_app.DB_PATH),
+                str(isolated_app.REPO_ROOT),
+                f"worker-{index}",
+            ],
+            cwd=repo_root,
+            env=env,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            text=True,
+        )
+        for index in range(3)
+    ]
+
+    results = []
+    try:
+        for process in processes:
+            stdout, stderr = process.communicate(timeout=15)
+            results.append((process.returncode, stdout, stderr))
+    finally:
+        for process in processes:
+            if process.poll() is None:
+                process.kill()
+
+    assert results
+    for returncode, stdout, stderr in results:
+        assert returncode == 0, stdout + stderr
+
+    with isolated_app.db_connect() as conn:
+        count = conn.execute("SELECT COUNT(*) FROM users WHERE username LIKE 'worker-%'").fetchone()[0]
+
+    assert count == 3
+
+
 def test_create_repository_persists_metadata_and_writes_hgrc(isolated_app):
     owner = create_user("alice")