Skip to content

perf(api): delta-based social aggregate counts + reconciliation backstop#898

Merged
raymondjacobson merged 3 commits into
mainfrom
perf/social-aggregate-delta-reconcile
Jun 2, 2026
Merged

perf(api): delta-based social aggregate counts + reconciliation backstop#898
raymondjacobson merged 3 commits into
mainfrom
perf/social-aggregate-delta-reconcile

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

@raymondjacobson raymondjacobson commented Jun 2, 2026

Summary

Replaces the per-write count(*) recounts in handle_repost / handle_save with O(1) deltas (consistency with handle_follow), and adds a periodic reconciliation job as the drift backstop for all aggregate counts.

1. Delta-based aggregate maintenance (hot path)

  • handle_repost.sql / handle_save.sql: every count(*) recount → <col> = <col> + delta.
  • Delta is now transition-aware: delta = (new active?1:0) − (UPDATE & old active?1:0), where active = NOT is_delete. Correct for upsert-in-place toggles; idempotent (delta 0) on no-op re-delivery.
  • All three triggers (handle_repost, handle_save, handle_follow) move from AFTER INSERTAFTER INSERT OR UPDATE so the entity_manager upsert path maintains counts. handle_follow gets the same transition-delta for consistency.

2. Reconciliation backstop (off hot path)

  • New jobs/reconcile_aggregates.go (ReconcileAggregatesJob), modeled on prune_plays.go. Ports the three full-recompute queries from discovery's update_aggregates.py (user / track / playlist counts + dominant_genre).
  • Scheduled every 10 min in indexer.go (matches discovery's celery cadence). Writes only the count columns — column-disjoint from the score-only AggregatesCalculator, so they run concurrently without collision.
  • Faithful 1:1 port of the SQL, with one deliberate improvement: nullable dominant_genre / dominant_genre_count comparisons use is distinct from instead of Python's !=. != returns NULL on a genre flipping to/from NULL, so such a row is silently skipped until a count also changes; is distinct from converges it. Never writes a different value — strictly catches one edge case Python misses.

3. Packaged etl dependency

Test plan

  • go build ./... passes against the bumped etl.
  • Rebased onto current main; dropped a stale copy of the per-processor-timing change already merged via obs(api): log per-processor timing in IndexChallengesJob #897 (would have double-declared slowProcessorThreshold).
  • After deploy: confirm repost/save/follow counts increment/decrement on toggle and stay correct on re-delivery.
  • Confirm ReconcileAggregatesJob logs only when it corrects drift (corrected > 0) and otherwise stays quiet.

raymondjacobson and others added 2 commits June 1, 2026 21:27
handle_repost/handle_save now maintain aggregate counts with O(1) deltas
instead of recounting with count(*) on every social write (consistency
with handle_follow). The delta is transition-aware
(active = not is_delete) so it is correct for upsert-in-place toggles and
idempotent (delta 0) on re-delivery. All three triggers move to
AFTER INSERT OR UPDATE to support the entity_manager upsert.

Adds ReconcileAggregatesJob, a low-priority drift backstop (every 10m,
matching discovery's update_aggregates celery cadence) that recomputes
the count columns from source tables. Column-disjoint from the score-only
AggregatesCalculator.

NOTE: requires the go-openaudio entity_manager social-upsert change to be
deployed FIRST. The AFTER UPDATE triggers only behave correctly once the
demote-then-insert writes are replaced by in-place upserts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Packages OpenAudio/go-openaudio#331 (merged), which switches the etl
indexer from demote-then-insert to upsert-in-place for reposts/saves/
follows/subscriptions. The delta-based aggregate triggers in this PR
require that behavior to track is_delete transitions correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@raymondjacobson raymondjacobson force-pushed the perf/social-aggregate-delta-reconcile branch from e104dda to 468212b Compare June 2, 2026 04:29
aggregate_monthly_plays was seeded in the randomized map-order phase of
Seed(), so it raced the plays fixtures. When plays seeded first, the
handle_play trigger created the aggregate_monthly_plays row and the
explicit fixture insert then collided (aggregate_monthly_plays_pkey).
Seed it in the deterministic entity phase, before plays, so the trigger
upserts the seeded row instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@raymondjacobson raymondjacobson merged commit bf124bf into main Jun 2, 2026
5 checks passed
@raymondjacobson raymondjacobson deleted the perf/social-aggregate-delta-reconcile branch June 2, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant