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:
ayurishchevandClaude Sonnet 5 committed 2026-09-21 09:56:43 +03:00
1 parent 46d56654d8
commit 0155fd3f38
13 files changed
+226 -32

No files matched your search

+25 -3
View File
@@ -108,6 +108,19 @@ def verify_token(x_api_key: Optional[str] = Header(None)):
raise HTTPException(status_code=401, detail="Invalid or missing X-API-Key.")
def kinds_of(address_type):
"""Типы данных в базе для значения параметра type."""
return {"asn" if t == AddressType.cidr else "fqdn"
for t in (AddressType.cidr, AddressType.fqdn) if address_type in (t, AddressType.all_types)}
def ensure_data_ready(conn, kinds):
"""503, пока база пересоздана после порчи и данные не собраны заново: пустой список ввёл бы потребителя в заблуждение."""
if db.recreated_pending(conn, kinds):
raise HTTPException(status_code=503, headers={"Retry-After": "300"},
detail="The database was recreated after corruption; data is being collected again.")
def get_cidrs() -> List[str]:
with db.session() as conn:
return db.get_values(conn, "asn")
@@ -133,6 +146,7 @@ def get_addresses(
):
# Курсор читаем до данных: изменения между чтением курсора и данных повторятся в diff, что безвредно
with db.session() as conn:
ensure_data_ready(conn, kinds_of(type))
headers = {"X-Changes-Cursor": str(db.journal_head(conn))}
results = set()
@@ -175,9 +189,9 @@ def get_addresses_diff(
ip_version: IPVersion = Query(IPVersion.all_versions, description="Filter by IP version"),
):
cursor, since_ts = parse_since(since)
kinds = {"asn" if t == AddressType.cidr else "fqdn"
for t in (AddressType.cidr, AddressType.fqdn) if type in (t, AddressType.all_types)}
kinds = kinds_of(type)
with db.session() as conn:
ensure_data_ready(conn, kinds)
changes = db.get_changes(conn, kinds, cursor, since_ts)
if changes is None:
raise HTTPException(status_code=410, detail="since is outside the change journal; fetch the full /addresses list")
@@ -216,13 +230,19 @@ def health():
with db.session() as conn:
counts = {"cidrs": db.count_values(conn, "asn"), "fqdn_ips": db.count_values(conn, "fqdn")}
pending = db.recreated_pending(conn, ("asn", "fqdn"))
try:
last_restore = load_json(cc.RESTORE_FILE, None) # след автовосстановления базы из копии
except StorageError:
last_restore = None
healthy = alive and not any(j.get("last_error") for j in jobs.values())
try:
recreated = load_json(cc.RECREATED_FILE, None) # база пересоздана после порчи без копий
except StorageError:
recreated = None
healthy = alive and not pending and not any(j.get("last_error") for j in jobs.values())
return {
"status": "ok" if healthy else "degraded",
"collector_alive": alive,
@@ -230,6 +250,8 @@ def health():
"jobs": jobs,
"counts": counts,
"last_restore": last_restore,
# Только время и признак ожидания (пути карантина наружу не отдаём)
"db_recreated": {"at": recreated.get("at"), "pending": pending} if recreated else None,
}