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:
1 parent
d6b69d0842
commit
aaf41adc79
8 files changed
+164
-28
No files matched your search
+41
-7
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user