fix(batch): stripe projection discovery startup cursors

Merged
GI
8h ago
+197-13

Problem

Projection discovery instances begin at the same keyset head after a roll, repeatedly offering the same owners to the advisory claim. Revision, hostname and PID alone may be identical across Cloud Run replicas.

Fix

Select a random 128-bit process seed on first use, mix it with the revision, and cache the resulting SHA-256 stripe modulo 4096 for the process lifetime. A local lock makes first-use initialization and logging occur once even with concurrent scanners. No metadata server or network I/O is used. The larger stripe space reduces collisions at 30–50 instances; restarts may choose a different stripe. Log the chosen stripe once in the process selector, rather than for every scanner.

Seed each scanner at its committed_at lower bound between the oldest matching result and startup time. Existing committed_at + document ID ordering remains oldest-first after that bound, then wraps to the head. Every row remains discoverable; no durable state changes. The existing follow-up seed takes precedence. Page sizes, sweep cadence, owner-drain concurrency and owner claims are unchanged.

Tests

314 passed across 8 files using BACKEND_UNIT_TEST_FILE_LIST=<file> bash backend/test.sh through bin/omi-heavy run --: projection discovery, batch-stage environment ratchet, flag budget, unit policy, follow-up desync, follow-up reads, sweep owner-chain and owner-drain tests. Injected seeds model separate processes with identical metadata and verify process-stable selection, different stripes and one log per process. Full-cycle coverage covers stripe boundaries through 4095, timestamp ties, wrap and empty/short collections.

B1 from the supplied correctness review is fixed. The insert-behind-cursor follow-up remains outside this change.

Risk

This is discovery desynchronization, not exclusive partitioning. Hash collisions and sparse/skewed timestamp ranges can still share starting rows. Startup adds one scoped head read per scanner; a seed beyond the newest result retries the head immediately. Throughput and claim-busy impact require a separately authorized production canary. No production access, merge or deployment in this lane.

Rollback

The switch defaults on. Set BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED=off and restart scanners to recover the exact pre-change cursor sequence and query budget. Pagination off also bypasses seeding.

Failure-Class: none
This is cross-instance discovery contention; the blocked-page class covers dependency starvation and the scheduler-shard class covers shared control mutations. Transaction fences remain unchanged.