Skip to content

fix(va-apple-music-url-remediation): make the cooperative live-DJ pause real - #2013

Open
jakebromberg wants to merge 3 commits into
mainfrom
bugfix/issue-2009
Open

fix(va-apple-music-url-remediation): make the cooperative live-DJ pause real#2013
jakebromberg wants to merge 3 commits into
mainfrom
bugfix/issue-2009

Conversation

@jakebromberg

Copy link
Copy Markdown
Member

Summary

Three defects in jobs/va-apple-music-url-remediation's cooperative live-DJ pause, all in the same small piece of plumbing:

  1. The pause was a no-op. CheckLiveActivityFn is a detector ((lookbackSeconds) => Promise<boolean>), not a sleeper — sleeping is the caller's job. Both phase call sites did if (opts.pauseMs > 0) await opts.checkLive(opts.lookbackSeconds, opts.pauseMs), passing a second argument the function doesn't accept and discarding the boolean it returned. The job ran the probe SELECT, threw away the answer, and proceeded regardless. These two call sites were also the job's two TS2554 compile errors — the job did not compile, and shipped anyway because npm run typecheck covers apps/**/shared/**/@wxyc/database but not jobs/**, and the job's tsup --minify build is esbuild (transpile-only, no typecheck).
  2. A probe throw killed the run with no summary and no resume cursor. checkLive was awaited outside both phases' try/catch, so a transient RDS blip on the probe SELECT propagated out of runRemediation unhandled — the run died mid-page with no summary log line, losing both last_id cursors.
  3. The flowsheet phase probed once per row, not once per page. The (discarded) call sat inside the per-row loop, so a full VA_REMEDIATION_BATCH_SIZE = 2000 page cost up to 2000 extra round-trips ahead of every LML lookup. Invisible only because defect 1 meant the result was discarded anyway.

Fix

Ports waitForQuietPeriod + a fail-open safeProbe from the streaming-url-remediation / flowsheet-ghost-row-sweep donors, verbatim in shape: built once in runRemediation, shared by both phases, probed once per page before that page loads. A throwing probe is now logged, captureErrord, and treated as no-activity rather than escaping the phase.

Decision recorded in the README (option (a), make the pause real, over (b) delete the plumbing): both sibling one-shot jobs already implement a real pause, the README and CLAUDE.md workspace-table row already promised the behavior, and the job's next production run issues ~206 UPDATEs against album_metadata during hours DJs may be live.

Compile visibility (the third tsc error that turned out not to exist)

Per the issue, a naive npx tsc --noEmit -p jobs/va-apple-music-url-remediation in a fresh worktree also surfaces a TS2322 on lml-fetch.ts's caller: 'va-apple-music-url-remediation'. This is not a real registration gap. 'va-apple-music-url-remediation' is already registered as a class-5 caller in shared/lml-client/src/policy.ts (ALL_LML_CALLERS + CALLER_CLASS), added in the same commit that introduced this job (0789d05b). The TS2322 is a fresh-worktree artifact: @wxyc/lml-client's dist/index.d.ts doesn't exist until shared/lml-client is built (the lint:prebuild step CI runs before the real Type check step), and without it TS falls back to a degraded resolution of the module that manifests as a bogus "not assignable" error on the caller literal rather than the "cannot find module" errors that show up on every other unbuilt import in the same raw run. After npm run build --workspace=@wxyc/lml-client (what lint:prebuild does), only the two genuine TS2554s remain. Independently confirmed via the existing CI guard built for exactly this invariant: node scripts/check-lml-caller-classification.mjs passes today, unmodified. No change to policy.ts in this PR, and no runtime behavior change — this caller has run under its class-5 policy (X-Caller-Budget-Ms header, LML_CLASS5_TIMEOUT_MS-derived budget; the job's own explicit timeoutMs override always wins over the class default) since the job first merged.

Compile-visibility fix (the real defect)

