Stage B of review fixes: daemon state lock, image build check, RIPEstat retries

Job state is changed under one RLock, the Dockerfile copies all root
modules and imports them at build time, RIPEstat requests go through a
retrying session with the sourceapp parameter (ripestat_sourceapp) and a
capped Retry-After. Adds the summary and marks review findings 5-10 fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5 committed 2026-09-21 10:12:48 +03:00
1 parent d6b69d0842
commit aaf41adc79
8 files changed
+164 -28

No files matched your search

+41 -7
View File
@@ -7,6 +7,9 @@ import logging
import socket
import sys
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import db
from storage import StorageError, file_lock, load_json, save_json_atomic
@@ -25,6 +28,10 @@ BACKUP_DIR = os.environ.get("RIPE_BACKUP_DIR", os.path.join(DATA_DIR, "backups")
RESTORE_FILE = os.path.join(DATA_DIR, "last_restore.json") # след автовосстановления базы (читает /health)
RECREATED_FILE = os.path.join(DATA_DIR, "db_recreated.json") # база пересоздана после порчи без копий (см. db.recreated_pending)
BASE_URL = "https://stat.ripe.net/data/announced-prefixes/data.json"
DEFAULT_SOURCEAPP = "ripe-cidr-collector" # RIPEstat просит указывать приложение (ключ ripestat_sourceapp в config.json)
RIPESTAT_TIMEOUT = 10 # секунд на одну попытку
RIPESTAT_RETRIES = 3 # повторов при сбоях соединения и кодах 429/5xx
MAX_RETRY_AFTER = 30 # потолок паузы по заголовку Retry-After, секунд: сборщик не должен зависать на часы
DEFAULT_TTL_DAYS = 90
DEFAULT_BACKUP_KEEP = 7 # сколько копий базы хранить
@@ -98,6 +105,24 @@ def pop_collection_requests():
return [t for t in types if t in COLLECT_TYPES]
class _BoundedRetry(Retry):
"""Retry, у которого пауза по Retry-After ограничена MAX_RETRY_AFTER."""
def get_retry_after(self, response):
value = super().get_retry_after(response)
return None if value is None else min(value, MAX_RETRY_AFTER)
def make_session():
"""Сессия для RIPEstat: повторы с нарастающей паузой при сбоях соединения и кодах 429/500/502/503/504."""
retry = _BoundedRetry(total=RIPESTAT_RETRIES, backoff_factor=1, status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=("GET",))
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))
return session
def _is_global(address):
"""Публичный адрес (не loopback, не частный, не 0.0.0.0, не link-local и т. п.)."""
try:
@@ -111,6 +136,8 @@ class CIDRCollector:
self.config = load_full_config()
self.asns = self.config.get("asns", [])
self.ttl_days = self.config.get("ttl_days", DEFAULT_TTL_DAYS)
self.sourceapp = self.config.get("ripestat_sourceapp", DEFAULT_SOURCEAPP)
self.session = None # создаётся при первом запросе, закрывается в конце сбора
def add_asn(self, asn):
added, self.asns = add_to_config_list("asns", asn)
@@ -124,9 +151,11 @@ class CIDRCollector:
print("Current ASNs:", self.asns)
def fetch_prefixes(self, asn):
params = {'resource': f'AS{asn}'}
params = {'resource': f'AS{asn}', 'sourceapp': self.sourceapp}
try:
response = requests.get(BASE_URL, params=params, timeout=10)
if self.session is None:
self.session = make_session()
response = self.session.get(BASE_URL, params=params, timeout=RIPESTAT_TIMEOUT)
response.raise_for_status()
data = response.json()
@@ -144,11 +173,16 @@ class CIDRCollector:
logger.info("Starting ASN CIDR collection...")
# Сетевые запросы выполняем до транзакции, чтобы держать блокировку записи минимально
fetched = {}
for asn in self.asns:
logger.info("Processing AS%s...", asn)
prefixes = self.fetch_prefixes(asn)
if prefixes is not None:
fetched[str(asn)] = set(prefixes)
try:
for asn in self.asns:
logger.info("Processing AS%s...", asn)
prefixes = self.fetch_prefixes(asn)
if prefixes is not None:
fetched[str(asn)] = set(prefixes)
finally:
if self.session is not None:
self.session.close()
self.session = None
now = datetime.datetime.now()
with db.session() as conn, db.transaction(conn):