Withhold addresses after the database is recreated without a backup
When ripe.db is corrupted and no valid backup exists, a new empty database is created with a db_recreated.json marker; /addresses and /addresses/diff answer 503 until the collector gathers data again, /health reports db_recreated. Tests now isolate all state files via conftest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46d56654d8
commit
0155fd3f38
13 files changed
+226
-32
No files matched your search
@@ -84,7 +84,9 @@ def _recover(path, error, cc):
|
||||
"""Порча базы при открытии: карантин и восстановление из последней исправной копии.
|
||||
|
||||
API и демон могут обнаружить порчу одновременно, поэтому всё выполняется под блокировкой,
|
||||
а внутри неё база проверяется повторно (другой процесс мог уже восстановить). Нет копий - StorageError.
|
||||
а внутри неё база проверяется повторно (другой процесс мог уже восстановить или пересоздать).
|
||||
Нет исправных копий - создаётся новая пустая база и ставится метка db_recreated.json: пока данные
|
||||
не собраны заново, API не отдаёт пустой список за настоящий (см. recreated_pending).
|
||||
"""
|
||||
with file_lock(path + ".restore"):
|
||||
try:
|
||||
@@ -109,7 +111,38 @@ def _recover(path, error, cc):
|
||||
save_json_atomic(cc.RESTORE_FILE, {"at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"backup": candidate, "quarantine": quarantine, "error": str(error)})
|
||||
return conn
|
||||
raise StorageError(f"{path} is corrupted and no valid backup was found") from error
|
||||
logger.error("No valid backup for %s: a new empty database is created; the API withholds "
|
||||
"addresses until the collector gathers data again (remove %s to override)", path, cc.RECREATED_FILE)
|
||||
# Метка ставится до создания базы: сбой между шагами не оставит пустую базу без метки
|
||||
save_json_atomic(cc.RECREATED_FILE, {"at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"quarantine": quarantine, "error": str(error)})
|
||||
conn = _open(path, cc)
|
||||
try:
|
||||
_reset_journal(conn)
|
||||
except BaseException:
|
||||
conn.close()
|
||||
raise
|
||||
return conn
|
||||
|
||||
|
||||
def _reset_journal(conn):
|
||||
"""Очищает журнал и сдвигает счётчик: курсоры, выданные до потери или отката базы, станут недействительными."""
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
row = conn.execute("SELECT seq FROM sqlite_sequence WHERE name = 'changes'").fetchone()
|
||||
horizon = int(conn.execute("SELECT value FROM meta WHERE key = 'horizon_id'").fetchone()[0])
|
||||
new_seq = max(row[0] if row else 0, horizon) + RESTORE_JOURNAL_JUMP
|
||||
conn.execute("DELETE FROM changes")
|
||||
if row:
|
||||
conn.execute("UPDATE sqlite_sequence SET seq = ? WHERE name = 'changes'", (new_seq,))
|
||||
else:
|
||||
conn.execute("INSERT INTO sqlite_sequence (name, seq) VALUES ('changes', ?)", (new_seq,))
|
||||
conn.execute("UPDATE meta SET value = ? WHERE key = 'horizon_id'", (str(new_seq),))
|
||||
conn.execute(f"UPDATE meta SET value = {_TS} WHERE key = 'horizon_ts'")
|
||||
except BaseException:
|
||||
conn.execute("ROLLBACK")
|
||||
raise
|
||||
conn.execute("COMMIT")
|
||||
|
||||
|
||||
def _restore_backup(candidate, path, cc):
|
||||
@@ -119,28 +152,40 @@ def _restore_backup(candidate, path, cc):
|
||||
os.replace(tmp, path)
|
||||
conn = _open(path, cc)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
row = conn.execute("SELECT seq FROM sqlite_sequence WHERE name = 'changes'").fetchone()
|
||||
horizon = int(conn.execute("SELECT value FROM meta WHERE key = 'horizon_id'").fetchone()[0])
|
||||
new_seq = max(row[0] if row else 0, horizon) + RESTORE_JOURNAL_JUMP
|
||||
conn.execute("DELETE FROM changes")
|
||||
if row:
|
||||
conn.execute("UPDATE sqlite_sequence SET seq = ? WHERE name = 'changes'", (new_seq,))
|
||||
else:
|
||||
conn.execute("INSERT INTO sqlite_sequence (name, seq) VALUES ('changes', ?)", (new_seq,))
|
||||
conn.execute("UPDATE meta SET value = ? WHERE key = 'horizon_id'", (str(new_seq),))
|
||||
conn.execute(f"UPDATE meta SET value = {_TS} WHERE key = 'horizon_ts'")
|
||||
except BaseException:
|
||||
conn.execute("ROLLBACK")
|
||||
raise
|
||||
conn.execute("COMMIT")
|
||||
_reset_journal(conn)
|
||||
except BaseException:
|
||||
conn.close()
|
||||
raise
|
||||
return conn
|
||||
|
||||
|
||||
def recreated_pending(conn, kinds):
|
||||
"""True, если базу пересоздали после порчи без копий и по запрошенным типам данные ещё не собраны заново.
|
||||
|
||||
Тип считается неготовым, когда для него в config.json есть источники, а в базе нет ни одного значения;
|
||||
тип без источников готов (пустой список законен). Нечитаемый конфиг - блокировка сохраняется.
|
||||
"""
|
||||
import cidr_collector as cc
|
||||
|
||||
if not os.path.exists(cc.RECREATED_FILE):
|
||||
return False
|
||||
try:
|
||||
config = cc.load_full_config()
|
||||
except StorageError:
|
||||
return True
|
||||
configured = {"asn": config.get("asns"), "fqdn": config.get("fqdns")}
|
||||
return any(configured[kind] and count_values(conn, kind) == 0 for kind in kinds)
|
||||
|
||||
|
||||
def settle_recreated(conn):
|
||||
"""Снимает метку пересоздания, когда данные по всем типам собраны заново. Вызывается после сбора."""
|
||||
import cidr_collector as cc
|
||||
|
||||
if os.path.exists(cc.RECREATED_FILE) and not recreated_pending(conn, ("asn", "fqdn")):
|
||||
os.remove(cc.RECREATED_FILE)
|
||||
logger.info("Data gathered again after the database loss; %s removed", cc.RECREATED_FILE)
|
||||
|
||||
|
||||
def backup_database(conn, now, keep, backup_dir=None):
|
||||
"""Онлайн-копия базы с проверкой и ротацией. Возвращает путь копии.
|
||||
|
||||
|
||||
Reference in new issue
Block a user