Add database backups and automatic restore
Daemon job "backup" (online copy, quick_check, rotation), restore of the newest valid copy when the database cannot be opened, change journal reset after restore, last_restore in /health, docs and tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c99665542a
commit
02f7b49e12
10 files changed
+294
-21
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import datetime
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
@@ -15,6 +17,8 @@ import db
|
||||
def files(tmp_path, monkeypatch):
|
||||
for attr, name in (("DB_FILE", "ripe.db"), ("DATA_FILE", "data.json"), ("FQDN_DATA_FILE", "fqdn_data.json")):
|
||||
monkeypatch.setattr(cc, attr, str(tmp_path / name))
|
||||
monkeypatch.setattr(cc, "BACKUP_DIR", str(tmp_path / "backups"))
|
||||
monkeypatch.setattr(cc, "RESTORE_FILE", str(tmp_path / "last_restore.json"))
|
||||
return tmp_path
|
||||
|
||||
|
||||
@@ -76,3 +80,50 @@ def test_change_journal(files):
|
||||
assert db.get_changes(conn, {"asn"}, cursor=0) is None
|
||||
assert db.get_changes(conn, {"asn"}, cursor=c3 + 1) is None
|
||||
assert db.get_changes(conn, {"asn"}, cursor=c3) == (set(), set(), c3)
|
||||
|
||||
|
||||
def test_backup_rotation_and_integrity(files):
|
||||
now = datetime.datetime.now()
|
||||
with db.session() as conn:
|
||||
with db.transaction(conn):
|
||||
db.merge_source(conn, "asn", "1", {"a"}, now, 90)
|
||||
for minutes in range(3):
|
||||
db.backup_database(conn, now + datetime.timedelta(minutes=minutes), keep=2)
|
||||
|
||||
# Ротация оставляет две самые свежие копии; копия самодостаточна и содержит данные
|
||||
backups = db.list_backups(cc.BACKUP_DIR)
|
||||
assert len(backups) == 2 and not glob.glob(os.path.join(cc.BACKUP_DIR, "*.tmp"))
|
||||
assert backups[0].endswith(f"{(now + datetime.timedelta(minutes=2)).astimezone(datetime.timezone.utc):%Y%m%dT%H%M%SZ}.db")
|
||||
assert sqlite3.connect(backups[0]).execute("SELECT COUNT(*) FROM addresses").fetchone()[0] == 1
|
||||
|
||||
|
||||
def test_restore_from_backup(files):
|
||||
now = datetime.datetime.now()
|
||||
with db.session() as conn:
|
||||
with db.transaction(conn):
|
||||
db.merge_source(conn, "asn", "1", {"a", "b"}, now, 90)
|
||||
old_cursor = db.get_changes(conn, {"asn"}, cursor=0)[2]
|
||||
db.backup_database(conn, now, keep=3)
|
||||
|
||||
# «Свежая» копия с мусором пропускается; живая база портится
|
||||
(files / "backups" / "ripe-99999999T999999Z.db").write_bytes(b"garbage" * 100)
|
||||
(files / "ripe.db").write_bytes(b"garbage" * 1000)
|
||||
|
||||
with db.session() as conn:
|
||||
assert sorted(db.get_values(conn, "asn")) == ["a", "b"]
|
||||
# Курсор, выданный до отката, недействителен (клиент сделает полную выгрузку); новый работает
|
||||
assert db.get_changes(conn, {"asn"}, cursor=old_cursor) is None
|
||||
head = db.journal_head(conn)
|
||||
assert db.get_changes(conn, {"asn"}, cursor=head) == (set(), set(), head)
|
||||
with db.transaction(conn):
|
||||
db.merge_source(conn, "asn", "1", {"a", "b", "c"}, now, 90)
|
||||
assert db.get_changes(conn, {"asn"}, cursor=head)[:2] == ({"c"}, set())
|
||||
assert len(glob.glob(str(files / "ripe.db.corrupt-*"))) == 1
|
||||
assert json.loads((files / "last_restore.json").read_text())["backup"].endswith(".db")
|
||||
|
||||
# Без исправных копий - прежнее поведение: ошибка хранилища, база в карантине
|
||||
for backup in db.list_backups(cc.BACKUP_DIR):
|
||||
os.unlink(backup)
|
||||
(files / "ripe.db").write_bytes(b"garbage" * 1000)
|
||||
with pytest.raises(db.StorageError):
|
||||
db.connect()
|
||||
Reference in new issue
Block a user