feat(worker): ручной пересчёт метрик через очередь, диагностика шагов и heartbeat

POST /metrics/refresh ставит задачу в sync_job (source=METRICS_JOB) и отвечает 202, пересчёт делает worker; GET /metrics/status отдаёт refreshing и consistent. metric_refresh_log хранит failed_step и step_timings. Источники с needs="tinvest_token" не попадают в расписание без токена, tinvest/moex добавлены в default_schedule. Воркер трогает heartbeat-файл для healthcheck.
This commit is contained in:
Dmitry
2026-09-19 21:54:30 +03:00
parent 989a780f9f
commit 3b3ee4d682
11 changed files with 341 additions and 30 deletions
+12 -4
View File
@@ -1,8 +1,8 @@
"""Rebuild every metric_* table after data changed (plan §3).
Order (each step is a function taking an AsyncSession and committing its own tables):
fx -> classify -> net worth -> cash flow -> spending -> runway -> data quality
Phase 2 inserts prices/lots/valuation/holdings/returns between fx and net worth.
Each step is a function taking an AsyncSession and committing its own tables. The steps and
their order live in `analytics.register_steps`, with the reason for each dependency — this
module only runs whatever is registered, in registration order.
`refresh_all` is what the worker calls after a sync that reported `changed=True`, what the
CLI `fintracker metrics refresh` runs, and what `POST /metrics/refresh` queues.
@@ -16,6 +16,7 @@ connection serialises them: a refresh that arrives during a sync WAITS and then
from __future__ import annotations
import logging
import time
import traceback
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
@@ -52,15 +53,22 @@ async def refresh_all(session: AsyncSession, trigger: str) -> MetricRefreshLog:
entry = MetricRefreshLog(trigger=trigger)
session.add(entry)
await session.commit()
timings: dict[str, float] = {}
current: str | None = None
try:
for name, step in STEPS:
current = name
log.info("metrics: %s", name)
started = time.monotonic()
await step(session)
await session.commit()
timings[name] = round(time.monotonic() - started, 3)
except Exception as exc:
await session.rollback()
entry.error = f"{type(exc).__name__}: {exc}\n{traceback.format_exc()[-4000:]}"
log.exception("metrics refresh failed at step")
entry.failed_step = current
log.exception("metrics refresh failed at step %s", current)
entry.step_timings = timings
entry.finished_at = datetime.now(UTC)
await session.commit()
return entry