{"record":{"id":"98c7873a9b8cc9fd","repo":"santifer/career-ops","slug":"could-not-claim-count-report-slot-s-after-ma","errorCode":null,"errorMessage":"Could not claim ${count} report slot(s) after ${MAX_RETRIES} retries","messagePattern":"Could not claim (.+?) report slot\\(s\\) after (.+?) retries","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"reserve-report-num.mjs","lineNumber":206,"sourceCode":"        } else {\n          failedAt = num;\n          break;\n        }\n      }\n      if (failedAt == null) {\n        Object.defineProperty(claimed, RESERVATION_TOKEN, { value: token });\n        return claimed;\n      }\n\n      for (const num of claimed) releaseSlot(reportsDir, num, { token });\n      occupied = collectOccupied(reportsDir, trackerPath);\n      base = Math.max(failedAt + 1, highestNumber(occupied) + 1);\n    }\n  } finally {\n    lock.release();\n  }\n\n  throw new Error(`Could not claim ${count} report slot(s) after ${MAX_RETRIES} retries`);\n}\n\n/**\n * Release reservation sentinels after report creation or on failure.\n * Only the array returned by reserveReportNumbers owns its sentinels. The CLI\n * uses force mode as an explicit administrative cleanup path.\n */\nexport async function releaseReportNumbers(numbers, options = {}) {\n  const reportsDir = reportsDirFor(options);\n  const values = Array.isArray(numbers) ? numbers : [numbers];\n  for (const num of values) {\n    if (!Number.isSafeInteger(num) || num < 1) {\n      throw new TypeError(`Report number must be a positive integer, got ${num}`);\n    }\n  }\n  const force = options.force === true;\n  const token = options.reservationToken || numbers?.[RESERVATION_TOKEN];\n  if (!force && !token) throw new Error('Reservation ownership token is required for release');","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/reserve-report-num.mjs#L188-L224","documentation":"After acquiring the tracker lock, reserveReportNumbers retries up to MAX_RETRIES (50) times to claim a contiguous run of `count` slots. If every retry fails — because sentinels keep appearing in the target range from concurrent or stale reservations — it throws a generic Error. Each retry re-reads occupied slots and advances base past the failure point, so this indicates sustained contention or corrupted sentinels that never clear.","triggerScenarios":"Many parallel workers (more than the lock allows through quickly) all competing for the next contiguous range; stale RESERVED sentinel files whose owning process died but whose staleMs window (10 min default) has not elapsed; a corrupted sentinel that readSentinelOwner cannot parse and that blocks the same slot every iteration.","commonSituations":"Running a large batch fan-out (>50 concurrent evaluators) without pre-reserving a shared range; sentinel files left behind by a killed process; clock skew causing stale-lock detection to fail; disk-full or permission issues causing wx flag failures.","solutions":["Reduce concurrency: reserve report numbers centrally once (node reserve-report-num.mjs --count N) and hand each worker a pre-assigned ID rather than having each worker call reserveReportNumbers itself.","Clean stale sentinels: node reserve-report-num.mjs --release <range> --force, or raise CAREER_OPS_TRACKER_LOCK_STALE_MS if processes are legitimately long-lived.","Inspect reports/*-RESERVED.md files: check the pid field with processIsAlive; remove orphans.","Retry the operation after a short delay — transient heavy contention may clear."],"exampleFix":"// before: each worker reserves its own number (contention)\nconst ids = await reserveReportNumbers(1);\n\n// after: reserve a shared range up front, distribute\nconst { execSync } = require('child_process');\nconst range = execSync('node reserve-report-num.mjs --count 10').toString().trim(); // e.g. '042-051'\nconst [start, end] = range.split('-').map(Number);\n// hand each worker its own id from start..end","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let ids;\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    ids = await reserveReportNumbers(count);\n    break;\n  } catch (err) {\n    if (err.message.includes('Could not claim') && attempt < 2) {\n      await new Promise(r => setTimeout(r, 500 * (attempt + 1)));\n      continue;\n    }\n    throw err;\n  }\n}","preventionTips":["Pre-reserve a shared range centrally and distribute IDs to workers instead of having each worker reserve.","Keep concurrency below the level that saturates the lock; clean stale sentinels between batch runs.","Monitor reports/*-RESERVED.md for orphaned sentinels whose pid is no longer alive."],"tags":["contention","locking","report-number","concurrency","retries"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}