{"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":220,"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":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/reserve-report-num.mjs#L202-L238","documentation":"reserveReportNumbers retries up to MAX_RETRIES (50) times: each attempt claims `count` contiguous slots, and on collision releases partial claims, rescans occupied numbers, and retries from the next free base. If all 50 attempts fail, the reservation is abandoned and this generic Error is thrown — meaning another process or pre-existing files keep winning the slots. This is the contention-exhaustion path, reached only after the loop releases every partially claimed sentinel.","triggerScenarios":"Massively concurrent fan-out where dozens of worker processes reserve simultaneously and each collision forces a rescan+retry, exhausting 50 attempts. A reports/ directory so densely pre-populated that contiguous runs of `count` free numbers keep getting taken. A racing process ignoring the tracker lock (e.g. writing NNN-RESERVED.md sentinels manually).","commonSituations":"Parallel batch evaluators spawned without first reserving a range via reserve-report-num.mjs (the documented anti-race pattern #749). Long CI pipelines where each worker calls reserveReportNumbers(1) in a hot loop instead of reserving a block once. Leftover sentinels from crashed runs shrinking the free range (run --gc).","solutions":["Reserve the whole range once before spawning N parallel workers: node reserve-report-num.mjs --count N, and hand each worker its assigned number (see AGENTS.md fan-out guidance).","Run node reserve-report-num.mjs --gc to clear stale sentinels (older than 4h) that are shrinking the free range.","Reduce concurrency: chunk the work so fewer processes reserve simultaneously, or add jitter/backoff between workers.","Increase MAX_RETRIES pressure indirectly by reserving smaller counts per call; large contiguous blocks collide more often.","Check for rogue processes writing reserved sentinels outside the API and stop them."],"exampleFix":"// before (per-worker race)\nworkers.forEach(() => spawn('node evaluator.mjs')); // each calls reserveReportNumbers itself\n// after\nconst [lo, hi] = await reserveReportNumbers(workers.length); // reserve up front\nworkers.forEach((_, i) => spawn('node evaluator.mjs', [String(lo + i)]));","handlingStrategy":"retry","validationCode":"// before fanning out, reserve the whole range up front (documented pattern #749)\nconst range = await reserveReportNumbers(workerCount);\nconsole.log('reserved', range[0], '-', range[range.length - 1]);","typeGuard":null,"tryCatchPattern":"try {\n  ids = await reserveReportNumbers(count);\n} catch (err) {\n  if (err.message.includes('Could not claim')) {\n    await new Promise(r => setTimeout(r, 1000)); // backoff\n    ids = await reserveReportNumbers(count); // one bounded retry after contention settles\n  } else throw err;\n}","preventionTips":["Reserve a range once before spawning parallel workers; never let each worker compute its own number.","Run `node reserve-report-num.mjs --gc` periodically to clear stale sentinels.","Keep per-call counts small; huge contiguous blocks collide more under contention.","Ensure all writers go through the tracker lock; rogue sentinel writers defeat the retry logic.","Free reserved slots promptly with releaseReportNumbers after report creation or failure."],"tags":["concurrency","contention","retry-exhausted","file-locking"],"backgroundTag":"retry-limit-exceeded","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"}