Skip to content

Commit 6618aca

Browse files
derekhigginsclaudecdoern
authored
fix(file_processor): offload sync parsing to thread pool (#6095)
## Summary - Inline file processors (pypdf, markitdown, docling) run CPU-intensive parsing synchronously in the async event loop, blocking all concurrent requests - Wrap parsing calls in `asyncio.to_thread()` to offload to the thread pool, matching the pattern used throughout the codebase (faiss, sqlite-vec, milvus, localfs, sentence-transformers, etc.) ## Reproduction With a ~4.5MB text file, concurrent `/v1/models` requests spike to **6500ms+** latency during file processing (vs normal ~5ms). After fix, max probe latency stays under **175ms**. | | Before | After | |---|---|---| | Max probe latency | 6572ms | 173ms | | Avg probe latency | 602ms | 9ms | ## Test plan - [x] Existing unit tests pass (`tests/unit/providers/file_processor/` - 35 tests) - [x] Pre-commit checks pass - [x] Manual verification: server stays responsive during file processing Fixes #6094 Signed-off-by: Derek Higgins <derekh@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Charlie Doern <cdoern@redhat.com>
1 parent 16c0ad8 commit 6618aca

3 files changed

Lines changed: 37 additions & 4 deletions

File tree

src/ogx/providers/inline/file_processor/docling/docling.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
# This source code is licensed under the terms described in the LICENSE file in
55
# the root directory of this source tree.
66

7+
import asyncio
78
import os
89
import tempfile
10+
import threading
911
import time
1012
import uuid
1113
from typing import Any
@@ -49,6 +51,7 @@ def __init__(self, config: DoclingFileProcessorConfig, files_api=None) -> None:
4951
self.converter = DocumentConverter(
5052
format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)}
5153
)
54+
self._converter_lock = threading.Lock()
5255

5356
async def process_file(
5457
self,
@@ -80,13 +83,25 @@ async def process_file(
8083
)
8184
content = content_response.body
8285

86+
return await asyncio.to_thread(self._process_content, content, filename, file_id, chunking_strategy, start_time)
87+
88+
def _process_content(
89+
self,
90+
content: bytes,
91+
filename: str,
92+
file_id: str | None,
93+
chunking_strategy: VectorStoreChunkingStrategy | None,
94+
start_time: float,
95+
) -> ProcessFileResponse:
96+
"""Convert and chunk file content. Runs in a thread."""
8397
# Preserve original file extension so DocumentConverter can detect the format
8498
suffix = os.path.splitext(filename)[1] or ".bin"
8599
with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as tmp:
86100
tmp.write(content)
87101
tmp.flush()
88102

89-
result = self.converter.convert(tmp.name)
103+
with self._converter_lock:
104+
result = self.converter.convert(tmp.name)
90105

91106
doc = result.document
92107
page_count = doc.num_pages()

src/ogx/providers/inline/file_processor/markitdown/markitdown_processor.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
# This source code is licensed under the terms described in the LICENSE file in
55
# the root directory of this source tree.
66

7+
import asyncio
78
import os
89
import tempfile
10+
import threading
911
import time
1012
import uuid
1113
from typing import Any
@@ -40,6 +42,7 @@ def __init__(self, config: MarkItDownFileProcessorConfig, files_api) -> None:
4042
self.config = config
4143
self.files_api = files_api
4244
self.converter = MarkItDown()
45+
self._converter_lock = threading.Lock()
4346

4447
async def process_file(
4548
self,
@@ -69,13 +72,25 @@ async def process_file(
6972
)
7073
content = content_response.body
7174

75+
return await asyncio.to_thread(self._process_content, content, filename, file_id, chunking_strategy, start_time)
76+
77+
def _process_content(
78+
self,
79+
content: bytes,
80+
filename: str,
81+
file_id: str | None,
82+
chunking_strategy: VectorStoreChunkingStrategy | None,
83+
start_time: float,
84+
) -> ProcessFileResponse:
85+
"""Convert and chunk file content. Runs in a thread."""
7286
suffix = os.path.splitext(filename)[1] or ".bin"
7387
with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as tmp:
7488
tmp.write(content)
7589
tmp.flush()
7690

7791
try:
78-
result = self.converter.convert(tmp.name)
92+
with self._converter_lock:
93+
result = self.converter.convert(tmp.name)
7994
except Exception as e:
8095
raise HTTPException(
8196
status_code=422,

src/ogx/providers/inline/file_processor/pypdf/pypdf.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This source code is licensed under the terms described in the LICENSE file in
55
# the root directory of this source tree.
66

7+
import asyncio
78
import io
89
import mimetypes
910
import time
@@ -74,9 +75,11 @@ async def process_file(
7475
mime_category = mime_type.split("/")[0] if (mime_type and "/" in mime_type) else None
7576

7677
if mime_type == "application/pdf":
77-
return self._process_pdf(content, filename, file_id, chunking_strategy, start_time)
78+
return await asyncio.to_thread(self._process_pdf, content, filename, file_id, chunking_strategy, start_time)
7879
elif mime_category == "text":
79-
return self._process_text(content, filename, file_id, chunking_strategy, start_time)
80+
return await asyncio.to_thread(
81+
self._process_text, content, filename, file_id, chunking_strategy, start_time
82+
)
8083
else:
8184
raise HTTPException(
8285
status_code=422,

0 commit comments

Comments
 (0)