"""Request bounds for inline ingestion (ADR-0017). Three independent bounds, each mapping to its own status code: the chunk ceiling (`413`, checked before embedding starts), process-wide concurrency (`503` + `Retry-After`, rejected rather than queued), and the work-phase deadline (`504`, and the caller must still write a terminal job status). """ from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager from anyio import Semaphore, WouldBlock from src.application.ingestion.errors import ChunkLimitExceededError, IngestionAtCapacityError from src.application.ingestion.models import Chunk def enforce_chunk_limit(chunks: Sequence[Chunk], *, max_chunks: int) -> None: """Raise `ChunkLimitExceededError` if `chunks` exceeds `max_chunks`. Call this immediately after parsing/chunking and before any embedding call — the ceiling must be discovered up front, not mid-batch. """ if len(chunks) > max_chunks: raise ChunkLimitExceededError( f"document produced {len(chunks)} chunks, over the {max_chunks}-chunk limit" ) @asynccontextmanager async def acquire_ingestion_slot(limiter: Semaphore) -> AsyncIterator[None]: """Hold one of `INGESTION_MAX_CONCURRENCY` process-wide slots for the block. `limiter` is an `anyio.Semaphore` created once in the lifespan. Rejects immediately with `IngestionAtCapacityError` when the process is already at capacity, rather than queueing the request behind an unbounded wait (ADR-0017) — the semaphore's own async `acquire()` would do the latter. """ try: limiter.acquire_nowait() except WouldBlock: raise IngestionAtCapacityError( "ingestion is at capacity; retry after the configured backoff" ) from None try: yield finally: limiter.release()