npm run typecheck intentionally does not cover jobs/** (fleet-wide, ~20 jobs, out of scope to widen here). Added narrow coverage instead:

  • "typecheck": "tsc --noEmit" in this job's package.json (mirrors the existing-but-CI-unwired pattern in jobs/artist-identity-etl).
  • A dedicated Type check: va-apple-music-url-remediation (BS#2009) step in .github/workflows/test.yml's lint-and-typecheck job, right after the root Type check step (whose lint:prebuild already builds this job's @wxyc/database/@wxyc/lml-client deps).

Verified the new step actually gates: reintroduced the original two-arg checkLive call locally and confirmed npm run typecheck --workspace=jobs/va-apple-music-url-remediation fails with the original TS2554s before reverting.

Local checks (GitHub Actions is in major_outage — no CI ran; these are the actual local results)

  • npx tsc --noEmit -p jobs/va-apple-music-url-remediation — clean, no output.
  • npm run typecheck — clean (all workspaces).
  • npm run lint — 0 errors, 835 pre-existing warnings (unrelated files; none introduced by this diff).
  • npm run format:check — clean.
  • npx jest --config jest.unit.config.ts tests/unit/jobs/va-apple-music-url-remediation/orchestrate.test.ts — 26/26 passed (4 new: sleep-while-active/proceed-when-quiet, once-per-page not once-per-row, throwing-probe doesn't abort + summary carries both cursors, lookback=0 disables the probe entirely).
  • Full job suite (tests/unit/jobs/va-apple-music-url-remediation) — 78/78 passed.
  • npm run test:unit — 410/410 suites, 6501/6501 tests passed (the four suites flagged as known Node-26-timer flake in this environment — album-plays-refresh, cdc-websocket, sse-metrics, lml.client — passed clean this run).
  • The other lint-and-typecheck guard scripts (lint:migrations, cross-cache-identity flags, precondition guards, legacy_entry_id writes, LML caller classification, bulk-update+ANALYZE pairing, auth-tables doc) all pass.

Rebase hazard

Unmerged PR #2008 (bugfix/album-metadata-any-array) rewrites invalidateAlbumBatch and the album-phase page SELECT in this same orchestrate.ts. This PR does not touch invalidateAlbumBatch; the album-phase SELECT is touched only to move the (now real) pause probe ahead of it. Expect a rebase against whichever of #2008/this PR merges second.

What the issue got wrong

The TS2322 "unregistered caller" claim above — see "Compile visibility" section. The caller was already correctly registered; only the fresh-worktree build artifact made it look otherwise.

Closes #2009

…se real, fail-open on probe errors, and per-page not per-row

Three defects in the same small piece of plumbing (BS#2009). CheckLiveActivityFn is a detector, not a sleeper, but both phase call sites awaited it and discarded the boolean while passing a pauseMs argument the function doesn't accept — the pause has never actually paused, and the job did not compile (two TS2554s). A probe throw escaped both phases' try/catch, killing the run before it could emit its summary log line and losing both resume cursors. The flowsheet phase also probed once per row instead of once per page, which was invisible only because the discarded result meant nothing happened anyway.

Ports waitForQuietPeriod + a fail-open safeProbe from the streaming-url-remediation / flowsheet-ghost-row-sweep donors, built once in runRemediation and shared by both phases so the probe is issued once per page load in each. A throwing probe is now logged, captured, and treated as no-activity rather than propagating.

Adds a narrow per-job typecheck (package.json script + a dedicated CI step) since npm run typecheck does not cover jobs/** and this job's tsup build is transpile-only, which is how it shipped non-compiling in the first place. Scoped to this job rather than widening typecheck to all of jobs/**, which would surface an unrelated pile of errors across ~20 other jobs.

README records why option (a) — a real pause — was chosen over deleting the plumbing: both sibling one-shot jobs already implement it, the docs already promised it, and the job's next run writes to album_metadata during hours DJs may be live.
…ion, and stop pauseMs<=0 from spinning the probe

Review of PR #2013 found two gaps. First, the "sleeps while active, proceeds when quiet" test never actually exercised a sleep: the outer beforeEach pins LIVE_ACTIVITY_PAUSE_MS='0' and the test never overrode it, so stopAwareSleep(0) was a no-op, and its >=2 call-count assertion passed on pure per-page probing alone with no looping at all. Replacing the whole loop with a single discarded probe call left the suite green. Fixed by giving that test a real nonzero pause, asserting on elapsed wall-clock time (the one thing a mutation that deletes the loop cannot fake without also being far slower than a genuine no-op), and pinning the exact call count for the fixture rather than a loose lower bound.

Second, LIVE_ACTIVITY_PAUSE_MS=0 is a legal, unremarkable value (requireNonNegativeInt allows it, and nothing before this pause existed made it dangerous), but stopAwareSleep(0) returns without awaiting a timer, so a probe that keeps reporting activity degenerates into an unthrottled hot loop against RDS for the run's entire duration instead of a cooperative pause. Gate on pauseMs<=0 the same way as lookbackSeconds<=0, with a bounded regression test (the mock caps itself after 50 calls so a future regression fails the assertion instead of hanging the suite).

Both fixes verified by mutation: reintroduced each defect in isolation, confirmed the relevant test(s) go red, and reverted byte-identical. Also fixed a latent, pre-existing test-isolation leak surfaced by the new precise call-count assertions: one existing test over-queued a db.execute mock value that its own code path never consumes, which was silently bleeding into whatever test ran next.
@jakebromberg

Copy link
Copy Markdown
Member Author

Addressed both review findings in 56e4479.

1. Pause loop is now defended by mutation-verified tests

Applied your exact mutation (loop/sleep deleted, single discarded await safeProbe()) — confirmed it reddened, then reverted byte-identical. Two causes, both fixed:

  • The "sleeps..." test now sets a real nonzero LIVE_ACTIVITY_PAUSE_MS (the outer beforeEach default of '0' had made stopAwareSleep(0) a no-op) and asserts on elapsed wall-clock time (>= 100ms against a 120ms configured pause) — the one thing your mutation can't fake without also being far slower than a no-op.
  • Replaced the loose >= 2 bound with an exact count, verified by running the fixture rather than inferring it: 4 (not the guessed 4 — it matched, but I ran it rather than trusting the arithmetic, per your ask). Confirmed strictly greater than the 3-call per-page baseline the sibling test pins.

Verified each of the 4 defects independently by mutation (apply → confirm red → revert → confirm identical + green):

  • Full loop deletion (your repro) → red on "sleeps" (elapsed=0) and the new "does not spin" test (3 calls instead of 0, since your snippet also dropped the pauseMs gate).
  • pauseMs-gate-only removal (loop intact) → red on "does not spin" only (53 calls), "sleeps" stayed green — clean separation.
  • fail-open try/catch removed → red on the throwing-probe test (unhandled rejection propagates, visible in the stack trace through safeProbe/waitForQuietPeriod/runFlowsheetPhase).
  • probe moved back inside the per-row loop → red on "sleeps" (3 instead of 4) and "probes once per page" (4 instead of 3).

2. pauseMs<=0 no longer spins

One-line fix as you specified: if (opts.lookbackSeconds <= 0 || opts.pauseMs <= 0) return false;. New test sets PAUSE_MS='0' with a probe that keeps reporting activity — bounded at 50 calls (not literally infinite) so a future regression fails the assertion cleanly instead of hanging the suite; verified that bound actually fires under the isolated mutation above (53 calls received, assertion failed as expected, no hang).

Bonus: fixed a latent test-isolation leak your review's precision surfaced

jest.clearAllMocks() clears call history but not queued mockResolvedValueOnce values. One pre-existing test ("skips the album phase when flowsheet failed") queued 3 db.execute values but its own control flow (apply rejects on page 1, breaking before page 2's fetch) only ever consumes 2 — the 3rd leaked into whichever test ran next. Invisible before because nothing asserted an exact count of anything derived from db.execute-driven page-iteration counts; my new precise checkLive call-count assertions made it visible for the first time (full-file run showed 5 calls where isolated -t runs of the same test showed the correct 4). Fixed by trimming the queue to match actual consumption. Confirmed stable across repeated full-file runs.

Local checks (still no CI — major_outage continues)

  • npx tsc --noEmit -p jobs/va-apple-music-url-remediation — clean.
  • npm run typecheck / npm run typecheck --workspace=jobs/va-apple-music-url-remediation — clean.
  • npx eslint on both changed files — 0 errors, 4 pre-existing warnings (unchanged from before this push).
  • npx prettier --check on both changed files — clean.
  • Full job test file — 27/27 (was 26; +1 for the new pauseMs<=0 regression test).
  • npm run test:unit — 410/410 suites, 6502/6502 tests.
  • check-lml-caller-classification.mjs / check-bulk-update-analyze.mjs --strict / check-auth-tables-doc.mjs — all PASS.
  • Pushed through the husky pre-push hook (typecheck, lint, format:check, migration/doc guards) without --no-verify.

… knob, and bound the lookback=0 regression test

Two nits from the second review pass. LIVE_ACTIVITY_PAUSE_MS=0 became a second way to disable this job's pause but nothing said so: the shared docs/env-vars.md entry describes only the sleep duration (correctly — the other jobs reusing that var have no such gate), and this job's README still named only LIVE_ACTIVITY_LOOKBACK_SECONDS=0. Added the job-specific caveat to both docs, and softened the orchestrate.ts comment's inaccurate "is a documented way" to point at where the semantics actually live instead of asserting they already did.

The 'skips the probe entirely when the lookback is 0' test used an unconditionally-active probe; mutating out just the lookbackSeconds<=0 clause (leaving pauseMs<=0 intact) hung the suite to its timeout instead of failing, asymmetric with the sibling pauseMs<=0 test that was deliberately capped for exactly this reason. Gave it the same bounded mock. Verified: the isolated mutation now fails in ~1.5s instead of timing out.
@jakebromberg

Copy link
Copy Markdown
Member Author

Both nits folded into 36f2fca.

A. Documented the second disable knob

LIVE_ACTIVITY_PAUSE_MS=0 disabling the pause is specific to this job (the shared docs/env-vars.md entry that other jobs reuse correctly documents no such value, since they have no pauseMs<=0 gate). Rather than editing that shared entry — which would misdescribe every other job reusing the var — added a job-specific caveat to this job's docs/env-vars.md section explaining both the semantics and why they diverge from the shared entry. Updated the README's pause section to name both knobs. Softened the orchestrate.ts comment's "is a documented way" (which was circular — it wasn't documented anywhere at the time it was written) to point at where the semantics now actually live.

B. Bounded the lookback=0 test

Same treatment as the pauseMs<=0 test: capped the probe mock at 50 calls. Verified by isolating the exact mutation you found (drop the lookbackSeconds <= 0 clause, keep pauseMs <= 0) — before, this would have hung to the suite's timeout; now it fails in ~1.5s with Expected number of calls: 0, Received number of calls: 53. Reverted byte-identical after.

Local checks

  • npx tsc --noEmit -p jobs/va-apple-music-url-remediation / job-workspace typecheck — clean.
  • npx eslint on both changed TS files — 0 errors, same 4 pre-existing warnings.
  • npx prettier --check on all four changed files — clean.
  • Full job test file — 27/27.
  • npm run test:unit — 410/410 suites, 6502/6502 tests.
  • Pushed through the pre-push hook (typecheck, lint, format:check, doc/migration guards) without --no-verify.

No other files touched. Still on bugfix/issue-2009, not merged.

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.

va-apple-music-url-remediation: the cooperative live-DJ pause never pauses, and a probe throw kills the run's summary

1 participant