commit f1c7ce7cb731529f167df84235ac047aac130362
parent e2424587e9bf1b79b822207f673837268942fc5e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 13:49:42 -0400
common: sharedAutoQueueState caches the in-flight read — one state object per process, even at boot
Boot starts all four lane runLoops without awaiting them, so every lane
reached the read before any had set singleton.state and each kept a private
copy of the whole file; on 2026-09-25 13:33:37 another lane's persist of
its boot-time copy undid the download lane's 13:20 backoff and deferral.
Cache the promise keyed by the state file; never cache a failure. Exported
for the regression tests (same object, mutation visible, write seam).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 158 insertions(+), 7 deletions(-)
diff --git a/common/controller/autoRunner.test.ts b/common/controller/autoRunner.test.ts
@@ -11,6 +11,7 @@ import {
pickMetadataScanChannel,
priorityContextFor,
resetPriorityContextForTest,
+ sharedAutoQueueState,
} from "./autoRunner";
import {
defaultChannelPriority,
@@ -20,7 +21,11 @@ import {
type FocusSummary,
} from "../lib/channelPriority";
import { LANES } from "../lib/autoQueueTypes";
-import { emptyAutoQueueState } from "../jobs/autoQueueState";
+import {
+ emptyAutoQueueState,
+ readAutoQueueState,
+ writeAutoQueueState,
+} from "../jobs/autoQueueState";
import type {
AutoQueueGroup,
AutoQueuePolicy,
@@ -543,3 +548,103 @@ test("an injected auto-queue state changes nothing about the answer", async () =
// will hand to the next lane.
assert.deepEqual(state[LANES[0]].runtime.currentWeights, { "prio-all": 17 });
});
+
+// ONE STATE OBJECT PER PROCESS, EVEN AT BOOT (release 8, slice S).
+//
+// Boot starts all four lanes' runLoops without awaiting them, so they all ask
+// `sharedAutoQueueState` inside one read's await gap. When only the resolved
+// value was cached each got its own copy, and on 2026-09-25 a lane persisting
+// its boot-time copy of the WHOLE file erased the download lane's 13:20 backoff
+// (`fails: 13` back to 12) and its video deferral. These pin the cache to the
+// in-flight read: same object, mutations visible before any write, and a write
+// through either caller carries the other's change to disk.
+
+function freshSingletonForTest(): void {
+ globalThis.__yttAutoRunner__ = undefined;
+}
+
+function stateTempPaths(): Paths {
+ const dir = mkdtempSync(path.join(tmpdir(), "shared-aq-state-"));
+ mkdirSync(path.join(dir, ".auto-queue"), { recursive: true });
+ const file = path.join(dir, ".auto-queue", "state.json");
+ // The boot-time content from the incident: a YouTube cooldown at fails 12.
+ const seeded = emptyAutoQueueState();
+ seeded.download.platformBackoff.youtube = { fails: 12, until: 1_000 };
+ writeFileSync(file, JSON.stringify(seeded));
+ return { autoQueueStateFile: file } as Paths;
+}
+
+test("sharedAutoQueueState: concurrent callers (the boot fan-out) get ONE object", async () => {
+ freshSingletonForTest();
+ const paths = stateTempPaths();
+ const [a, b, c, d] = await Promise.all(
+ LANES.map(() => sharedAutoQueueState(paths)),
+ );
+ assert.equal(a, b);
+ assert.equal(a, c);
+ assert.equal(a, d);
+ assert.equal(a.download.platformBackoff.youtube?.fails, 12);
+ // And a caller after the read resolved gets the same one too.
+ assert.equal(await sharedAutoQueueState(paths), a);
+ freshSingletonForTest();
+});
+
+test("sharedAutoQueueState: a mutation through one caller is visible to the other before any write", async () => {
+ freshSingletonForTest();
+ const paths = stateTempPaths();
+ const pa = sharedAutoQueueState(paths);
+ const pb = sharedAutoQueueState(paths);
+ const a = await pa;
+ a.download.videoDeferrals["_60iFE_FBPQ"] = {
+ until: 99_000,
+ channelSlug: "some-channel",
+ };
+ a.download.platformBackoff.youtube = { fails: 13, until: 2_000 };
+ const b = await pb;
+ assert.equal(b.download.platformBackoff.youtube?.fails, 13);
+ assert.ok(b.download.videoDeferrals["_60iFE_FBPQ"]);
+ freshSingletonForTest();
+});
+
+test("sharedAutoQueueState: another lane's persist carries the download lane's backoff to disk", async () => {
+ freshSingletonForTest();
+ const paths = stateTempPaths();
+ // Obtained concurrently, as boot does.
+ const [downloadLane, otherLane] = await Promise.all([
+ sharedAutoQueueState(paths),
+ sharedAutoQueueState(paths),
+ ]);
+ // 13:20 — the download lane escalates the cooldown and persists.
+ downloadLane.download.platformBackoff.youtube = { fails: 13, until: 2_000 };
+ await writeAutoQueueState(paths, downloadLane);
+ // 13:33:37 — a different lane persists ITS reference to the state.
+ otherLane.digest.runtime.currentWeights = { someLeaf: 1 };
+ await writeAutoQueueState(paths, otherLane);
+ const onDisk = await readAutoQueueState(paths);
+ assert.deepEqual(onDisk.download.platformBackoff.youtube, {
+ fails: 13,
+ until: 2_000,
+ });
+ assert.deepEqual(onDisk.digest.runtime.currentWeights, { someLeaf: 1 });
+ freshSingletonForTest();
+});
+
+test("sharedAutoQueueState: a reset singleton reads afresh; a different state file is its own object", async () => {
+ freshSingletonForTest();
+ const p1 = stateTempPaths();
+ const p2 = stateTempPaths();
+ const first = await sharedAutoQueueState(p1);
+ freshSingletonForTest();
+ const afterReset = await sharedAutoQueueState(p1);
+ assert.notEqual(afterReset, first);
+ // A read for another file in flight at the same time does not hand either
+ // caller the other file's object, and the later file owns the singleton.
+ freshSingletonForTest();
+ const [x, y] = await Promise.all([
+ sharedAutoQueueState(p1),
+ sharedAutoQueueState(p2),
+ ]);
+ assert.notEqual(x, y);
+ assert.equal(await sharedAutoQueueState(p2), y);
+ freshSingletonForTest();
+});
diff --git a/common/controller/autoRunner.ts b/common/controller/autoRunner.ts
@@ -247,6 +247,10 @@ type AutoRunnerSingleton = {
// See sharedAutoQueueState.
state: AutoQueueState | null;
stateFile: string | null;
+ // The READ in flight, so every caller in the same window awaits ONE read and
+ // receives ONE object. See sharedAutoQueueState.
+ loading: Promise<AutoQueueState> | null;
+ loadingFile: string | null;
};
declare global {
@@ -260,6 +264,8 @@ function getSingleton(): AutoRunnerSingleton {
runners: new Map(),
state: null,
stateFile: null,
+ loading: null,
+ loadingFile: null,
};
}
return globalThis.__yttAutoRunner__;
@@ -281,15 +287,55 @@ function getSingleton(): AutoRunnerSingleton {
// lifetime is the auto-runner singleton's, which the e2e harness already clears
// between specs (api/test/invalidate-cache) — so a reset corpus does not
// inherit a previous spec's picks.
-async function sharedAutoQueueState(paths: Paths): Promise<AutoQueueState> {
+//
+// THE CACHE IS THE PROMISE, NOT THE VALUE — and until release 8 it was the
+// value, which made the paragraph above false at exactly the moment it matters.
+// Boot (`startAutoRunnersIfEnabled`) starts all four lanes' `runLoop`s without
+// awaiting them, so all four reached the `await readAutoQueueState` here before
+// any of them had set `singleton.state`; each read its own copy and each
+// runLoop kept that private copy for its lifetime. Live, 2026-09-25: the
+// download lane persisted `fails: 13`, a later `until` and a 6 h video deferral
+// at 13:20; at 13:33:37 another lane persisted its boot-time copy of the whole
+// file (`fails: 12`, deferrals `{}`) and silently undid the pacing on disk.
+// Caching the in-flight read keyed by the state file means every caller in the
+// same window awaits one read and receives the same object. A reset (the e2e
+// harness dropping `__yttAutoRunner__`) replaces the singleton wholesale, so a
+// read still in flight resolves into the OLD singleton and cannot leak across.
+export async function sharedAutoQueueState(
+ paths: Paths,
+): Promise<AutoQueueState> {
const singleton = getSingleton();
- if (singleton.state && singleton.stateFile === paths.autoQueueStateFile) {
+ const file = paths.autoQueueStateFile;
+ if (singleton.state && singleton.stateFile === file) {
return singleton.state;
}
- const state = await readAutoQueueState(paths);
- singleton.state = state;
- singleton.stateFile = paths.autoQueueStateFile;
- return state;
+ if (singleton.loading && singleton.loadingFile === file) {
+ return singleton.loading;
+ }
+ const loading = readAutoQueueState(paths).then(
+ (state) => {
+ // A later call for a DIFFERENT file superseded this read; it owns the
+ // singleton now, and this read's object goes only to its own awaiters.
+ if (singleton.loading === loading) {
+ singleton.state = state;
+ singleton.stateFile = file;
+ singleton.loading = null;
+ singleton.loadingFile = null;
+ }
+ return state;
+ },
+ (err: unknown) => {
+ // Never cache a failure: the next caller retries the read.
+ if (singleton.loading === loading) {
+ singleton.loading = null;
+ singleton.loadingFile = null;
+ }
+ throw err;
+ },
+ );
+ singleton.loading = loading;
+ singleton.loadingFile = file;
+ return loading;
}
export type AutoRunnerStatus = {