{"record":{"id":"0589f9732d34e5ab","repo":"ruvnet/ruflo","slug":"storage-is-locked-by-another-process-holder-lo","errorCode":null,"errorMessage":"Storage is locked by another process (holder: ${lockData.holder}, age: ${age}ms). Lock file: ${this.lockPath}","messagePattern":"Storage is locked by another process \\(holder: (.+?), age: (.+?)ms\\)\\. Lock file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/guidance/src/persistence.ts","lineNumber":221,"sourceCode":"    };\n  }\n\n  /**\n   * Acquire a file-based lock for concurrent access prevention.\n   * Throws if the lock is already held by another process.\n   */\n  async acquireLock(): Promise<void> {\n    await this.ensureDirectory();\n\n    // Check for stale locks\n    if (existsSync(this.lockPath)) {\n      try {\n        const lockContent = await readFile(this.lockPath, 'utf-8');\n        const lockData = JSON.parse(lockContent);\n        const age = Date.now() - lockData.timestamp;\n\n        if (age < LOCK_STALE_MS) {\n          throw new Error(\n            `Storage is locked by another process (holder: ${lockData.holder}, age: ${age}ms). ` +\n            `Lock file: ${this.lockPath}`\n          );\n        }\n        // Stale lock, remove it\n        await unlink(this.lockPath);\n      } catch (err) {\n        if (err instanceof Error && err.message.startsWith('Storage is locked')) {\n          throw err;\n        }\n        // Corrupted lock file, remove it\n        try { await unlink(this.lockPath); } catch { /* ignore */ }\n      }\n    }\n\n    const holder = randomUUID();\n    const lockData = { holder, timestamp: Date.now(), pid: process.pid };\n    await writeFile(this.lockPath, JSON.stringify(lockData), 'utf-8');","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/guidance/src/persistence.ts#L203-L239","documentation":"EventStore.acquireLock() implements single-writer file locking: if the lock file exists, parses as JSON, and is younger than LOCK_STALE_MS (30 seconds), it throws with the recorded holder and age. Locks older than 30s are treated as stale and deleted, and unparseable/corrupted lock files are also removed — so this error means a genuinely recent lock: another process is writing, or a crashed process died less than 30s ago.","triggerScenarios":"Two processes using the same storage directory calling save()/append() concurrently; the previous writer crashed moments ago and left its lock file; a long-running write still holding the lock while a second one starts.","commonSituations":"Multiple workers/CLI invocations pointed at one storagePath; orchestrators restarting a crashed job immediately; tests running in parallel against shared fixture storage.","solutions":["Serialize all writes to a storage path through a single owner process","Retry acquireLock() with backoff — locks older than 30s self-heal via the stale check","If you can verify the holder process is dead (holder PID), manually remove the lock file; otherwise wait out the stale window","Give each concurrent process its own storagePath so no contention exists"],"exampleFix":"// before\nawait store.acquireLock(); // throws if another writer holds it\n// after\nasync function acquireWithRetry(store: EventStore, tries = 5, delayMs = 2_000) {\n  for (let i = 0; i < tries; i++) {\n    try { return await store.acquireLock(); }\n    catch (err) {\n      if (i === tries - 1 || !String(err.message).startsWith('Storage is locked')) throw err;\n      await new Promise(r => setTimeout(r, delayMs));\n    }\n  }\n}","handlingStrategy":"retry","validationCode":"import { existsSync, readFileSync } from 'node:fs';\n\nfunction lockAgeMs(lockPath: string): number | null {\n  if (!existsSync(lockPath)) return null; // free\n  try {\n    const { timestamp } = JSON.parse(readFileSync(lockPath, 'utf-8'));\n    return Date.now() - timestamp;\n  } catch {\n    return 0; // corrupted: acquireLock will clear it\n  }\n}\nconst age = lockAgeMs(store['lockPath'] ?? lockPath);\nif (age !== null && age < 30_000) {\n  // another writer is active; wait or route to the owning process\n}","typeGuard":null,"tryCatchPattern":"async function withLock<T>(store: EventStore, fn: () => Promise<T>): Promise<T> {\n  const maxTries = 10;\n  for (let i = 0; ; i++) {\n    try {\n      await store.acquireLock();\n      break;\n    } catch (err) {\n      const msg = err instanceof Error ? err.message : '';\n      if (!msg.startsWith('Storage is locked') || i === maxTries - 1) throw err;\n      await new Promise(r => setTimeout(r, Math.min(1000 * 2 ** i, 30_000)));\n    }\n  }\n  try { return await fn(); } finally { await store.releaseLock(); }\n}","preventionTips":["Serialize writers: one owning process per storage directory","Always pair acquireLock() with releaseLock() in a finally block","Locks self-heal after 30s (LOCK_STALE_MS); only remove a lock file manually after confirming the holder is dead"],"tags":["guidance","persistence","file-lock","concurrency","storage"],"backgroundTag":"file-lock-contention","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}