ops: consistent rotated db backups + cron, consecutive-api-error staff alert

This commit is contained in:
Oleksandr Kozachuk
2026-07-13 18:25:01 +02:00
parent d0819c2683
commit 4166520923
7 changed files with 237 additions and 2 deletions
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Consistent, rotated bot.db backups (SPEC-012 OPS-13).
Run from cron on each host. Uses the sqlite3 online-backup API so the
snapshot is consistent even while the bot writes (WAL-safe), gzips it,
and keeps the newest N. Stdlib only.
Usage: python3 backup_db.py <bot.db> <backup-dir> [keep]
"""
import gzip
import os
import shutil
import sqlite3
import sys
import tempfile
import time
from pathlib import Path
DEFAULT_KEEP = 14
BACKUP_GLOB = "bot-*.db.gz"
def snapshot(src: Path, dest_gz: Path) -> None:
"""Write a consistent gzipped snapshot of src to dest_gz (OPS-13)."""
fd, tmp_path = tempfile.mkstemp(suffix=".db", dir=str(dest_gz.parent))
os.close(fd)
tmp = Path(tmp_path)
try:
source = sqlite3.connect(str(src))
try:
target = sqlite3.connect(str(tmp))
try:
source.backup(target) # atomic, WAL-safe online backup
finally:
target.close()
finally:
source.close()
with open(tmp, "rb") as raw, gzip.open(str(dest_gz), "wb") as gz:
shutil.copyfileobj(raw, gz)
os.chmod(dest_gz, 0o600) # conversation data
finally:
tmp.unlink(missing_ok=True)
def victims(existing: list, keep: int) -> list:
"""Given backup paths (any order), return the ones to delete, oldest first (OPS-14)."""
ordered = sorted(existing) # timestamped names sort chronologically
return ordered[: max(0, len(ordered) - keep)]
def rotate(backup_dir: Path, keep: int) -> int:
removed = 0
for path in victims(list(backup_dir.glob(BACKUP_GLOB)), keep):
Path(path).unlink(missing_ok=True)
removed += 1
return removed
def main() -> int:
if len(sys.argv) < 3:
print("usage: backup_db.py <bot.db> <backup-dir> [keep]", file=sys.stderr)
return 2
src = Path(sys.argv[1]).expanduser()
backup_dir = Path(sys.argv[2]).expanduser()
keep = int(sys.argv[3]) if len(sys.argv) > 3 else DEFAULT_KEEP
if not src.exists():
print(f"backup: source {src} missing", file=sys.stderr)
return 1
backup_dir.mkdir(parents=True, exist_ok=True)
stamp = time.strftime("%Y%m%d-%H%M%S", time.gmtime())
dest = backup_dir / f"bot-{stamp}.db.gz"
snapshot(src, dest)
removed = rotate(backup_dir, keep)
print(f"backup: wrote {dest.name} ({dest.stat().st_size} bytes), rotated {removed} old")
return 0
if __name__ == "__main__":
sys.exit(main())