From dccd7fd0a37781e05082a341a538b3a02ed64609 Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Sun, 30 Aug 2026 15:20:04 -0700 Subject: [PATCH] Scope Docker WAL guard to SQLite --- archivebox/config/common.py | 9 ++++----- archivebox/tests/test_cli_server.py | 31 +++++++++++++++++++++++++++++ docs/Configuration.md | 6 +++--- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/archivebox/config/common.py b/archivebox/config/common.py index f79eabe0..125b71c0 100644 --- a/archivebox/config/common.py +++ b/archivebox/config/common.py @@ -423,15 +423,14 @@ class DatabaseConfig(BaseConfigSet): SQLITE_LOCK_RETRY_TIMEOUT: float = Field(default=60.0, alias="ARCHIVEBOX_SQLITE_LOCK_RETRY_TIMEOUT", ge=0) SQLITE_LOCK_RETRY_INTERVAL: float = Field(default=5.0, alias="ARCHIVEBOX_SQLITE_LOCK_RETRY_INTERVAL", gt=0) - @field_validator("SQLITE_JOURNAL_MODE", mode="after") - @classmethod - def reject_docker_wal(cls, value: str) -> str: - if IN_DOCKER and value.upper() == "WAL": + @model_validator(mode="after") + def reject_docker_sqlite_wal(self): + if IN_DOCKER and self.DATABASE_ENGINE.lower() == "sqlite" and self.SQLITE_JOURNAL_MODE.upper() == "WAL": raise ValueError( "SQLITE_JOURNAL_MODE=WAL is unsafe for Docker collections because host bind mounts cross SQLite " "locking domains; use DELETE (the Docker default) or PostgreSQL", ) - return value + return self class ArchivingConfig(BaseConfigSet): diff --git a/archivebox/tests/test_cli_server.py b/archivebox/tests/test_cli_server.py index b8f32513..49e48a97 100644 --- a/archivebox/tests/test_cli_server.py +++ b/archivebox/tests/test_cli_server.py @@ -268,6 +268,37 @@ def test_docker_rejects_explicit_wal_override(tmp_path): assert "WAL is unsafe for Docker collections" in result.stderr +def test_docker_postgres_ignores_irrelevant_sqlite_wal_override(tmp_path): + """The Docker SQLite safety invariant must not reject PostgreSQL. + + Operators can switch an existing deployment to PostgreSQL while an old + SQLITE_JOURNAL_MODE setting remains in ArchiveBox.conf or the environment. + PostgreSQL never consumes that SQLite pragma, so rejecting the otherwise + valid configuration would prevent ArchiveBox from starting without making + any database safer. + """ + env = os.environ.copy() + env["IN_DOCKER"] = "True" + result = subprocess.run( + [ + sys.executable, + "-c", + ( + "from archivebox.config.common import DatabaseConfig;" + "config=DatabaseConfig(DATABASE_ENGINE='postgres',SQLITE_JOURNAL_MODE='WAL');" + "print(config.DATABASE_ENGINE)" + ), + ], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "postgres" + + def test_server_shows_usage_info(initialized_archive): """Test that server command shows usage or starts.""" diff --git a/docs/Configuration.md b/docs/Configuration.md index 1c38d530..a7ce83fb 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -717,13 +717,13 @@ With the default [`DATABASE_ENGINE`](#database_engine)`=sqlite`, this is the pat --- #### `SQLITE_JOURNAL_MODE` -**Possible Values:** [`WAL`]/`DELETE`/`TRUNCATE`/`PERSIST`/`MEMORY`/`OFF` +**Possible Values:** [`WAL` on native installs]/[`DELETE` in Docker]/`TRUNCATE`/`PERSIST`/`MEMORY`/`OFF` SQLite [journal mode](https://www.sqlite.org/pragma.html#pragma_journal_mode), applied via `PRAGMA journal_mode = ...` on every new connection. Settable as `ARCHIVEBOX_SQLITE_JOURNAL_MODE`. -The default `WAL` (Write-Ahead Logging) lets readers and a single writer operate concurrently without blocking each other — readers see a stable snapshot while a write is in progress, instead of being serialized behind it. This is a substantial win for ArchiveBox, where the web UI, admin, and CLI workers frequently read the index while an extractor is writing. +The default is `WAL` (Write-Ahead Logging) on native installs and `DELETE` in Docker. WAL lets readers and a single writer operate concurrently without blocking each other, but its shared-memory locking is unsafe when a Docker bind mount exposes the same live database to host-side SQLite processes. Docker therefore rejects an explicit WAL override for the SQLite backend; use PostgreSQL for safe cross-runtime concurrency. > [!WARNING] -> Do not change this unless you have a specific reason. `DELETE` and `TRUNCATE` serialize all readers against any writer (much worse concurrency). `MEMORY` and `OFF` disable durable journaling and can corrupt the database on crash or power loss. `WAL` requires the database to live on a real local filesystem — it does not work correctly over network filesystems like NFS or SMB. +> Do not change this unless you have a specific reason. `DELETE` and `TRUNCATE` serialize readers against writers. `MEMORY` and `OFF` disable durable journaling and can corrupt the database on crash or power loss. WAL requires one local filesystem and one locking domain; it is unsafe across network filesystems and Docker host bind mounts. --- #### `SQLITE_MMAP_SIZE`