From b9ee10a83a69a7224accfbf74f1513ed688f4166 Mon Sep 17 00:00:00 2001 From: ImBenji Date: Tue, 4 Aug 2026 21:07:42 +0100 Subject: [PATCH] fix: serialize autonomy job leases --- src/autonomy/jobs.js | 10 +++++++++- workers/autonomyWorker.js | 1 + workers/calibrationWorker.js | 1 + workers/coordinatorWorker.js | 1 + workers/executionWorker.js | 1 + workers/outcomeAutonomyWorker.js | 1 + 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/autonomy/jobs.js b/src/autonomy/jobs.js index 028d619..8a5e8c4 100644 --- a/src/autonomy/jobs.js +++ b/src/autonomy/jobs.js @@ -31,7 +31,15 @@ function leaseNextJob(db, workerId, leaseSeconds = 60, jobTypes = null) { `).run(workerId, `+${Math.max(1, Math.floor(leaseSeconds))} seconds`, job.id); return updated.changes ? { ...job, status: 'leased', leased_by: workerId } : null; }); - return tx(); + try { + // Acquire the write reservation before selecting. A deferred transaction can + // otherwise read a snapshot, lose the writer race, and fail with + // SQLITE_BUSY_SNAPSHOT when it attempts the lease update. + return tx.immediate(); + } catch (error) { + if (String(error.code || '').startsWith('SQLITE_BUSY')) return null; + throw error; + } } function completeJob(db, id, workerId) { diff --git a/workers/autonomyWorker.js b/workers/autonomyWorker.js index 88509a4..e4a0b0a 100644 --- a/workers/autonomyWorker.js +++ b/workers/autonomyWorker.js @@ -71,6 +71,7 @@ async function runAutonomyWorker({ archivePath, intelligencePath, workerId = `au const archiveDb = new Database(archivePath, { readonly: true }); const intelligenceDb = new Database(intelligencePath); intelligenceDb.pragma('journal_mode = WAL'); + intelligenceDb.pragma('busy_timeout = 5000'); initAutonomySchema(intelligenceDb); while (true) { diff --git a/workers/calibrationWorker.js b/workers/calibrationWorker.js index 220fc5d..e293428 100644 --- a/workers/calibrationWorker.js +++ b/workers/calibrationWorker.js @@ -71,6 +71,7 @@ function createDecisions(db, strategyVersion = 'autonomy-1') { async function runCalibrationWorker({ intelligencePath, pollMs = 60000, workerId = `calibration-${os.hostname()}-${process.pid}` } = {}) { const db = new Database(intelligencePath); db.pragma('journal_mode = WAL'); + db.pragma('busy_timeout = 5000'); initAutonomySchema(db); while (true) { try { diff --git a/workers/coordinatorWorker.js b/workers/coordinatorWorker.js index 74de840..5737a6e 100644 --- a/workers/coordinatorWorker.js +++ b/workers/coordinatorWorker.js @@ -34,6 +34,7 @@ async function runCoordinatorWorker({ archivePath, intelligencePath, workerId = const archiveDb = new Database(archivePath, { readonly: true }); const intelligenceDb = new Database(intelligencePath); intelligenceDb.pragma('journal_mode = WAL'); + intelligenceDb.pragma('busy_timeout = 5000'); initAutonomySchema(intelligenceDb); const config = loadConfig(); while (true) { diff --git a/workers/executionWorker.js b/workers/executionWorker.js index 2e677dd..539b71c 100644 --- a/workers/executionWorker.js +++ b/workers/executionWorker.js @@ -10,6 +10,7 @@ async function runExecutionWorker({ intelligencePath, pollMs = 10000, mode = 'sh if (!['shadow', 'paper'].includes(mode)) throw new Error(`unsupported execution mode: ${mode}`); const db = new Database(intelligencePath); db.pragma('journal_mode = WAL'); + db.pragma('busy_timeout = 5000'); initAutonomySchema(db); const paperClient = mode === 'paper' ? createAlpacaPaperClient({ keyId: process.env.ALPACA_PAPER_KEY_ID, secretKey: process.env.ALPACA_PAPER_SECRET_KEY }) diff --git a/workers/outcomeAutonomyWorker.js b/workers/outcomeAutonomyWorker.js index deb0737..81727b4 100644 --- a/workers/outcomeAutonomyWorker.js +++ b/workers/outcomeAutonomyWorker.js @@ -33,6 +33,7 @@ async function history(symbol) { async function resolveAutonomyOutcomes({ intelligencePath, workerId = `outcome-${os.hostname()}-${process.pid}`, pollMs = 60000 } = {}) { const db = new Database(intelligencePath); db.pragma('journal_mode = WAL'); + db.pragma('busy_timeout = 5000'); initAutonomySchema(db); const cache = new Map(); while (true) {