# How the Career-Ops Atomic Report Numbering Reservation System Prevents Conflicts

> Discover how the Career-Ops atomic report numbering reservation system prevents ID conflicts. Learn about its centralized process lock, unified view, and atomic sentinel file creation for high concurrency.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: internals
- Published: 2026-08-19

---

**The Career-Ops atomic report numbering reservation system prevents conflicts by combining a centralized process lock, a unified view of existing report files and tracker entries, and atomic sentinel file creation using exclusive write flags, ensuring parallel workers never allocate duplicate IDs even under high concurrency.**

The `career-ops` repository implements a robust atomic report numbering reservation system to coordinate parallel workers such as batch evaluators, scanners, and manual CLI invocations. This system guarantees that every report receives a unique sequential number by eliminating race conditions through three coordinated safety mechanisms that operate across the `reserve-report-num.mjs`, `tracker-utils.mjs`, and `tracker-parse.mjs` modules.

## Centralized Locking with trackerLockDirFor

Before any reservation attempt, the process must acquire a shared **tracker lock** via `acquireTrackerLock`. This utility, defined in `tracker-utils.mjs`, is invoked within `reserve-report-num.mjs` at lines 85-86. The lock guarantees exclusive access while the reservation logic executes, preventing interleaved reads and writes from different processes that could otherwise lead to duplicate number assignments. Every component that writes to the tracker or creates report files must obtain this lock first, creating a single-writer bottleneck that serializes all reservation attempts.

## Unified Occupancy Detection

The system constructs a complete set of already-used numbers by merging two distinct sources before attempting any allocation. The `occupiedFromReports` function (lines 71-79 in `reserve-report-num.mjs`) scans existing report files on disk, while `occupiedFromTracker` (lines 84-96) extracts referenced report numbers from tracker rows using parsers from `tracker-parse.mjs`. This merged view captures every number present in the filesystem or referenced in the tracker—including report links embedded inside tracker rows—creating a definitive inventory of unavailable IDs. By combining these sources, the system prevents collisions with legacy reports and ongoing tracker entries alike.

## Atomic Sentinel File Creation

When the system identifies a free number, it writes a **reservation sentinel** file (e.g., [`042-RESERVED.md`](https://github.com/santifer/career-ops/blob/main/042-RESERVED.md)) using `writeFileSync` with the `O_CREAT|O_EXCL` flag (`{flag:'wx'}`). Implemented in the `claimSlot` function at lines 19-24 of `reserve-report-num.mjs`, this atomic operation fails if another process has already created the same sentinel, converting potential race conditions into detectable filesystem errors. If any slot in a requested batch cannot be claimed, the entire reservation is aborted, previously claimed sentinels are released, and the algorithm retries with a newly calculated base range.

## Retry Logic and Batch Reservation

The `reserveReportNumbers` function implements a retry loop with a maximum of **50 attempts**. On each iteration, it:

1. Calculates a starting base: `base = highestNumber(occupied) + 1`
2. Attempts to claim a contiguous block of the requested size
3. If any slot is already taken, releases partially claimed sentinels and recomputes: `base = Math.max(failedAt + 1, highestNumber(occupied) + 1)`

Because the tracker lock is held throughout the entire sequence, no other process can alter the occupancy set during the check-and-claim steps. This guarantees that the finally returned numbers are **unique and conflict-free**, even when dozens of parallel workers request batches simultaneously.

## Garbage Collection for Stale Reservations

To prevent resource leaks from crashed processes, the `gcStaleReportReservations` function (lines 57-73 in `reserve-report-num.mjs`) removes stale sentinel files after a configurable TTL (default **4 hours**). This cleanup operation also runs under the same tracker lock acquired via `acquireTrackerLock`, ensuring that removal never races with ongoing reservations and that reserved numbers eventually return to the available pool.

## Practical Usage Examples

### Reserve a Single Report Number

```bash
node reserve-report-num.mjs

# → prints e.g. 042

```

The CLI invokes `reserveReportNumbers(1)`. The lock is taken, the next free number is discovered, a sentinel [`042-RESERVED.md`](https://github.com/santifer/career-ops/blob/main/042-RESERVED.md) is created atomically, and the number is printed.

### Reserve Multiple Consecutive Numbers

```bash
node reserve-report-num.mjs --count 5

# → prints e.g. 056-060

```

A batch of five contiguous IDs is reserved atomically. If any of the five slots were already taken, the reservation retries until a free range is found.

### Release Reserved Numbers

```bash
node reserve-report-num.mjs --release 056-060

```

After workers finish writing the actual report files (e.g., [`reports/056-example.md`](https://github.com/santifer/career-ops/blob/main/reports/056-example.md)), you free the sentinel files. The release operation also uses the same lock to avoid race conditions.

### Programmatic API

```javascript
import { reserveReportNumbers, releaseReportNumbers } from './reserve-report-num.mjs';

async function makeReports(count) {
  const nums = await reserveReportNumbers(count);
  // nums is an array like [101, 102, 103]
  // … generate reports using these numbers …
  // finally release the reservation tokens:
  await releaseReportNumbers(nums);
}

```

The exported async functions can be used by any mode (`scan.mjs`, batch workers, etc.). The returned array carries a hidden `RESERVATION_TOKEN` that must be passed to `releaseReportNumbers` unless you force the release.

## Summary

- **Centralized locking** via `acquireTrackerLock` ensures exclusive access during the reservation window (lines 85-86 in `reserve-report-num.mjs`).
- **Unified occupancy** detection merges existing reports and tracker entries to identify unavailable numbers (lines 71-79 and 84-96).
- **Atomic sentinel creation** with `O_CREAT|O_EXCL` flags prevents duplicate claims by converting races into filesystem errors (lines 19-24).
- **Automatic retry logic** with up to 50 attempts handles transient conflicts by recalculating base numbers and releasing partial claims.
- **Garbage collection** removes stale sentinels after 4 hours to prevent leaks from crashed workers (lines 57-73).

## Frequently Asked Questions

### What happens if two processes request report numbers simultaneously?

When two processes request numbers simultaneously, the centralized tracker lock ensures they execute sequentially. The first process acquires the lock via `acquireTrackerLock`, scans occupancy, and creates sentinel files atomically. The second process waits until the lock releases, then sees the updated occupancy set including the new sentinels, forcing it to select higher numbers.

### How does the system handle crashed processes that never release their reservations?

The `gcStaleReportReservations` function runs periodically to remove sentinel files older than the configured TTL (default 4 hours). This garbage collection operates under the same tracker lock used for reservations, preventing cleanup from racing with active allocations and ensuring reserved numbers eventually return to the available pool.

### Can I reserve non-consecutive report numbers?

The current implementation reserves contiguous blocks only. The `reserveReportNumbers` function calculates a base number and attempts to claim a consecutive range of the requested size. If you need scattered numbers, you must make multiple single-count reservations, though this reduces efficiency compared to batch allocation.

### Where is the locking mechanism implemented?

The locking primitives reside in `tracker-utils.mjs`, which provides `acquireTrackerLock` and `trackerLockDirFor` utilities. The reservation logic that consumes these locks is located in `reserve-report-num.mjs` at lines 85-86, ensuring synchronized access to both the tracker state and the reservation sentinels.