{"record":{"id":"e14a6eae42aaf013","repo":"actualbudget/actual","slug":"lockedmessage-timeoutms","errorCode":null,"errorMessage":"lockedMessage(timeoutMs)","messagePattern":"lockedMessage\\(timeoutMs\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/lock.ts","lineNumber":93,"sourceCode":"function sweepStaleReaders(dir: string) {\n  const readers = readersDir(dir);\n  for (const name of readReaderNames(readers)) {\n    const pid = Number(name.split('-')[0]);\n    if (!Number.isFinite(pid) || !pidIsAlive(pid)) {\n      rmSync(join(readers, name), { force: true });\n    }\n  }\n}\n\nasync function waitForReadersEmpty(dir: string, timeoutMs: number) {\n  const readers = readersDir(dir);\n  const deadline = Date.now() + timeoutMs;\n  while (Date.now() < deadline) {\n    sweepStaleReaders(dir);\n    if (readReaderNames(readers).length === 0) return;\n    await new Promise(resolve => setTimeout(resolve, READER_POLL_INTERVAL_MS));\n  }\n  throw new Error(lockedMessage(timeoutMs));\n}\n\nasync function acquireGate(\n  dir: string,\n  timeoutMs: number,\n): Promise<() => Promise<void>> {\n  ensureDir(dir);\n  try {\n    return await lockfile.lock(dir, {\n      lockfilePath: lockfilePath(dir),\n      retries: retriesForTimeout(timeoutMs),\n      stale: 30_000,\n    });\n  } catch (err) {\n    if (isLockedError(err)) throw new Error(lockedMessage(timeoutMs));\n    throw err;\n  }\n}","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/cli/src/lock.ts#L75-L111","documentation":"waitForReadersEmpty polls the readers directory waiting for all shared (read) locks to be released before an exclusive lock can proceed. If readers still hold the lock after timeoutMs, the timeout error from lockedMessage is thrown. This prevents an exclusive writer from mutating state while readers are active.","triggerScenarios":"acquireExclusive is called while other processes hold read locks that never clear within timeoutMs, e.g. crashed readers whose sweepStaleReaders hasn't aged them out, or a long-running reader exceeding the timeout window.","commonSituations":"Concurrent CLI processes: one process reads while another tries an exclusive operation; stale reader files left behind by a killed process; READER_POLL_INTERVAL_MS polling never observing an empty directory before the deadline.","solutions":["Retry the exclusive operation later, after the reader process has finished.","Increase the timeoutMs passed to acquireExclusive if readers legitimately run long.","Verify no orphaned reader files remain in the lock directory and remove stale ones manually.","Check for crashed/killed reader processes leaving stale lock entries beyond the 30s stale threshold."],"exampleFix":"// before\nawait acquireExclusive(dir, 1000);\n// after\nawait acquireExclusive(dir, 30000); // allow long-running readers to finish","handlingStrategy":"try-catch","validationCode":"import { readdir } from 'fs/promises';\nconst readers = await readdir(readersDir).catch(() => []);\nif (readers.length > 0) {\n  throw new Error(`Cannot acquire exclusive lock: ${readers.length} reader(s) active`);\n}","typeGuard":"function isLockTimeoutError(err: unknown): err is Error {\n  return err instanceof Error && /lock/i.test(err.message) && /timed out|locked/i.test(err.message);\n}","tryCatchPattern":"try {\n  await acquireExclusive(dir, timeoutMs);\n} catch (err) {\n  if (isLockTimeoutError(err)) {\n    // back off and retry later\n    await sleep(5000);\n    return acquireExclusive(dir, timeoutMs);\n  }\n  throw err;\n}","preventionTips":["Keep read-lock sections short so they finish well within the exclusive timeout.","Use a generous timeoutMs for exclusive operations.","Monitor for stale reader files and rely on sweepStaleReaders' staleness window.","Serialize operations per data directory instead of mixing readers and writers concurrently."],"tags":["locking","concurrency","timeout"],"backgroundTag":"lock-acquisition-timeout","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}