Compare commits

..

No commits in common. "master" and "v1.2.5" have entirely different histories.

View file

@ -1,10 +1,8 @@
"""SQLite database layer with async CRUD operations.
"""SQLite database layer with WAL mode and async CRUD operations.
Uses aiosqlite for async access. ``init_db`` sets critical PRAGMAs
(busy_timeout, journal_mode, synchronous) *before* creating any tables so
that concurrent download workers never hit ``SQLITE_BUSY``. WAL mode is
preferred on local filesystems; DELETE mode is used automatically when a
network filesystem (CIFS, NFS) is detected.
(busy_timeout, WAL, synchronous) *before* creating any tables so that
concurrent download workers never hit ``SQLITE_BUSY``.
"""
from __future__ import annotations
@ -92,30 +90,43 @@ async def init_db(db_path: str) -> aiosqlite.Connection:
PRAGMA order matters:
1. ``busy_timeout`` prevents immediate ``SQLITE_BUSY`` on lock contention
2. ``journal_mode`` WAL for local filesystems, DELETE for network mounts
(CIFS/NFS lack the shared-memory primitives WAL requires)
3. ``synchronous=NORMAL`` safe durability level
2. ``journal_mode=WAL`` enables concurrent readers + single writer
(falls back to DELETE on filesystems that lack shared-memory support,
e.g. CIFS/SMB mounts)
3. ``synchronous=NORMAL`` safe durability level for WAL mode
Returns the ready-to-use connection.
"""
# Detect network filesystem *before* opening the DB so we never attempt
# WAL on CIFS/NFS (which creates broken SHM files that persist).
use_wal = not _is_network_filesystem(db_path)
db = await aiosqlite.connect(db_path)
db.row_factory = aiosqlite.Row
# --- PRAGMAs (before any DDL) ---
await db.execute("PRAGMA busy_timeout = 5000")
if use_wal:
journal_mode = await _try_journal_mode(db, "wal")
else:
logger.info(
"Network filesystem detected for %s — using DELETE journal mode",
db_path,
# Attempt WAL mode; if the underlying filesystem doesn't support the
# shared-memory primitives WAL requires (CIFS, NFS, some FUSE mounts),
# the PRAGMA silently stays on the previous mode or returns an error.
# In that case, fall back to DELETE mode which works everywhere.
try:
result = await db.execute("PRAGMA journal_mode = WAL")
row = await result.fetchone()
journal_mode = row[0] if row else "unknown"
except Exception:
journal_mode = "error"
if journal_mode != "wal":
logger.warning(
"WAL mode unavailable (got %s) — falling back to DELETE mode "
"(network/CIFS filesystem?)",
journal_mode,
)
journal_mode = await _try_journal_mode(db, "delete")
try:
result = await db.execute("PRAGMA journal_mode = DELETE")
row = await result.fetchone()
journal_mode = row[0] if row else "unknown"
except Exception:
logger.warning("Failed to set DELETE journal mode, continuing with default")
journal_mode = "default"
logger.info("journal_mode set to %s", journal_mode)
@ -129,54 +140,6 @@ async def init_db(db_path: str) -> aiosqlite.Connection:
return db
def _is_network_filesystem(db_path: str) -> bool:
"""Return True if *db_path* resides on a network filesystem (CIFS, NFS, etc.).
Parses ``/proc/mounts`` (Linux) to find the filesystem type of the
longest-prefix mount matching the database directory. Returns False
on non-Linux hosts or if detection fails.
"""
import os
network_fs_types = {"cifs", "nfs", "nfs4", "smb", "smbfs", "9p", "fuse.sshfs"}
try:
db_dir = os.path.dirname(os.path.abspath(db_path))
with open("/proc/mounts", "r") as f:
mounts = f.readlines()
best_match = ""
best_fstype = ""
for line in mounts:
parts = line.split()
if len(parts) < 3:
continue
mountpoint, fstype = parts[1], parts[2]
if db_dir.startswith(mountpoint) and len(mountpoint) > len(best_match):
best_match = mountpoint
best_fstype = fstype
is_net = best_fstype in network_fs_types
if is_net:
logger.info(
"Detected %s filesystem at %s for database %s",
best_fstype, best_match, db_path,
)
return is_net
except Exception:
return False
async def _try_journal_mode(
db: aiosqlite.Connection, mode: str,
) -> str:
"""Try setting *mode* and return the actual journal mode string."""
try:
result = await db.execute(f"PRAGMA journal_mode = {mode}")
row = await result.fetchone()
return (row[0] if row else "unknown").lower()
except Exception as exc:
logger.warning("PRAGMA journal_mode=%s failed: %s", mode, exc)
return "error"
# ---------------------------------------------------------------------------
# CRUD helpers
# ---------------------------------------------------------------------------