Skip to content

Commit ae3bb94

Browse files
committed
fix sitemap implementation, improve document ingestion and pipeline reliability
1 parent b1e5412 commit ae3bb94

10 files changed

Lines changed: 1489 additions & 341 deletions

File tree

apps/ingestion-worker/handlers/file.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import structlog
33
import os
4+
import time as time_mod
45
import pebble
56
from concurrent.futures import TimeoutError
67
# Deferred imports for docling to ensure clean process initialization
@@ -172,25 +173,61 @@ def process_file_sync(file_path: str) -> dict:
172173
raise e
173174

174175

175-
# Use Pebble ProcessPool for robust process management (timeout = kill)
176-
# Scaled to 8 workers for high-core machines (12 cores / 24 threads)
177-
# We rely on deferred imports in init_worker to simulate clean state and strict thread limits
178-
executor = pebble.ProcessPool(max_workers=8, initializer=init_worker)
179-
180176
# Increase timeout to 30 minutes to accommodate large PDF books with OCR
181177
TIMEOUT_SECONDS = 1800
182178

179+
# Maximum file size: 200MB — prevents OOM in worker processes
180+
MAX_FILE_SIZE_BYTES = 200 * 1024 * 1024
181+
182+
# Lazy-initialized ProcessPool with recovery on crash
183+
_executor: pebble.ProcessPool | None = None
184+
185+
186+
def _get_executor() -> pebble.ProcessPool:
187+
"""Get or create the Pebble ProcessPool, recreating if the pool is broken."""
188+
global _executor
189+
if _executor is None or not _executor.active:
190+
if _executor is not None:
191+
logger.warning("process_pool_recreating", reason="pool_inactive")
192+
_executor = pebble.ProcessPool(max_workers=8, initializer=init_worker)
193+
return _executor
194+
183195

184196
async def handle_file_task(file_path: str) -> list[dict]:
185197
"""
186198
Converts a document to markdown using Docling.
187199
Executes in a Pebble ProcessPool to enforce hard timeouts and kill stuck processes.
188200
"""
189-
logger.info("conversion_starting", path=file_path)
201+
file_ext = os.path.splitext(file_path)[1].lower()
202+
203+
# --- Pre-flight validation ---
204+
if not os.path.isfile(file_path):
205+
raise IngestionError(ERR_INVALID_FORMAT, f"File not found: {file_path}")
206+
207+
file_size = os.path.getsize(file_path)
208+
if file_size == 0:
209+
raise IngestionError(ERR_EMPTY, "File is empty (0 bytes)")
210+
if file_size > MAX_FILE_SIZE_BYTES:
211+
raise IngestionError(
212+
ERR_INVALID_FORMAT,
213+
f"File too large: {file_size} bytes (limit: {MAX_FILE_SIZE_BYTES})",
214+
)
215+
216+
logger.info(
217+
"conversion_starting",
218+
operation="handle_file_task",
219+
path=file_path,
220+
file_size=file_size,
221+
file_extension=file_ext,
222+
)
223+
start = time_mod.monotonic()
190224

191225
try:
226+
# Get or recreate the process pool (recovers from crashed workers)
227+
pool = _get_executor()
228+
192229
# Schedule the task with a hard timeout managed by Pebble
193-
future = executor.schedule(
230+
future = pool.schedule(
194231
process_file_sync, args=[file_path], timeout=TIMEOUT_SECONDS
195232
)
196233

@@ -200,6 +237,15 @@ async def handle_file_task(file_path: str) -> list[dict]:
200237
if not result["content"].strip():
201238
raise IngestionError(ERR_EMPTY, "File contains no text")
202239

240+
elapsed_ms = (time_mod.monotonic() - start) * 1000
241+
logger.info(
242+
"conversion_completed",
243+
operation="handle_file_task",
244+
path=file_path,
245+
duration_ms=round(elapsed_ms, 1),
246+
content_length=len(result["content"]),
247+
)
248+
203249
return [
204250
{
205251
"content": result["content"],
@@ -212,20 +258,39 @@ async def handle_file_task(file_path: str) -> list[dict]:
212258
]
213259

214260
except (TimeoutError, pebble.ProcessExpired):
215-
logger.error("conversion_timeout_killed", path=file_path)
261+
elapsed_ms = (time_mod.monotonic() - start) * 1000
262+
logger.error(
263+
"conversion_timeout_killed",
264+
operation="handle_file_task",
265+
path=file_path,
266+
timeout_seconds=TIMEOUT_SECONDS,
267+
duration_ms=round(elapsed_ms, 1),
268+
)
216269
raise IngestionError(
217270
ERR_TIMEOUT, "Processing timed out and worker process was terminated"
218271
)
219272
except IngestionError:
220273
raise
221274
except Exception as e:
275+
elapsed_ms = (time_mod.monotonic() - start) * 1000
222276
# Check for wrapped exceptions
223277
err_msg = str(e).lower()
224278
if "timeout" in err_msg:
225-
logger.error("conversion_timeout_exception", path=file_path)
279+
logger.error(
280+
"conversion_timeout_exception",
281+
operation="handle_file_task",
282+
path=file_path,
283+
duration_ms=round(elapsed_ms, 1),
284+
)
226285
raise IngestionError(ERR_TIMEOUT, "Processing timed out")
227286

228-
logger.error("conversion_failed", path=file_path, error=str(e))
287+
logger.error(
288+
"conversion_failed",
289+
operation="handle_file_task",
290+
path=file_path,
291+
error=str(e),
292+
duration_ms=round(elapsed_ms, 1),
293+
)
229294
if "password" in err_msg or "encrypted" in err_msg:
230295
raise IngestionError(ERR_ENCRYPTED, "File is password protected")
231296
elif "format" in err_msg:

0 commit comments

Comments
 (0)