The OCR dispatcher acquired a borrowed semaphore permit, then ran the CPU-bound inference in `spawn_blocking`. On cancellation (graceful shutdown) the future dropped the permit while the detached blocking task kept running, briefly oversubscribing the blocking pool past ANALYSIS_WORKERS. Extract `run_ocr_blocking`, which acquires an *owned* permit and moves it into the blocking task so the concurrency slot's lifetime matches the actual work. Covered by a cancellation-invariant test asserting the permit stays held after the caller future is aborted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
862 B
862 B