Skip to content

Analytics TimescaleDB lane fails intermittently on a concurrent continuous-aggregate refresh #587

Description

@abdulrafey1

Summary

The Analytics TimescaleDB Lane CI job fails intermittently with a LockNotAvailableError while resetting state between tests. The failure is timing-dependent and unrelated to the code under test — it lands on whichever PR happens to lose the race, and a re-run with no change goes green.

sqlalchemy.exc.DBAPIError: (sqlalchemy.dialects.postgresql.asyncpg.Error)
<class 'asyncpg.exceptions.LockNotAvailableError'>: could not refresh continuous
aggregate "login_activity_daily" due to a concurrent refresh
DETAIL:  A concurrent refresh on window [4714-11-24 00:00:00+00 BC, infinity) is already in progress.
[SQL: CALL refresh_continuous_aggregate(CAST($1 AS regclass), NULL, NULL)]
[parameters: ('login_activity_daily',)]

It surfaces as an ERROR during fixture setup, so the affected test never runs (2 passed, 1 error).

Cause

Two refreshes of the same continuous aggregate overlap inside a single CI job.

  1. sparkth/migrations/analytics/versions/287f281e6558_create_login_activity_daily_cagg.py:52-57 registers a background refresh policy when the aggregate is created:

    SELECT add_continuous_aggregate_policy(
        'login_activity_daily',
        start_offset      => INTERVAL '3 days',
        end_offset        => INTERVAL '1 hour',
        schedule_interval => INTERVAL '1 hour'
    )

    TimescaleDB's background worker runs the policy's first pass shortly after registration, not an hour later. The pg fixtures apply these migrations to the target database, so the policy is registered on every CI run.

  2. tests/analytics/pg/conftest.py:96-97 resets state before each test by truncating raw_events and then full-refreshing every aggregate via _refresh_all_caggs (conftest.py:47-51), which issues refresh_continuous_aggregate(..., NULL, NULL) in AUTOCOMMIT. A full refresh is required here because continuous aggregates cannot be isolated by transaction rollback — this is the documented reason the pg lane exists in the form it does.

When the background policy pass and the fixture's full refresh overlap, the latter cannot take the lock and raises. The window in the error message — [-infinity, infinity) — identifies it as the fixture's full refresh, colliding with the policy's trailing-window one.

Each CI job gets its own timescaledb service container (.github/workflows/ci.yml:75-93), so this is entirely self-inflicted within one run; concurrent PRs do not interfere with each other.

Evidence that it is timing, not code

Why it matters

  • Red builds on PRs that cannot have caused them, which trains reviewers to ignore this lane.
  • The failure is an ERROR in setup, so test_pg_only_counts_login_events is silently not executed on affected runs — the lane reports failure, but had it been a flake in the other direction it would report success with reduced coverage.
  • It affects every PR in the repo, since the lane runs on all non-draft PRs.

This description was written with the assistance of an LLM (Claude).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions