Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Data migration to schedule the daily-tier reconciliation pass.

The 15-minute aggregation reads a narrow source window, which cannot repair
gaps left by cron downtime. This runs the same task once a day at a wider
window to backfill them.
"""

from django.db import migrations

RECONCILE_TASK_NAME = "dashboard_metrics_reconcile_source_window"


def create_reconciliation_task(apps, schema_editor):
"""Create the once-daily reconciliation periodic task."""
crontab_model = apps.get_model("django_celery_beat", "CrontabSchedule")
periodic_task_model = apps.get_model("django_celery_beat", "PeriodicTask")

# 4:00 AM UTC — clear of the 2:00 and 3:00 cleanup tasks
schedule_4am, _ = crontab_model.objects.get_or_create(
minute="0",
hour="4",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [Lens 6, 8] — the reconciliation pass silently no-ops for the day whenever it loses the lock

This row reuses the same task name, so it contends for the single AGGREGATION_LOCK_KEY. The 15-minute row is an IntervalSchedule(every=15, period="minutes") (0002_setup_periodic_tasks.py:20-21), which drifts against this fixed 04:00 crontab.

On collision the run returns {"success": True, "skipped": True, "reason": "lock_held"} and is never retried. At roughly 138 s of work per 15-minute slot that is a materially recurring loss of the only repair mechanism for the narrowed window — and it is reported as success.

Suggested fix: give the reconciliation its own lock key, or retry on lock_held.

day_of_week="*",
day_of_month="*",
month_of_year="*",
defaults={"timezone": "UTC"},
)

periodic_task_model.objects.update_or_create(
name=RECONCILE_TASK_NAME,
defaults={
"task": "dashboard_metrics.aggregate_from_sources",
"crontab": schedule_4am,
"queue": "dashboard_metric_events",
"kwargs": '{"source_window_days": 7}',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [Lens 5, 11] — this kwarg cannot reach the task on the PG scheduler path

kwargs='{"source_window_days": 7}' on task path dashboard_metrics.aggregate_from_sources. Tracing it:

  1. mirror_pg_periodic_tasks mirrors every non-pipeline Beat periodic — _EXCLUDED_TASK_PATHS covers only the two pipeline paths and celery.backend_cleanup, so this row is mirrored.
  2. entrypoint.sh:71 runs converge_pg_scheduler --periodics on every backend start, which adopts mirrored rows.
  3. pg_scheduler.py:348 dispatches kwargs=dict(row.task_kwargs or {}).
  4. workers/scheduler/dashboard_metrics_tasks.py:108 is dashboard_metrics_aggregate()no parametersTypeError every tick. autoretry_for=(DatabaseError, OperationalError) does not cover TypeError.

Widening the worker signature alone is not enough: internal_views.py:97 forwards nothing, so the window would silently fall back to the 2-day default.

Net: once PG_SCHEDULER_ADOPT_PERIODICS is on, the reconciliation pass does not work — and the 2-day window's entire safety argument rests on it.

Also: tests/test_pg_periodic_task_declarations.py:20 pins _BEAT_MIGRATION to 0002, so the Beat/PG drift guard never looks at this migration and stays green with a fourth schedule on one side only.

"enabled": True,
"description": (
"Re-aggregate metrics over a 7 day source window to repair "
"daily-tier gaps left by cron downtime"
),
},
)


def remove_reconciliation_task(apps, schema_editor):
"""Remove the reconciliation periodic task on rollback."""
periodic_task_model = apps.get_model("django_celery_beat", "PeriodicTask")
periodic_task_model.objects.filter(name=RECONCILE_TASK_NAME).delete()


class Migration(migrations.Migration):
dependencies = [
("dashboard_metrics", "0004_pg_periodic_tasks"),
("django_celery_beat", "0018_improve_crontab_helptext"),
]

operations = [
migrations.RunPython(
create_reconciliation_task,
remove_reconciliation_task,
),
]
Loading