{"record":{"id":"1211e5a89a8e8459","repo":"santifer/career-ops","slug":"lock-timeout","errorCode":"LOCK_TIMEOUT","errorMessage":"Timed out waiting for follow-ups lock at ${lockDir}","messagePattern":"Timed out waiting for follow-ups lock at (.+?)","errorType":"error_code","errorClass":"SeedError","httpStatus":null,"severity":"error","filePath":"followup-seed.mjs","lineNumber":436,"sourceCode":"          // STALE only. VANISHED means the lock was absent when we looked, and\n          // by the time this line runs another acquirer may have won the mkdir\n          // and be partway through writing owner.json — deleting on that answer\n          // destroys a live lock and kills its winner with ENOENT.\n          if (lockRecoveryVerdict(lockDir, staleMs) === RECOVER_STALE) {\n            if (rmLockArtifactSync(lockDir)) continue;\n            // rm hit contention: another process is touching the stale lock at\n            // this instant — back off instead of treating the collision as fatal.\n          }\n        } finally {\n          rmLockArtifactSync(recoverGuardDir);\n        }\n      }\n\n      await sleep(backoffMs());\n    }\n  }\n\n  throw new SeedError('LOCK_TIMEOUT', `Timed out waiting for follow-ups lock at ${lockDir}`);\n}\n\n// --- Atomic write (mirrors writeFileAtomic in tracker.mjs / merge-tracker.mjs) --\n\nfunction writeFileAtomic(filePath, content) {\n  const tmpPath = join(dirname(filePath), `.${basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);\n  try {\n    writeFileSync(tmpPath, content);\n    renameSyncWithRetry(tmpPath, filePath);\n  } catch (err) {\n    rmSync(tmpPath, { force: true });\n    throw err;\n  }\n}\n\nfunction appendPins(existingContent, pinLines) {\n  const joined = pinLines.join('\\n');\n  if (existingContent == null) {","sourceCodeStart":418,"sourceCodeEnd":454,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/followup-seed.mjs#L418-L454","documentation":"acquireFollowupsLock uses a lock directory with retry/backoff to serialize concurrent mutations of data/follow-ups.md. When it cannot acquire the lock within the timeout window, it throws a SeedError with code LOCK_TIMEOUT. This protects the follow-ups file from concurrent-corruption during writes by seedFollowup or other tooling.","triggerScenarios":"Another process holds the follow-ups lock past the timeout budget; a crashed run left a stale lock directory behind; many seedFollowup invocations run in parallel and each retries until the overall deadline expires.","commonSituations":"Batch mode fanning out several seedFollowup calls concurrently; a prior run was killed (Ctrl-C or OOM) without releasing the lock, leaving a stale lockdir; an extremely slow filesystem making lock create/check exceed backoff windows.","solutions":["Wait briefly and re-run the command — the competing process may finish and release the lock.","Check whether another career-ops process (seed, merge, tracker write) is still running and let it finish or stop it.","If no other process is running, remove the stale lock directory referenced in the message and re-run.","Avoid parallel seedFollowup invocations; run seeding sequentially (locks exist precisely because concurrent writes are unsafe)."],"exampleFix":"// before: parallel fan-out causes lock contention\nawait Promise.all(rows.map(r => seedFollowup(r.num)));\n// after: sequential seeding avoids timeout\nfor (const r of rows) {\n  await seedFollowup(r.num);\n}","handlingStrategy":"retry","validationCode":"import { existsSync } from 'node:fs';\nconst lockDir = join(process.cwd(), 'data', '.followups.lock');\nif (existsSync(lockDir)) {\n  const ageMs = Date.now() - (await stat(lockDir)).mtimeMs;\n  if (ageMs < 60_000) await new Promise(r => setTimeout(r, Math.min(60_000 - ageMs, 5000)));\n}","typeGuard":null,"tryCatchPattern":"try {\n  await seedFollowup(appNum);\n} catch (e) {\n  if (e.code === 'LOCK_TIMEOUT') {\n    console.error('Follow-ups lock busy; retrying once after a delay...');\n    await new Promise(r => setTimeout(r, 5000));\n    await seedFollowup(appNum);\n  } else throw e;\n}","preventionTips":["Run follow-up seeding sequentially, never in Promise.all fan-outs.","Ensure the process releases the lock on exit (avoid SIGKILL; handle SIGINT cleanup).","Clean stale lock directories after crashed runs before re-running.","Keep the lock directory on a local filesystem, not a slow network mount."],"tags":["concurrency","locking","timeout","filesystem"],"backgroundTag":"request-timeout","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}