Bound confluence replay so Unraid HTTP stays alive.
CI / Test (push) Canceled after 0s
CI / Build and push (push) Canceled after 0s

Replay of 80 as-of days per symbol was a sync CPU loop; boot and symbol-page reads ran it on the main thread, so login and every tRPC call hit the 8s timeout. Cap wall time, yield every as-of, and stop filling the learning ledger on boot.
This commit is contained in:
Investor Flow Build
2026-09-09 10:29:18 -04:00
parent 0b901fc190
commit a79331fe9a
4 changed files with 42 additions and 17 deletions
@@ -148,6 +148,19 @@ describe('runConfluenceReplay', () => {
assert.equal(second.evaluationsStored, 0);
assert.equal(confluenceRepo.countEvaluations('PLTR', repoFullRack()), first.evaluationsStored / 3);
});
it('stops when maxMs elapses instead of locking the event loop', async () => {
const t0 = Date.now();
const cut = await runConfluenceReplay(db, cache as unknown as CacheRepository, {
symbols: ['PLTR'],
lookbackDays: 30,
budgetDaysPerSymbol: 30,
symbolsPerTick: 1,
maxMs: 1,
});
assert.ok(Date.now() - t0 < 2000, 'maxMs must bound wall time');
assert.equal(cut.complete, false);
});
});
function repoFullRack(): string {
+19 -7
View File
@@ -216,6 +216,8 @@ export interface ReplayOptions {
lookbackDays?: number;
budgetDaysPerSymbol?: number;
symbolsPerTick?: number;
/** Stop after this many ms (checked per as-of). HTTP stays responsive. */
maxMs?: number;
/** When true, do not apply live reliability weights (historical ledger stays raw). */
unweighted?: boolean;
}
@@ -276,8 +278,15 @@ export async function runConfluenceReplay(
complete: true,
};
const deadline = opts.maxMs != null ? Date.now() + opts.maxMs : null;
const yieldLoop = () => new Promise<void>((r) => setImmediate(r));
let symbolsUsed = 0;
for (const symbol of unique) {
symbolLoop: for (const symbol of unique) {
if (deadline != null && Date.now() >= deadline) {
summary.complete = false;
break;
}
if (symbolsUsed >= symbolCap) {
summary.complete = false;
break;
@@ -301,12 +310,12 @@ export async function runConfluenceReplay(
const weeklyFull = await provider.resolveAsOf(symbol, '1wk', '9999-12-31');
const benchFull = await provider.resolveAsOf(BENCHMARK_SYMBOL, '1d', '9999-12-31');
let asOfN = 0;
for (const asOf of slice) {
asOfN += 1;
if (asOfN % 4 === 0) {
await new Promise<void>((r) => setImmediate(r));
if (deadline != null && Date.now() >= deadline) {
summary.complete = false;
break symbolLoop;
}
await yieldLoop();
const dailyCut = daily.candles.filter((c) => (c.ts ?? '').slice(0, 10) <= asOf);
const weeklyCut = weeklyFull.candles.filter((c) => (c.ts ?? '').slice(0, 10) <= asOf);
const benchCut = benchFull.candles.filter((c) => (c.ts ?? '').slice(0, 10) <= asOf);
@@ -383,10 +392,13 @@ export async function fillLearningLedger(
while (Date.now() - started < maxMs) {
const status = await learningLedgerStatus(db, cache);
if (status.ready) return { ...last, complete: true, ledger: status };
const remaining = maxMs - (Date.now() - started);
if (remaining <= 0) break;
const batch = await runConfluenceReplay(db, cache, {
symbols: CONFLUENCE_LEARNING_UNIVERSE,
symbolsPerTick: 12,
budgetDaysPerSymbol: 80,
symbolsPerTick: 2,
budgetDaysPerSymbol: 15,
maxMs: remaining,
});
last = {
symbolsTouched: [...new Set([...last.symbolsTouched, ...batch.symbolsTouched])],