{"record":{"id":"d4c251df258eb15d","repo":"shareAI-lab/learn-claude-code","slug":"could-not-allocate-a-cron-job-id","errorCode":null,"errorMessage":"Could not allocate a cron job ID","messagePattern":"Could not allocate a cron job ID","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"s12_cron_scheduler/code.py","lineNumber":411,"sourceCode":"                if not job.prompt.strip():\n                    raise ValueError(\"prompt cannot be empty\")\n            except (TypeError, ValueError) as error:\n                print(f\"  [cron] skipped invalid saved job: {error}\")\n                continue\n            scheduled_jobs[job.id] = job\n            if job.pending_delivery:\n                cron_queue.append(job)\n            loaded += 1\n    if loaded:\n        print(f\"  [cron] loaded {loaded} durable job(s)\")\n\n\ndef new_cron_id() -> str:\n    for _ in range(100):\n        job_id = f\"cron_{secrets.token_hex(4)}\"\n        if job_id not in scheduled_jobs:\n            return job_id\n    raise RuntimeError(\"Could not allocate a cron job ID\")\n\n\ndef schedule_job(cron: str, prompt: str, recurring: bool = True,\n                 durable: bool = True) -> CronJob | str:\n    error = validate_cron(cron)\n    if error:\n        return error\n    if not prompt.strip():\n        return \"Prompt cannot be empty\"\n\n    with cron_lock:\n        job = CronJob(\n            id=new_cron_id(),\n            cron=cron,\n            prompt=prompt,\n            recurring=recurring,\n            durable=durable,\n        )","sourceCodeStart":393,"sourceCodeEnd":429,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s12_cron_scheduler/code.py#L393-L429","documentation":"Raised by new_cron_id() in s12_cron_scheduler/code.py:411 after 100 attempts to mint an unused 'cron_' + 8-hex-char ID, every one of which was already present in the in-memory scheduled_jobs dict. IDs come from secrets.token_hex(4) (32 bits); with a normal number of jobs, 100 random draws colliding is astronomically improbable, so hitting this error in practice signals a degenerate state — e.g. scheduled_jobs pre-populated with an enormous or exhaustive ID set, or a bug seeding the dict with every generated candidate.","triggerScenarios":"A seeded/mocked scheduled_jobs containing huge numbers of IDs (approaching 2^32) before calling new_cron_id(); a loop bug that inserts each candidate ID before checking, guaranteeing collisions; corrupted state loaded from a tampered .scheduled_tasks.json with an ID flood. Real-world stores with dozens of jobs cannot trigger this.","commonSituations":"Tests that exhaustively seed the ID space to verify the guard; tampered or machine-generated durable files containing thousands of IDs (still far from the threshold); bugs in code that snapshots and restores scheduled_jobs.","solutions":["Inspect len(scheduled_jobs) and the contents of .scheduled_tasks.json — if the dict is unexpectedly full of IDs, the state is corrupted; clear or prune it.","Widen the ID space (e.g. secrets.token_hex(8) with the matching prefix check) if you legitimately need billions of jobs, and migrate old IDs.","In code that manages scheduled_jobs manually, never insert a candidate ID before new_cron_id() returns it."],"exampleFix":"# before\njob_id = f'cron_{secrets.token_hex(4)}'\n\n# after (wider space; also relax the reload prefix check to accept 16 hex chars)\njob_id = f'cron_{secrets.token_hex(8)}'","handlingStrategy":"retry","validationCode":"def id_space_is_healthy(scheduled_jobs: dict) -> bool:\n    return len(scheduled_jobs) < 1_000_000  # far below any realistic collision risk","typeGuard":null,"tryCatchPattern":"try:\n    job_id = new_cron_id()\nexcept RuntimeError:\n    # state is degenerate; clear or prune scheduled_jobs, then retry once\n    prune_or_reset_scheduled_jobs()\n    job_id = new_cron_id()","preventionTips":["Never insert candidate IDs into scheduled_jobs yourself; let new_cron_id() manage allocation.","Keep .scheduled_tasks.json free of machine-generated ID floods.","If you truly need massive job counts, widen the ID to token_hex(8) and migrate."],"tags":["cron","ids","state-corruption","race-condition"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}