Skip to content

feat(jobs/challenges): Phase 1 challenge processors (poll-based)#835

Open
raymondjacobson wants to merge 1 commit into
api/parity-jobsfrom
api/challenges-phase-1
Open

feat(jobs/challenges): Phase 1 challenge processors (poll-based)#835
raymondjacobson wants to merge 1 commit into
api/parity-jobsfrom
api/challenges-phase-1

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

Summary

Stacked on #834 (api/parity-jobs).

Implements the discovery-provider "challenges" reward system in api/, replacing apps' Redis event-bus design with a poll-based reconciliation model (Option C from the earlier architecture discussion). Each Phase 1 challenge has its own `Processor` that reads source tables on a tick and writes derived state into `user_challenges` + per-challenge state tables.

Why poll-based

  • Idempotent / restart-safe / backfill-friendly by construction. A fresh DB rebuilds full state on first scan; a crash mid-batch leaves nothing inconsistent.
  • No producer coupling. entity_manager handlers (in pkg/etl, soon-to-be-vendored) don't need to know challenges exist. Same architectural boundary as the agreed-upon Populate data from $AUDIO specific tables to mint agnostic tables #267 split.
  • No Redis dependency introduced. Drops one moving part.
  • Consistency parity with apps. Apps' challenge processing is already async (Redis-backed, rate-limited); we're just batching differently.

Phase 1 challenges (11 in 7 processor files)

ID Source Notes
`p` `users` + `follows` + `saves` + `reposts` 7 boolean steps; also updates `challenge_profile_completion`
`u` `tracks` 3 public non-stem tracks since starting_block
`fp` `playlists` Boolean — any non-deleted playlist
`v` `users.is_verified` Boolean
`e` `plays` (checkpoint-incremental) Endless streak; currently `active=false` in catalog
`p1`/`p2`/`p3` `aggregate_monthly_plays` Verified artists; 250/1k/10k; p2 gates on p1, p3 on p2
`tt`/`tut`/`tp` `track_trending_scores` / `playlist_trending_scores` Fridays UTC; idempotent same-week; rank-tiered amounts

Architecture

`api/jobs/challenges/processor.go`:
```go
type Processor interface {
ChallengeID() string
Reconcile(ctx context.Context, tx pgx.Tx) error // idempotent
}
```

`api/jobs/index_challenges.go` is the umbrella job:

  • Runs each processor in its own `pgx.Tx` (one bad processor doesn't kill the rest).
  • Scheduled via `ScheduleEvery(ctx, 30*time.Second)` in `CoreIndexer.startParityJobs`.

Common helpers in `processor.go`:

  • `LoadChallenge` — read catalog row by id.
  • `UpsertUserChallenge` — preserves `completed_at` once set; sticky `is_complete`.
  • `SpecifierFromUserID` — matches Python's `hex(user_id)[2:]`.

Migration

`ddl/migrations/0203_seed_phase_1_challenges.sql` seeds the catalog rows from `challenges.json`. `ON CONFLICT DO UPDATE` matches apps' `create_new_challenges.py` behavior so the catalog stays aligned even if rows already exist.

Out of scope (called out)

  • Trending notifications + tastemaker challenge dispatch — depend on the event-bus mechanism we explicitly skipped.
  • Send-tip / audio-matching (`b`, `s`) — Solana.
  • Mobile install (`m`), one-shot (`o`), referrals (`r`, `rv`, `rd`) — need a client-reported signals endpoint (Phase 3).
  • First weekly comment (`c`), cosign (`cs`), pinned comment (`cp`), remix contest winner (`w`), tastemaker (`t`) — Phase 2.

Test plan

12 DB-backed tests against the `test_jobs` template DB:

  • `TestTrackUpload_CompletesAt3` — 3 tracks → complete
  • `TestTrackUpload_IgnoresStemsAndUnlisted` — only public non-stem tracks count
  • `TestFirstPlaylist_CompletesOnAnyPlaylist` — boolean completion + correct amount
  • `TestProfileCompletion_PartialProgress` — 3/7 reflected accurately
  • `TestProfileCompletion_FullyComplete` — 7/7 marks complete
  • `TestProfileVerified_CompletesForVerifiedUsers` — verified-only
  • `TestListenStreak_SkippedWhenInactive` — no-op when catalog row is inactive (current default)
  • `TestListenStreak_AdvancesAcrossDays` — activated; streak increments across 16h+ gaps
  • `TestPlayCount250_RequiresVerified` — non-verified users skipped
  • `TestPlayCount1000_GatedOnPrevious` — p2 only fires after p1 complete
  • `TestTrending_IdempotentSameWeek` — Friday-only; second run no-ops
  • `TestTrending_SkipsNonFriday` — gated weekday check

All 12 pass; `go build ./...` clean; `go vet ./...` clean.

🤖 Generated with Claude Code

Adds the IndexChallengesJob + 11 challenge processors (in 7 files) that
mirror apps' discovery-provider challenges system. Unlike apps' Redis
event-bus design, processors here reconcile derived state from existing
source tables on a tick — idempotent, restart-safe, no producer-side
plumbing required (architecture decision; see processor.go package docs).

Phase 1 challenges:
  p   profile completion        — 7 boolean steps from users/follows/saves/reposts
  u   track upload              — 3 public, non-stem tracks since starting_block
  fp  first playlist            — any non-deleted playlist
  v   connect-verified          — users.is_verified = true
  e   listen streak             — endless streak from plays (currently inactive)
  p1  play count 250 milestone  — verified artists, 2025+ play sum
  p2  play count 1000 milestone — gated on p1 complete
  p3  play count 10000 milestone — gated on p2 complete
  tt  trending track            — top-10 of week, Fridays UTC, idempotent
  tut trending underground      — same, UNDERGROUND_TRACKS type
  tp  trending playlist         — top-10 playlists, rank-tiered payout

Each processor:
  * Implements challenges.Processor (ChallengeID + Reconcile).
  * Runs in its own pgx tx via the umbrella IndexChallengesJob.
  * Skips quietly when its catalog row is inactive or absent.

The umbrella job runs every 30s and is wired into CoreIndexer.Start
alongside the rest of the parity jobs in #834.

Migration 0203 seeds the Phase 1 challenges catalog rows from
challenges.json (mirrors apps' create_new_challenges with ON CONFLICT
DO UPDATE so catalog stays in sync).

Out of scope here (per the architecture discussion):
  * Trending notifications + tastemaker challenge — depend on the
    challenge-event mechanism we explicitly skipped.
  * Send-tip / audio-matching (Solana).
  * Mobile install / one-shot / referrals — need a signals endpoint
    (Phase 3).

Tests: 12 DB-backed tests (one Friday-coupled trending test
auto-skips on non-Fridays), all passing against test_jobs template DB.
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