You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Child of #1108 — finishes Layer 1. Completes the TMDb discovery stubs (movie rows that never got a full /movie fetch) by reusing the proven #1106 unified-refresh worker, retires the no-op RepairImdbIds path, and closes the imdb_id arm of #760. After this, every movie has a tmdb_details ledger row and imdb_id reaches ~100% terminal.
1. Baseline — the residual after #1109 (prod, 2026-06-10)
never had a full /movie fetch → imdb_id was never actually checked
movies with no tmdb_details ledger row
~250,678
superset: discovery stubs (incl. the 161k) + ~89k fetched-pre-backfill
These are discovery stubs: a movie row created from a bulk/discovery payload (which does not carry imdb_id) that never received a full /movie/{id} fetch. They are genuine backlog, not source-absent.
Why nothing currently fixes them:
RepairImdbIds → TMDbDetailsWorker is a no-op — process_tmdb_movie/3 (tmdb_details_worker.ex:94) skips any movie that already exists. (Visible in prod: TMDbDetailsWorker shows ~5,194 cancelled.)
This is exactly the dynamic #760 documents (sweepers that don't actually resolve the bar).
2. The fix — complete stubs via the unified refresh worker
TMDbMovieRefreshWorker.refresh/1 works on any existing movie (verified tmdb_movie_refresh_worker.ex:37 — Repo.get + full get_movie_for_refresh + refresh_movie_from_tmdb). Unlike TMDbDetailsWorker, it refreshes rather than skips. So a sustained capped sweeper that enqueues it for movies lacking a tmdb_details ledger row will, per movie:
bootstrap a tmdb_details ledger row (+ metrics, credits, watch, imdb_id) → the movie enters the normal age-tiered refresh cycle,
— which lets us retire the no-op RepairImdbIds/ImdbIdRepairSweeper entirely.
Watch-region fan-out is cheap here: long-tail stubs almost always have no providers (watch_providers → :empty), so store_tmdb_watch_providers writes little.
3. Changes (file-by-file)
3a. New maintenance module — Cinegraph.Maintenance.CompleteTmdbStubs
Eligible set — movies with no tmdb_details ledger row, canonical-first:
frommin"movies",where: fragment("NOT EXISTS (SELECT 1 FROM data_refreshes dr WHERE dr.entity_type='movie' AND dr.entity_id=? AND dr.source='tmdb_details')",m.id),order_by: [desc: fragment("? != '{}'::jsonb",m.canonical_sources),desc: m.id],select: m.id
Enqueues TMDbMovieRefreshWorker.new(%{movie_id: id}) in chunked Oban.insert_all (the worker is uniqueness-keyed on :movie_id/1h, so re-runs collapse).
3b. New sweeper — Cinegraph.Workers.StubCompletionSweeper
lib/cinegraph/workers/stub_completion_sweeper.ex (mirror BiographyRefreshSweeper): queue: :maintenance, max_attempts: 1, priority: 3, @per_run_limit 5_000, perform → CompleteTmdbStubs.run(limit:). Sustained (drains the ~250k and keeps pace with new discovery stubs — the #760 fix).
3c. Cron — config/config.exs
Add {"0 7 * * *", Cinegraph.Workers.StubCompletionSweeper} in the slot vacated byImdbIdRepairSweeper (3e). Gating: reuses TMDbMovieRefreshWorker, so enable only once #1106's soak has proven that worker. (By the time this slice deploys, the soak — 2026-06-11 — will have passed; if implementing earlier, land the cron commented and flip it on post-soak.)
3d. Retire the no-op repair path
Remove the {"0 7 * * *", ImdbIdRepairSweeper} cron from config/config.exs.
In lib/cinegraph/admin/job_registry.ex: move:imdb_id_repair_sweeper from a scheduled entry to an on-demand entry (schedule: nil) — keeps it manually runnable, drops it from cron parity. Add the :stub_completion_sweeper scheduled entry (queue :maintenance, schedule "0 7 * * *"). Net scheduled count stays 27 (−1 +1) → parity test green.
Add a @deprecated-style moduledoc note to RepairImdbIds/ImdbIdRepairSweeper pointing at CompleteTmdbStubs (full deletion optional — keep callable for now).
3e. (no surface_area change)
imdb_id_row/0 already counts :empty markers as terminal (#1109); as stubs complete, fetched/source_absent rise and needs_fetch falls automatically. tmdb_details_row/0 already keys on tmdb_data, so its terminal stays ~100% — but the ledger coverage (which drives refresh) is what this backfills.
complete_tmdb_stubs_test.exs (new): movie with notmdb_details ledger → enqueues one TMDbMovieRefreshWorker; movie with a tmdb_details ledger → excluded; dry_run enqueues nothing, found > 0; :limit caps; canonical-first ordering.
job_registry_test.exs parity: green after the −imdb_id_repair / +stub_completion swap (count stays 27).
(Retirement) keep/adjust repair_imdb_ids_test.exs if the module stays; remove if deleted.
6. Verification (prod, read-only)
# stubs remaining (no tmdb_details ledger)importEcto.Query;aliasCinegraph.RepoRepo.one(frommin"movies",where: fragment("NOT EXISTS (SELECT 1 FROM data_refreshes dr WHERE dr.entity_type='movie' AND dr.entity_id=? AND dr.source='tmdb_details')",m.id),select: count(m.id))# → ~250,678 before; trends to ~0Cinegraph.Maintenance.CompleteTmdbStubs.run(dry_run: true)# sizes the setCinegraph.Health.SurfaceArea.report().sources|>Enum.find(&&1.source=="imdb_id")# imdb_id terminal_pct trends 86 → ~100; needs_fetch → ~0
7. Rollout & soak
Deploy (cron on only post-Unified per-movie TMDb refresh (one call, all sources) + near-term availability throughput bump #1106-soak). 2. Sweeper drains 5k/day. 3. Soak/checkpoints (multi-week):tmdb_details ledger rows climb toward ~1.16M; imdb_id terminal 86% → ~100%; needs_fetch → ~0; TMDbDetailsWorkercancelled count stops growing (repair retired). 4. Watch DB pool/box memory vs the combined sweeper load.
8. Definition of Done
Every movie has a tmdb_details ledger row (or :ineligible); the no-tmdb_details-ledger set → ~0.
imdb_id terminal ~100% (residual resolved to fetched-or-source-absent).
1. Baseline — the residual after #1109 (prod, 2026-06-10)
#1109 marked 349,324 checked-but-null movies source-absent →
imdb_idterminal 56% → 86.18%. What's left:imdb_idneeds_fetch(null, no:emptymarker)/moviefetch →imdb_idwas never actually checkedtmdb_detailsledger rowThese are discovery stubs: a movie row created from a bulk/discovery payload (which does not carry
imdb_id) that never received a full/movie/{id}fetch. They are genuine backlog, not source-absent.Why nothing currently fixes them:
RepairImdbIds → TMDbDetailsWorkeris a no-op —process_tmdb_movie/3(tmdb_details_worker.ex:94) skips any movie that already exists. (Visible in prod:TMDbDetailsWorkershows ~5,194cancelled.)Freshness.due("tmdb_details")only selects movies that already have a ledger row; stubs have none.This is exactly the dynamic #760 documents (sweepers that don't actually resolve the bar).
2. The fix — complete stubs via the unified refresh worker
TMDbMovieRefreshWorker.refresh/1works on any existing movie (verifiedtmdb_movie_refresh_worker.ex:37—Repo.get+ fullget_movie_for_refresh+refresh_movie_from_tmdb). UnlikeTMDbDetailsWorker, it refreshes rather than skips. So a sustained capped sweeper that enqueues it for movies lacking atmdb_detailsledger row will, per movie:/moviefetch →imdb_idresolves: present →:ok(fetched); null-after-real-fetch →:empty(source-absent) via the [Impl] Make the imdb_id ceiling explicit — mark source-absent in the freshness ledger so Layer 1 reads 100% terminal (child of #1108) #1109 inline touch,tmdb_detailsledger row (+ metrics, credits, watch,imdb_id) → the movie enters the normal age-tiered refresh cycle,RepairImdbIds/ImdbIdRepairSweeperentirely.Watch-region fan-out is cheap here: long-tail stubs almost always have no providers (
watch_providers→:empty), sostore_tmdb_watch_providerswrites little.3. Changes (file-by-file)
3a. New maintenance module —
Cinegraph.Maintenance.CompleteTmdbStubslib/cinegraph/maintenance/complete_tmdb_stubs.ex(mirrormaintenance/refresh_biographies.ex):tmdb_detailsledger row, canonical-first:TMDbMovieRefreshWorker.new(%{movie_id: id})in chunkedOban.insert_all(the worker is uniqueness-keyed on:movie_id/1h, so re-runs collapse).run(opts) :: {:ok, %{found, enqueued, failed, dry_run}};:limit+:dry_run.3b. New sweeper —
Cinegraph.Workers.StubCompletionSweeperlib/cinegraph/workers/stub_completion_sweeper.ex(mirrorBiographyRefreshSweeper):queue: :maintenance, max_attempts: 1, priority: 3,@per_run_limit 5_000,perform → CompleteTmdbStubs.run(limit:). Sustained (drains the ~250k and keeps pace with new discovery stubs — the #760 fix).3c. Cron —
config/config.exsAdd
{"0 7 * * *", Cinegraph.Workers.StubCompletionSweeper}in the slot vacated byImdbIdRepairSweeper(3e). Gating: reusesTMDbMovieRefreshWorker, so enable only once #1106's soak has proven that worker. (By the time this slice deploys, the soak — 2026-06-11 — will have passed; if implementing earlier, land the cron commented and flip it on post-soak.)3d. Retire the no-op repair path
{"0 7 * * *", ImdbIdRepairSweeper}cron fromconfig/config.exs.lib/cinegraph/admin/job_registry.ex: move:imdb_id_repair_sweeperfrom a scheduled entry to an on-demand entry (schedule: nil) — keeps it manually runnable, drops it from cron parity. Add the:stub_completion_sweeperscheduled entry (queue:maintenance, schedule"0 7 * * *"). Net scheduled count stays 27 (−1 +1) → parity test green.@deprecated-style moduledoc note toRepairImdbIds/ImdbIdRepairSweeperpointing atCompleteTmdbStubs(full deletion optional — keep callable for now).3e. (no surface_area change)
imdb_id_row/0already counts:emptymarkers as terminal (#1109); as stubs complete,fetched/source_absentrise andneeds_fetchfalls automatically.tmdb_details_row/0already keys ontmdb_data, so its terminal stays ~100% — but the ledger coverage (which drives refresh) is what this backfills.4. Capping & DB load
5. Tests
complete_tmdb_stubs_test.exs(new): movie with notmdb_detailsledger → enqueues oneTMDbMovieRefreshWorker; movie with atmdb_detailsledger → excluded;dry_runenqueues nothing,found > 0;:limitcaps; canonical-first ordering.job_registry_test.exsparity: green after the −imdb_id_repair / +stub_completion swap (count stays 27).repair_imdb_ids_test.exsif the module stays; remove if deleted.6. Verification (prod, read-only)
7. Rollout & soak
tmdb_detailsledger rows climb toward ~1.16M;imdb_idterminal 86% → ~100%;needs_fetch→ ~0;TMDbDetailsWorkercancelledcount stops growing (repair retired). 4. Watch DB pool/box memory vs the combined sweeper load.8. Definition of Done
tmdb_detailsledger row (or:ineligible); the no-tmdb_details-ledger set → ~0.imdb_idterminal ~100% (residual resolved to fetched-or-source-absent).ImdbIdRepairSweeperretired (off cron);CompleteTmdbStubssustained.9. Relation
imdb_idarm of Sweeper caps don't outpace catalog growth — OMDb/IMDb-id/profile_path drift bars stay flat or grow #760 (the sustained sweeper + real-fetch completion is the resolution; recommend scoping Sweeper caps don't outpace catalog growth — OMDb/IMDb-id/profile_path drift bars stay flat or grow #760 down to its remaining OMDb/profile_path arms or closing if subsumed).TMDbMovieRefreshWorker— gated on its soak (2026-06-11)./admin/homeostasisdashboard (§10c).