{"record":{"id":"5f09d947e793502a","repo":"Yeachan-Heo/oh-my-codex","slug":"question-not-open","errorCode":"question_not_open","errorMessage":"Timed out acquiring submit lock for ${recordPath}","messagePattern":"Timed out acquiring submit lock for (.+?)","errorType":"exception","errorClass":"QuestionSubmitError","httpStatus":null,"severity":"error","filePath":"src/question/state.ts","lineNumber":277,"sourceCode":"  const ownerToken = lockOwnerToken();\n  const deadline = Date.now() + QUESTION_SUBMIT_LOCK_TIMEOUT_MS;\n  await mkdir(dirname(lockDir), { recursive: true });\n  while (true) {\n    try {\n      await mkdir(lockDir);\n      try {\n        await writeFile(ownerPath, ownerToken, 'utf8');\n      } catch (error) {\n        await rm(lockDir, { recursive: true, force: true });\n        throw error;\n      }\n      break;\n    } catch (error) {\n      const err = error as NodeJS.ErrnoException;\n      if (err.code !== 'EEXIST') throw error;\n      if (await maybeRecoverStaleQuestionLock(lockDir)) continue;\n      if (Date.now() > deadline) {\n        throw new QuestionSubmitError('question_not_open', `Timed out acquiring submit lock for ${recordPath}`);\n      }\n      await sleep(25);\n    }\n  }\n\n  try {\n    return await fn();\n  } finally {\n    try {\n      const currentOwner = await readFile(ownerPath, 'utf8');\n      if (currentOwner.trim() === ownerToken) {\n        await rm(lockDir, { recursive: true, force: true });\n      }\n    } catch {\n    }\n  }\n}\n","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/question/state.ts#L259-L295","documentation":"withQuestionSubmitLock acquires a directory-based lock (mkdir, recovering stale locks on EEXIST). If the lock dir still exists after the deadline and cannot be recovered as stale, it throws QuestionSubmitError with code question_not_open. This guards a record against concurrent submissions.","triggerScenarios":"Two callers submit/mark prompting on the same recordPath simultaneously and the loser waits past the timeout; or a crashed process left a lock directory whose mtime/owner info prevents maybeRecoverStaleQuestionLock from classifying it stale.","commonSituations":"Duplicate submit events (double-click, retry storm); a killed process leaving a stale lock the recovery heuristic (e.g. PID liveness check) won't reclaim because the PID was reused; NFS or container filesystems where stale-lock detection fails; overly long-held locks during slow writes.","solutions":["Check whether a stale lock directory remains at the lock path and remove it if its owner process is dead","Prevent duplicate submissions upstream (debounce/disable submit button while a request is in flight)","Increase the lock timeout if updates legitimately take long","If a stale-lock PID check is failing due to PID reuse, clear the lock manually or restart the offending process"],"exampleFix":"// before\nawait withQuestionSubmitLock(recordPath, () => submit(recordPath, answers));\n// after\n// debounce: only one in-flight submit per record\nlet submitting = false;\nif (submitting) return;\nsubmitting = true;\ntry { await withQuestionSubmitLock(recordPath, () => submit(recordPath, answers)); }\nfinally { submitting = false; }","handlingStrategy":"retry","validationCode":"// before submit, check for an obviously stale lock dir owned by a dead process\nawait maybeClearDeadLock(lockDirFor(recordPath));","typeGuard":null,"tryCatchPattern":"try { await withQuestionSubmitLock(p, fn); } catch (e) { if ((e as QuestionSubmitError).code === 'question_not_open') { await maybeClearDeadLock(lockDirFor(p)); return withQuestionSubmitLock(p, fn); // one retry } throw e; }","preventionTips":["Debounce/dedupe submit actions so only one lock attempt runs","Clear orphaned lock dirs after crashes","Keep update callbacks fast to minimize lock hold time"],"tags":["locking","concurrency","question-submit"],"backgroundTag":"lock-acquisition-timeout","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}