Fork of patx/hglab.
tokenmess/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
Comments
No comments yet.
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")