feat(ocr): opt-in batch OCR mode via the gateway's async batch routes
Add DOCUMENT_OCR_MODE=sync|batch (default sync). In batch mode the tier-3 OCR
processor submits documents to the embedding gateway's async Batch OCR routes
(POST /v1/ocr/batch + GET /v1/ocr/batch/{job_id}, astrolabe-cloud-website#372)
for ~50% cheaper large-corpus backfill. The direct Mistral OCR path is left
untouched. Tracked on Deck #332.
Batch jobs run minutes-hours, so the OCR tier cannot block (the procrastinate
worker reclaims jobs in `doing` after INGEST_STALLED_JOB_SECONDS). Instead it
submits, records the gateway job id in a new per-tenant `batch_ocr_jobs` table
(procrastinate args are immutable across retries), and raises a BatchPending
signal that TieredEscalationStrategy turns into a same-queue deferred re-poll —
releasing the worker slot between polls. On completion the per-page markdown is
indexed like the sync path; a failure or a job past
DOCUMENT_OCR_BATCH_MAX_WAIT_SECONDS marks the document parse-failed.
Batch is opt-in and gateway-only: with the direct mistral backend, no gateway
URL, or the inline/memory pipeline (which can't defer), it falls back to sync.
One batch job per document (coalescing N docs/job is a follow-up).
- embedding/gateway_batch_client.py: submit/poll client (reuses GatewayTokenProvider).
- vector/batch_ocr_store.py + migration 008: job tracking (portable SQLite+PG).
- document_processors/escalation.py: BatchPending control-flow signal.
- document_processors/ocr.py: batch state machine + sync fallback.
- vector/processor.py: thread doc identity to the OCR tier; raise BatchPending
from the pending sentinel; propagate it as control flow (not a failure).
- vector/queue/procrastinate.py: BatchPending -> same-queue retry_in, exempt
from the transient cap (bounded by the processor's deadline).
- config + docs; tests across client/store/processor/strategy/parse-tier.
1653 unit tests pass; ruff + ty green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
07ee91399b
commit
3b7e8d779b
@@ -93,3 +93,26 @@ class EscalateError(Exception):
|
||||
super().__init__(
|
||||
f"escalate {from_tier}->{to_tier} (reason={reason})",
|
||||
)
|
||||
|
||||
|
||||
class BatchPending(Exception):
|
||||
"""Raised when a tier's work is in flight on an async backend and the worker
|
||||
should poll again later (Deck #332 — batch OCR).
|
||||
|
||||
Like :class:`EscalateError` it is a **control-flow signal, NOT a failure**:
|
||||
the document's batch OCR job is still running on the gateway, so the OCR tier
|
||||
submits it (or polls an existing job) and raises this to ask the procrastinate
|
||||
retry strategy to re-run the SAME job on the SAME queue after ``retry_in``
|
||||
seconds — releasing the worker slot meanwhile so a multi-minute/hour batch
|
||||
doesn't pin a worker (and isn't reclaimed as a stalled ``doing`` job).
|
||||
|
||||
It must propagate untouched to the retry strategy: never swallowed by a broad
|
||||
``except Exception`` on the indexing path, never counted as a drop/parse
|
||||
error, and never marks the placeholder failed (the doc isn't done yet).
|
||||
Unlike ``EscalateError`` it does NOT change queue — the job stays on its own
|
||||
(``ocr``) tier queue and is simply deferred.
|
||||
"""
|
||||
|
||||
def __init__(self, *, retry_in: int) -> None:
|
||||
self.retry_in = retry_in
|
||||
super().__init__(f"batch OCR pending (retry_in={retry_in}s)")
|
||||
|
||||
Reference in New Issue
Block a user