UN-3973 [DEV] Cut dashboard cron DB time by deriving monthly metrics from the daily tier - #2255
Conversation
…dow to 2 days The dashboard aggregation widened its DAY-granularity query to the first of the previous month so monthly buckets could be summed in Python from the same rows. Every run re-read 32-62 days of source data per metric, per org, 96 times a day. Monthly is now rolled up from event_metrics_daily in one statement for all orgs, so the source queries only need the daily window. That window drops to 2 days, sized against the measured worst created_at -> terminal-status lag of ~2h. A once-daily 7-day pass reruns the same task at a wider bound to repair gaps left by cron downtime. The active-org prefilter is decoupled from the daily window and pinned at 7 days: metrics filtered on another column (hitl_completions on approved_at) can land for an org whose executions are older than the source window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc
|
| Filename | Overview |
|---|---|
| backend/dashboard_metrics/migrations/0005_add_reconciliation_task.py | Adds an idempotent, reversible reconciliation schedule with the corrected dependency on dashboard_metrics migration 0004. |
| backend/dashboard_metrics/tasks.py | Narrows source-query windows and replaces raw-source monthly aggregation with an atomic daily-tier rollup. |
| backend/dashboard_metrics/tests/test_tasks.py | Covers rollup correctness, reconciliation behavior, query boundaries, orphan handling, and migration scheduling. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[15-minute aggregation] --> B[Read last 2 days of source data]
C[Daily 04:00 reconciliation] --> D[Read last 7 days of source data]
B --> E[Upsert hourly and daily tiers]
D --> E
E --> F[Aggregate current and previous month from daily tier]
F --> G[Atomically upsert monthly rows and remove obsolete keys]
Reviews (11): Last reviewed commit: "Merge remote-tracking branch 'origin/UN-..." | Re-trigger Greptile
Sonar: - S117: rename apps.get_model() locals in 0004 to snake_case - S3776: cut _run_aggregation cognitive complexity from 22 by hoisting the static metric config tables to module level and extracting the per-org body, the active-org prefilter and the result shape into helpers Greptile: - Monthly rows in the rebuilt window whose daily rows are gone are now deleted alongside the upsert, so the two tiers cannot disagree. An empty daily tier still short-circuits, so a wiped tier cannot cascade into deleting monthly history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc
|
@greptile-apps please review |
…iation schedule Closes the acceptance criteria that had no automated check: - the monthly rollup issues no source-table SQL, asserted by capturing the queries it actually sends - the window ladder at 2 / 7 / 62 days, including a row that finishes after the narrow window has moved past its created_at and so never re-enters it - the reconciliation schedule row, its idempotency and its reverse The schedule tests call the migration's function directly. The suite runs with --no-migrations, so data migrations never execute and asserting on the beat row would fail regardless of the migration being correct. Also moves the dotenv load in settings/base.py above the Celery block. CELERY_BROKER_BASE_URL, _USER and _PASS were read above it, so they could not be supplied by an env file at all and had to be ambient. Ambient values still take precedence, so deployed behaviour is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc
|
@greptile-apps please review again |
|
@greptile-apps On the summary verdict — the mechanism is right, "not safe to merge" isn't. This is the trade the ticket asks for, and it was in the PR description before the review. It's documented and accepted. Under "Can this PR break any existing features", item 1:
Monthly stopped self-healing over 62 days deliberately — that 62-day re-derivation from source tables is the DB load UN-3973 exists to remove. Widening the rollup's source window to make orphan cleanup safe against arbitrarily old gaps would revert the PR. You did surface one thing the description understated, and I've fixed it. "Inherits gaps" reads as undercounting; the deletion case is worse than that:
Confirmed against the test DB ( The one-time exposure is real, and it's a deploy step rather than a code change. Gaps already sitting in the daily tier would propagate into monthly on the first rollup after this ships. Added a That rebuilds daily from source across the rollup window; the scheduled rollup then derives monthly from a complete tier. Ongoing gaps beyond 7 days remain the accepted trade, with |
|
You're right. Given that this tradeoff is explicit, ticket-approved, bounded to the current and previous month, and addressed operationally by the one-time daily backfill, it should not be a merge blocker. The orphan-deletion behavior is a real consequence, but it is now documented rather than an unaddressed regression. I withdraw the “not safe to merge” verdict. The remaining operational caveat is to run the documented backfill before the first rollup and use |
|
@greptile-apps can you review and generate new confidence score? |
…hange Cut the verbose comments and docstrings down to the purpose and the non-obvious bits. Code is unchanged. Restore backend/settings/base.py to main — moving the dotenv load ahead of get_required_setting was a local test convenience, not part of this change. The test rig exports the broker vars itself, so CI never needed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc
…d cut _run_aggregation's complexity Migration 0005 used update_or_create for the PG row of the schedule it was only re-keying, which reset pg_owned to False. converge_pg_scheduler disables a row's Beat twin when the PG scheduler adopts it, so on an adopted deployment the migration would have left the aggregation with no firer at all — Beat disabled, PG no longer owning it. It now updates only task_kwargs on that row, leaving enabled and pg_owned to the scheduler that owns them. Rollback is symmetric. Threading the tier through _run_aggregation took its cognitive complexity from 25 to 27 against a limit of 15. Extracted _collect_org_metrics and _aggregate_org, and hoisted the two static metric tables to module level so they are not rebuilt per call. Names match the same extraction on #2255 so the two reconcile cleanly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc
…into UN-3973-optimize-db-queries-reduce-monthly-metrics
UN-3445 landed 0004_pg_periodic_tasks on main after this branch was cut, leaving dashboard_metrics with two 0004s depending on 0003 and nothing depending on either. Django saw two leaf nodes and refused to build the graph, so `migrate` failed before applying anything — every app, not just this one. Depend on 0004_pg_periodic_tasks and renumber to match, so the short prefix form stays usable for a rollback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc
…-causing-high-DB-load' into UN-3973-optimize-db-queries-reduce-monthly-metrics
|
|
@greptile-apps migration order is revised. Review again. |



What
Monthly dashboard totals are now added up from the daily figures we already store, instead of
re-reading the raw processing history. Each run looks at the last 2 days instead of up to two
months, and a once-a-day pass looks back 7 days to repair anything the cron missed while down.
Why
Every 15 minutes, for each of 38 organisations, the cron re-read up to two months of processing
history — just so it could total the month. Monthly figures do not need that freshness, and the
daily totals are already stored.
Worth about 460 s of database time per 6 h. It will not move the two most expensive queries;
those are #2264's.
How
The reconciliation pass is the same task at a wider bound — a second schedule row, no new code path.
_aggregate_single_metric/_aggregate_llm_combineddropmonthly_start/monthly_agg; theDAY query binds
daily_startand the redundant Python guard goes._bulk_upsert_monthly→_rollup_monthly_from_daily()— one ORM aggregate overevent_metrics_dailyfor all orgs, so the monthly tier stops scaling with tenant count.metric_typeis aggregated (Min), not grouped, or a mid-month type change breaks the upsert.daily_startand pinned at 7 days.Can this PR break any existing features. If yes, please list possible items. If no, please explain why.
Monthly totals now inherit any holes in the stored daily figures. The once-a-day pass repairs the
last 7 days; older holes need the backfill command below, which is why it runs once at deploy.
Deleting old source data no longer corrects totals beyond that same window.
Nothing already counted changes, and a slow-finishing file cannot land in the wrong day.
Database Migrations
dashboard_metrics/0005_add_reconciliation_task.py— adds the once-a-day 04:00 UTC schedule row.No table changes. Safe to re-run and removable on rollback.
Run once at deploy, before the first monthly rollup:
manage.py backfill_metrics --days 60 --skip-hourly --skip-monthly. Monthly is built from the daily figures now, so those have to becomplete for this month and last month first.
Migration Order
The three UN-3883 PRs stack on the same integration branch. Merge in this order.
dashboard_metrics/0005_add_reconciliation_task0004_pg_periodic_tasks(UN-3445, already onmain)file_execution/0007_wfe_status_created_idxfile_execution/0006_…dashboard_metrics/0006_split_aggregation_schedule0005_add_reconciliation_task(#2255)#2264's migration is in a different app and has no interaction with the other two. The one
hard dependency is #2265 on #2255 — merged out of order, #2265 fails at graph build:
Its tests are unaffected — the backend suite runs with
--no-migrations.Verified on a throwaway Postgres:
0004→0005→0006applies from an empty database,reverses, and re-applies, with
makemigrations --checkclean at each step.Env Config
None.
Relevant Docs
UN-3883 analysis §6.2, §6.3, §7 step 1, §8.
Related Issues or PRs
Parent UN-3883. #2264 (UN-3972), #2265 (UN-3974) — see Migration Order above.
Dependencies Versions
None.
Notes on Testing
38 tests. Detail in Jira comment 45429.
event_metrics_daily, not source tablesScreenshots
n/a — backend only.
Checklist
I have read and understood the Contribution Guidelines.