fix(worker): keepalive RAR ca dashboard-ul sa nu afiseze fals "RAR inaccesibil"

Dashboard-ul deduce starea RAR din vechimea ultimului login reusit (>30h ->
"indisponibil?"). Cand coada e goala, worker-ul nu are de ce sa se logheze,
deci timestamp-ul devine stale si banner-ul "Blocat: RAR inaccesibil —
declaratiile NU pleaca" apare fals, desi RAR raspunde.

Worker-ul face acum un login de proba o data pe zi (interval configurabil,
24h < pragul de 30h) cand coada e goala: pe succes reimprospateaza
last_rar_login_ok; pe esec real last_rar_login_ok ramane vechi -> dashboard
degradeaza corect. Forteaza login real (invalideaza sesiunea) ca proba sa fie
autentica. Gating: cel mult o sondare pe interval, sa nu hartuiasca RAR jos.

_keepalive_target sare conturile ale caror creds NU se decripteaza sub cheia
curenta (start.sh both genereaza cheie efemera noua la fiecare pornire ->
creds durabile vechi dau decrypt None) si cade pe creds <test> in dev.

Teste: tests/test_worker_keepalive_rar.py (6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-29 06:48:32 +00:00
parent ce90dac833
commit c05fa00007
3 changed files with 257 additions and 1 deletions

View File

@@ -99,6 +99,11 @@ class Settings(BaseSettings):
# Dev: foloseste creds <test> din settings.xml pt login worker. In productie
# creds vin per-cerere de la ROAAUTO — lasa False.
worker_use_test_creds: bool = False
# Keepalive RAR: cand coada e goala, worker-ul face un login de proba la fiecare
# atata timp ca sa pastreze last_rar_login_ok proaspat (sub pragul de 30h al
# dashboard-ului) — altfel banner-ul "RAR inaccesibil" apare fals doar din lipsa
# de trafic. 0 = dezactivat. Implicit o data pe zi (24h < 30h, margine de 6h).
worker_rar_keepalive_interval_s: int = 86400
worker_sending_lease_s: int = 120 # rand 'sending' mai vechi de atat = orfan (worker mort mid-POST)
worker_retry_base_s: int = 5 # backoff = base * 2^retry (plafonat la max)
worker_retry_max_s: int = 300

View File

@@ -34,7 +34,7 @@ import httpx
from .. import errors
from ..config import Settings, get_settings, load_test_credentials
from ..crypto import decrypt_creds
from ..db import get_connection, init_db, write_heartbeat
from ..db import get_connection, init_db, read_heartbeat, write_heartbeat
from ..observ import log_event, set_source
from ..mapping import DEFAULT_ACCOUNT_ID, upsert_nomenclator
from ..payload import build_rar_payload
@@ -428,6 +428,68 @@ def _creds_from_account(conn, account_id: int) -> dict | None:
return None
def _keepalive_target(conn, settings: Settings) -> tuple[int | None, dict | None]:
"""Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev).
Sare conturile ale caror creds NU se decripteaza sub cheia curenta — in dev
`start.sh both` genereaza o cheie efemera noua la fiecare pornire, deci creds-urile
durabile criptate sub cheia veche dau decrypt -> None. Fallback la creds <test>.
"""
rows = conn.execute(
"SELECT id, rar_creds_enc FROM accounts "
"WHERE rar_creds_enc IS NOT NULL ORDER BY id"
).fetchall()
for row in rows:
creds = decrypt_creds(row["rar_creds_enc"])
if creds and creds.get("email") and creds.get("password"):
return row["id"], creds
if settings.worker_use_test_creds:
return DEFAULT_ACCOUNT_ID, load_test_credentials()
return None, None
def _maybe_keepalive(conn, settings: Settings, sessions: "AccountSessions", state: dict) -> None:
"""Login de proba periodic cand coada e goala — verifica reachability RAR si
pastreaza last_rar_login_ok proaspat ca dashboard-ul sa nu afiseze fals
'RAR inaccesibil' doar din lipsa de trafic.
Sondeaza la cel mult o data pe interval (si pe succes, si pe esec): pe succes
heartbeat-ul se reimprospateaza singur; pe esec real (RAR jos) last_rar_login_ok
ramane vechi -> dashboard-ul degradeaza corect. Forteaza login real (invalideaza
sesiunea cache-uita) ca proba sa fie autentica, nu un token vechi din cache.
"""
interval = settings.worker_rar_keepalive_interval_s
if interval <= 0:
return
hb = read_heartbeat(conn)
last = hb["last_rar_login_ok"] if hb else None
if last:
try:
age = (datetime.now(timezone.utc) - datetime.fromisoformat(last)).total_seconds()
if age < interval:
return # login inca proaspat — nimic de facut
except (ValueError, TypeError):
pass
now_ts = time.time()
if now_ts - state["last_attempt"] < interval:
return # deja am incercat recent (nu hartui RAR daca e jos)
state["last_attempt"] = now_ts
account_id, creds = _keepalive_target(conn, settings)
if account_id is None or not creds:
return # niciun cont cu creds durabile — nimic de sondat
sessions.invalidate(account_id) # forteaza login real, nu token din cache
try:
sessions.get_token(conn, account_id, creds) # reimprospateaza last_rar_login_ok la succes
except RarAuthError:
pass # creds invalide — deja logat in get_token (WARNING)
except Exception as exc:
# RAR indisponibil: last_rar_login_ok ramane vechi (corect). Nu propaga.
log_event("rar_keepalive", nivel="WARNING", account_id=account_id,
mesaj=f"keepalive RAR esuat (cont {account_id}): {type(exc).__name__}",
context={"rezultat": "esuat"}, conn=conn, sursa="worker")
def run() -> int:
signal.signal(signal.SIGTERM, _stop)
signal.signal(signal.SIGINT, _stop)
@@ -440,6 +502,7 @@ def run() -> int:
sessions = AccountSessions(settings)
_last_purge_time: float = 0.0
_keepalive_state = {"last_attempt": 0.0}
while _running:
try:
@@ -466,6 +529,9 @@ def run() -> int:
# Nimic de trimis: recupereaza orfanii conturilor deja logate.
for acct, rar, tok in sessions.active():
recover_orphans(conn, settings, rar, tok, account_id=acct)
# Login de proba periodic ca dashboard-ul sa nu afiseze fals
# "RAR inaccesibil" din lipsa de trafic (vezi _maybe_keepalive).
_maybe_keepalive(conn, settings, sessions, _keepalive_state)
time.sleep(settings.worker_poll_interval_s)
continue