Skip to content

Commit d949b3b

Browse files
feat: durable out-of-process job scheduling for file_processors (#6160)
## Summary Inline providers run in the server's event loop today, so a single large `file_processors.process_file` call blocks the whole server, and the work is an in-process closure that dies on restart. This PR introduces **one substrate** that delivers both **process isolation** and **durable scheduling**: a job is an addressable record in a SQL-backed queue that a separate worker process leases by id. That simultaneously gives non-blocking execution (separate process, separate GIL) and survives restarts. The engine is generic; only a thin per-API adapter is provider-specific. Scope is intentionally **file_processors only**. Migrating `batches` and promoting other inline APIs are deferred follow-ups. ## Design New package `src/ogx/core/jobs/`: - **`queue.py`** — `JobQueue` over `SqlStore` (the DB is the IPC channel). Atomic guarded-UPDATE leasing claims a job exclusively; `fail()` requeues until `max_attempts`; `reclaim_stale()` returns expired leases to `scheduled` on startup (the durability/restart property). - **`worker.py`** — `WorkerPool` spawns N processes (spawn context, own GIL). Each rebuilds the real provider impl and its dependencies from a `ProviderDescriptor`, then loops: lease → run method → complete/fail, heartbeating the lease. - **`proxy.py`** — generic `JobBackedProxy` base (enqueue, blocking run-and-wait, get, cancel, list) plus a `WORKER_PROXY_FACTORIES` registry. - **`file_processor_proxy.py`** — the only file-processor-specific code: maps the `FileProcessors` protocol onto the base, stages direct uploads into the Files API so payloads carry only a `file_id` (never bytes), and self-registers via `register_worker_proxy(Api.file_processors, ...)`. - **`dispatch.py` / `runtime.py` / `bootstrap.py`** — per-(api, method) payload (de)serialization, and process-global runtime construction wired into the stack lifecycle. Wiring: - `InlineProviderSpec` gains `execution_mode` (`"inline" | "worker"`), default `"inline"` so **no other API's topology changes**. The resolver returns the registered worker proxy instead of importing the impl in-process when `execution_mode == "worker"`. - All four `file_processors` specs (auto, pypdf, markitdown, docling) are set to worker mode — the problem is file size and blocking in general, not CPU heaviness. ### Adding worker mode for another API later Purely additive, no engine/resolver/worker changes: set `execution_mode: worker` on the spec, add a `dispatch.py` entry, write a ~15-line proxy adapter, call `register_worker_proxy(...)`. ## API changes `file_processors` is `/v1alpha`, so no minimum deprecation window applies. - `POST /file-processors/process` is marked **deprecated** — it still works and keeps its signature, but now runs out-of-process via the job engine. - New async endpoints: `POST /file-processors/jobs` (submit), `GET /file-processors/jobs`, `GET /file-processors/jobs/{job_id}`, `POST /file-processors/jobs/{job_id}/cancel`, with `ProcessFileJob` and `ListProcessFileJobsResponse` models. OpenAPI specs regenerated. ## Test plan Unit coverage for every layer of the substrate: - `test_queue.py` — lease/complete/fail/retry/cancel/reclaim, lease-expiry recovery - `test_worker.py` — worker dispatch (complete and failure paths) - `test_build_impl.py` — descriptor → real provider reconstruction (with a dependency) - `test_proxy.py` — proxy round-trip incl. the deprecated blocking surface - `test_resolver_worker_mode.py` — the resolver seam: a worker spec mounts the proxy and registers a reconstructable descriptor that captures dependencies ```bash uv run --no-sync pytest tests/unit/core/jobs/ -q ``` ``` 25 passed ``` No regressions in the surrounding suites: ```bash uv run --no-sync pytest tests/unit/core tests/unit/providers/files -q ``` ``` 347 passed (+ jobs) ``` ## Out of scope (follow-ups) - Streaming the upload to storage so large files never transit server memory. - Migrating `batches` onto this substrate. - Off-box workers over HTTP (the queue boundary already makes this reachable). - Promoting other inline APIs to `execution_mode: worker`. - End-to-end integration tests (real spawned-worker round-trip, durability across restart, non-blocking-latency proof). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Charlie Doern <cdoern@redhat.com> Signed-off-by: Francisco Javier Arceo <farceo@redhat.com> Co-authored-by: Francisco Javier Arceo <farceo@redhat.com>
1 parent a84aa6b commit d949b3b

44 files changed

Lines changed: 4348 additions & 191 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

client-sdks/stainless/openapi.yml

Lines changed: 250 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3717,13 +3717,158 @@ paths:
37173717
tags:
37183718
- File Processors
37193719
summary: Process a file into chunks ready for vector database storage.
3720-
description: Process a file into chunks ready for vector database storage. Supports direct upload via multipart form or processing files already uploaded to file storage via file_id. Exactly one of file or file_id must be provided.
3720+
description: Deprecated. This endpoint blocks until processing completes, which can hold a connection open for large files. Use POST /file-processors/jobs to submit the file asynchronously and poll GET /file-processors/jobs/{job_id} for the result. Supports direct upload via multipart form or processing files already uploaded to file storage via file_id. Exactly one of file or file_id must be provided.
37213721
operationId: process_file_v1alpha_file_processors_process_post
37223722
requestBody:
37233723
content:
37243724
multipart/form-data:
37253725
schema:
37263726
$ref: '#/components/schemas/Body_process_file_v1alpha_file_processors_process_post'
3727+
deprecated: true
3728+
/v1alpha/file-processors/jobs:
3729+
get:
3730+
responses:
3731+
'200':
3732+
description: Successful Response
3733+
content:
3734+
application/json:
3735+
schema:
3736+
$ref: '#/components/schemas/ListProcessFileJobsResponse'
3737+
'400':
3738+
$ref: '#/components/responses/BadRequest400'
3739+
description: Bad Request
3740+
'429':
3741+
$ref: '#/components/responses/TooManyRequests429'
3742+
description: Too Many Requests
3743+
'500':
3744+
$ref: '#/components/responses/InternalServerError500'
3745+
description: Internal Server Error
3746+
default:
3747+
$ref: '#/components/responses/DefaultError'
3748+
description: Default Response
3749+
tags:
3750+
- File Processors
3751+
summary: List file-processing jobs.
3752+
description: List file-processing jobs, most recent first.
3753+
operationId: list_process_file_jobs_v1alpha_file_processors_jobs_get
3754+
parameters:
3755+
- name: after
3756+
in: query
3757+
required: false
3758+
schema:
3759+
anyOf:
3760+
- type: string
3761+
- type: 'null'
3762+
description: Return jobs after this job ID.
3763+
title: After
3764+
description: Return jobs after this job ID.
3765+
- name: limit
3766+
in: query
3767+
required: false
3768+
schema:
3769+
type: integer
3770+
maximum: 100
3771+
minimum: 1
3772+
description: Maximum jobs to return.
3773+
default: 100
3774+
title: Limit
3775+
description: Maximum jobs to return.
3776+
post:
3777+
responses:
3778+
'200':
3779+
description: The created file-processing job.
3780+
content:
3781+
application/json:
3782+
schema:
3783+
$ref: '#/components/schemas/ProcessFileJob'
3784+
'400':
3785+
$ref: '#/components/responses/BadRequest400'
3786+
description: Bad Request
3787+
'429':
3788+
$ref: '#/components/responses/TooManyRequests429'
3789+
description: Too Many Requests
3790+
'500':
3791+
$ref: '#/components/responses/InternalServerError500'
3792+
description: Internal Server Error
3793+
default:
3794+
$ref: '#/components/responses/DefaultError'
3795+
description: Default Response
3796+
tags:
3797+
- File Processors
3798+
summary: Submit a file for asynchronous processing.
3799+
description: Submit a file for processing and return immediately with a job handle. The work runs out-of-process so the server is not blocked. Poll GET /file-processors/jobs/{job_id} until the status is terminal. Supports direct upload via multipart form or a previously uploaded file_id.
3800+
operationId: create_process_file_job_v1alpha_file_processors_jobs_post
3801+
requestBody:
3802+
content:
3803+
multipart/form-data:
3804+
schema:
3805+
$ref: '#/components/schemas/Body_create_process_file_job_v1alpha_file_processors_jobs_post'
3806+
/v1alpha/file-processors/jobs/{job_id}:
3807+
get:
3808+
responses:
3809+
'200':
3810+
description: Successful Response
3811+
content:
3812+
application/json:
3813+
schema:
3814+
$ref: '#/components/schemas/ProcessFileJob'
3815+
'400':
3816+
$ref: '#/components/responses/BadRequest400'
3817+
description: Bad Request
3818+
'429':
3819+
$ref: '#/components/responses/TooManyRequests429'
3820+
description: Too Many Requests
3821+
'500':
3822+
$ref: '#/components/responses/InternalServerError500'
3823+
description: Internal Server Error
3824+
default:
3825+
$ref: '#/components/responses/DefaultError'
3826+
description: Default Response
3827+
tags:
3828+
- File Processors
3829+
summary: Retrieve a file-processing job.
3830+
description: Retrieve the current state of a file-processing job, including its result once completed.
3831+
operationId: retrieve_process_file_job_v1alpha_file_processors_jobs__job_id__get
3832+
parameters:
3833+
- name: job_id
3834+
in: path
3835+
required: true
3836+
schema:
3837+
type: string
3838+
title: Job Id
3839+
/v1alpha/file-processors/jobs/{job_id}/cancel:
3840+
post:
3841+
responses:
3842+
'200':
3843+
description: Successful Response
3844+
content:
3845+
application/json:
3846+
schema:
3847+
$ref: '#/components/schemas/ProcessFileJob'
3848+
'400':
3849+
$ref: '#/components/responses/BadRequest400'
3850+
description: Bad Request
3851+
'429':
3852+
$ref: '#/components/responses/TooManyRequests429'
3853+
description: Too Many Requests
3854+
'500':
3855+
$ref: '#/components/responses/InternalServerError500'
3856+
description: Internal Server Error
3857+
default:
3858+
$ref: '#/components/responses/DefaultError'
3859+
description: Default Response
3860+
tags:
3861+
- File Processors
3862+
summary: Cancel a file-processing job.
3863+
description: Cancel a scheduled or in-progress file-processing job.
3864+
operationId: cancel_process_file_job_v1alpha_file_processors_jobs__job_id__cancel_post
3865+
parameters:
3866+
- name: job_id
3867+
in: path
3868+
required: true
3869+
schema:
3870+
type: string
3871+
title: Job Id
37273872
/v1/messages:
37283873
post:
37293874
responses:
@@ -11547,6 +11692,32 @@ components:
1154711692
Represents token usage details including input tokens, output tokens, a
1154811693
breakdown of output tokens, and the total tokens used. Only populated on
1154911694
batches created after September 7, 2025.
11695+
Body_create_process_file_job_v1alpha_file_processors_jobs_post:
11696+
properties:
11697+
file:
11698+
anyOf:
11699+
- type: string
11700+
contentMediaType: application/octet-stream
11701+
- type: 'null'
11702+
description: The File object to be uploaded and processed. Mutually exclusive with file_id.
11703+
file_id:
11704+
anyOf:
11705+
- type: string
11706+
- type: 'null'
11707+
description: ID of file already uploaded to file storage. Mutually exclusive with file.
11708+
options:
11709+
anyOf:
11710+
- additionalProperties: true
11711+
type: object
11712+
- type: 'null'
11713+
description: Optional processing options. Provider-specific parameters.
11714+
chunking_strategy:
11715+
anyOf:
11716+
- type: string
11717+
- type: 'null'
11718+
description: Optional chunking strategy for splitting content into chunks. Must be valid JSON string.
11719+
type: object
11720+
title: Body_create_process_file_job_v1alpha_file_processors_jobs_post
1155011721
Body_create_skill_v1alpha_skills_post:
1155111722
properties:
1155211723
file:
@@ -12728,6 +12899,16 @@ components:
1272812899
- cached_tokens
1272912900
title: InputTokensDetails
1273012901
description: A detailed breakdown of the input tokens.
12902+
JobStatus:
12903+
type: string
12904+
enum:
12905+
- completed
12906+
- in_progress
12907+
- failed
12908+
- scheduled
12909+
- cancelled
12910+
title: JobStatus
12911+
description: Status of a job execution.
1273112912
ListConnectorsResponse:
1273212913
properties:
1273312914
data:
@@ -12768,6 +12949,24 @@ components:
1276812949
- has_more
1276912950
title: ListMessageBatchesResponse
1277012951
description: Response from GET /v1/messages/batches.
12952+
ListProcessFileJobsResponse:
12953+
properties:
12954+
data:
12955+
items:
12956+
$ref: '#/components/schemas/ProcessFileJob'
12957+
type: array
12958+
title: Data
12959+
description: The list of file-processing jobs.
12960+
has_more:
12961+
type: boolean
12962+
title: Has More
12963+
description: Whether more jobs are available after this page.
12964+
default: false
12965+
type: object
12966+
required:
12967+
- data
12968+
title: ListProcessFileJobsResponse
12969+
description: Response model listing file-processing jobs.
1277112970
ListSkillVersionsResponse:
1277212971
properties:
1277312972
object:
@@ -13634,6 +13833,44 @@ components:
1363413833
- reasoning_tokens
1363513834
title: OutputTokensDetails
1363613835
description: A detailed breakdown of the output tokens.
13836+
ProcessFileJob:
13837+
properties:
13838+
job_id:
13839+
type: string
13840+
title: Job Id
13841+
description: Unique identifier for the job.
13842+
status:
13843+
$ref: '#/components/schemas/JobStatus'
13844+
description: Current execution status of the job.
13845+
created_at:
13846+
type: integer
13847+
title: Created At
13848+
description: Unix timestamp (seconds) for when the job was created.
13849+
result:
13850+
anyOf:
13851+
- $ref: '#/components/schemas/ProcessFileResponse'
13852+
title: ProcessFileResponse
13853+
- type: 'null'
13854+
description: The processed file result. Present only once the job has completed successfully.
13855+
title: ProcessFileResponse
13856+
error:
13857+
anyOf:
13858+
- type: string
13859+
- type: 'null'
13860+
description: Error message. Present only if the job failed.
13861+
type: object
13862+
required:
13863+
- job_id
13864+
- status
13865+
- created_at
13866+
title: ProcessFileJob
13867+
description: |-
13868+
An asynchronous file-processing job.
13869+
13870+
Returned when a file is submitted for processing via the job-based API. Poll
13871+
by job_id until ``status`` is terminal (completed/failed/cancelled). When
13872+
completed, ``result`` holds the processed chunks; when failed, ``error``
13873+
explains why.
1363713874
ProcessFileResponse:
1363813875
properties:
1363913876
chunks:
@@ -15048,6 +15285,18 @@ components:
1504815285
The container image to use for this implementation. If one is provided, pip_packages will be ignored.
1504915286
If a provider depends on other providers, the dependencies MUST NOT specify a container image.
1505015287
nullable: true
15288+
execution_mode:
15289+
default: inline
15290+
description: |2
15291+
15292+
How this provider's work is executed. 'inline' runs the implementation in the server
15293+
process (default, unchanged behavior). 'worker' runs the implementation in a separate
15294+
worker process via the job queue, so long or large operations do not block the server.
15295+
enum:
15296+
- inline
15297+
- worker
15298+
title: Execution Mode
15299+
type: string
1505115300
description:
1505215301
anyOf:
1505315302
- type: string
@@ -15787,16 +16036,6 @@ components:
1578716036
- batch_id
1578816037
title: CancelBatchRequest
1578916038
type: object
15790-
JobStatus:
15791-
description: Status of a job execution.
15792-
enum:
15793-
- completed
15794-
- in_progress
15795-
- failed
15796-
- scheduled
15797-
- cancelled
15798-
title: JobStatus
15799-
type: string
1580016039
Job:
1580116040
description: A job execution instance with status tracking.
1580216041
properties:

docs/docs/providers/file_processors/inline_docling.mdx

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ description: |
1111
- **Layout preservation** — tables, lists, and nested structures are converted to Markdown
1212
- **Multi-format support** — PDF, DOCX, PPTX, HTML, and images
1313
- **Better RAG quality** — structured chunks with heading metadata produce more relevant retrieval results
14-
- **VLM-based processing** — optionally route Vision Language Model inference through the stack's model-serving
15-
infrastructure for richer document understanding (layout analysis, OCR via vision models)
14+
- **VLM configuration validation** — VLM processing requires worker-side inference routing, which is not yet
15+
supported; configuring `vlm_model` currently produces a clear startup error
1616
1717
## Usage
1818
@@ -33,23 +33,13 @@ description: |
3333
config: {}
3434
```
3535
36-
### Enabling VLM Processing
36+
### VLM Processing
3737
38-
To enable VLM-based document processing, set `vlm_model` to a vision model registered with the
39-
stack's inference API. The VLM pipeline routes inference through the stack's model-serving
40-
infrastructure — no separate GPU resources are needed for document processing.
38+
VLM-based document processing is not currently available while this provider runs in worker
39+
mode because auto-routed inference cannot yet be reconstructed in the worker process. A
40+
configuration with `vlm_model` fails startup rather than silently running a different pipeline.
4141
42-
```yaml
43-
file_processors:
44-
- provider_id: docling
45-
provider_type: inline::docling
46-
config:
47-
vlm_model: granite-docling-258M
48-
vlm_preset: granite_docling
49-
```
50-
51-
When `vlm_model` is not set or no inference provider is available, the processor gracefully
52-
degrades to the standard non-VLM pipeline.
42+
Leave `vlm_model` unset to use the standard non-VLM pipeline.
5343
5444
## Installation
5545
@@ -80,8 +70,8 @@ preserves semantic boundaries. It supports PDF, DOCX, PPTX, HTML, and images.
8070
- **Layout preservation** — tables, lists, and nested structures are converted to Markdown
8171
- **Multi-format support** — PDF, DOCX, PPTX, HTML, and images
8272
- **Better RAG quality** — structured chunks with heading metadata produce more relevant retrieval results
83-
- **VLM-based processing**optionally route Vision Language Model inference through the stack's model-serving
84-
infrastructure for richer document understanding (layout analysis, OCR via vision models)
73+
- **VLM configuration validation**VLM processing requires worker-side inference routing, which is not yet
74+
supported; configuring `vlm_model` currently produces a clear startup error
8575

8676
## Usage
8777

@@ -102,23 +92,13 @@ file_processors:
10292
config: {}
10393
```
10494
105-
### Enabling VLM Processing
95+
### VLM Processing
10696
107-
To enable VLM-based document processing, set `vlm_model` to a vision model registered with the
108-
stack's inference API. The VLM pipeline routes inference through the stack's model-serving
109-
infrastructure — no separate GPU resources are needed for document processing.
110-
111-
```yaml
112-
file_processors:
113-
- provider_id: docling
114-
provider_type: inline::docling
115-
config:
116-
vlm_model: granite-docling-258M
117-
vlm_preset: granite_docling
118-
```
97+
VLM-based document processing is not currently available while this provider runs in worker
98+
mode because auto-routed inference cannot yet be reconstructed in the worker process. A
99+
configuration with `vlm_model` fails startup rather than silently running a different pipeline.
119100

120-
When `vlm_model` is not set or no inference provider is available, the processor gracefully
121-
degrades to the standard non-VLM pipeline.
101+
Leave `vlm_model` unset to use the standard non-VLM pipeline.
122102

123103
## Installation
124104

0 commit comments

Comments
 (0)