"""`refresh_all` under concurrency: every step replaces its whole table, so two refreshes must not interleave. The advisory lock makes the second one wait instead of racing (and instead of being skipped — `POST /rules/apply` during a sync has to take effect).""" from __future__ import annotations import asyncio from datetime import timedelta from sqlalchemy import func, select from factories import make_account, make_cbr_rate, make_txn from fintracker.analytics import today_local from fintracker.db import get_sessionmaker from fintracker.metrics.refresh import refresh_all from fintracker.models import MetricNetWorthDaily async def _refresh(trigger: str): async with get_sessionmaker()() as session: return await refresh_all(session, trigger=trigger) async def test_two_concurrent_refreshes_both_succeed(app): t = today_local() card = await make_account(name="Карта", balance="10000") await make_account(name="Валютный", currency="USD", balance="100") await make_cbr_rate(t - timedelta(days=1), "USD", "90") for day in range(1, 6): await make_txn(t - timedelta(days=day), outcome="100", outcome_account_id=card) first, second = await asyncio.gather(_refresh("sync:a"), _refresh("rules")) assert first.error is None, first.error assert second.error is None, second.error # serialised, not interleaved: one refresh finished before the other started assert first.finished_at is not None and second.finished_at is not None assert first.finished_at <= second.started_at or second.finished_at <= first.started_at async with get_sessionmaker()() as session: rows = ( await session.execute(select(func.count()).select_from(MetricNetWorthDaily)) ).scalar_one() days = ( await session.execute(select(func.count(func.distinct(MetricNetWorthDaily.d)))) ).scalar_one() # one row per day of the series, written exactly once assert rows == days == 6