From 7ec116a3c7013706a8207be1e9e8cd582902f00b Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 4 Jun 2026 21:57:55 +0200 Subject: [PATCH] fix(review): close doc via try/finally; don't count parse failures as indexed Address PR #852 review: - pymupdf.py: the metadata `doc` was only closed on the PdfParseFailed and success paths, so a failure in `_extract_metadata`/`mkdir`/`get_settings` leaked it. `doc` is only needed for metadata + page_count (the heavy parse works from `content` bytes in the worker), so open it, read metadata, and close it immediately under try/finally; drop the two later doc.close() calls. - processor.py: a permanent parse failure early-returned from `_index_document`, after which `process_document` still recorded record_qdrant_operation("upsert", "success") + record_vector_sync_processing(success) -- counting an OOM/timeout bomb as astrolabe_documents_indexed_total{status="success"}. `_index_document` now returns False on that path and the caller skips the success metrics (the failure is already recorded via document_parse_failed_total + the registry's document_parse_total{error}). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../document_processors/pymupdf.py | 19 ++++++++++--------- nextcloud_mcp_server/vector/processor.py | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/nextcloud_mcp_server/document_processors/pymupdf.py b/nextcloud_mcp_server/document_processors/pymupdf.py index 722fad40..d1262b02 100644 --- a/nextcloud_mcp_server/document_processors/pymupdf.py +++ b/nextcloud_mcp_server/document_processors/pymupdf.py @@ -107,14 +107,19 @@ class PyMuPDFProcessor(DocumentProcessor): if progress_callback: await progress_callback(0, 100, "Opening PDF document") - # Open document and extract metadata in thread + # Open document only to read metadata + page count, then close it + # immediately (try/finally so a failure in _extract_metadata can't + # leak it). The heavy extraction below works from ``content`` bytes + # in the isolated worker, so ``doc`` is not needed past this point. doc = await anyio.to_thread.run_sync( # type: ignore[attr-defined] lambda: pymupdf.open("pdf", content) ) - - metadata = self._extract_metadata(doc, filename) - metadata["file_size"] = len(content) - page_count = doc.page_count + try: + metadata = self._extract_metadata(doc, filename) + metadata["file_size"] = len(content) + page_count = doc.page_count + finally: + doc.close() if progress_callback: await progress_callback(10, 100, f"Extracting {page_count} pages") @@ -143,7 +148,6 @@ class PyMuPDFProcessor(DocumentProcessor): mem_limit_mb=settings.document_parse_mem_limit_mb, ) except PdfParseFailed as exc: - doc.close() logger.warning( "Isolated PDF parse failed for %s (reason=%s): %s", filename or "", @@ -198,9 +202,6 @@ class PyMuPDFProcessor(DocumentProcessor): metadata["image_paths"] = image_paths metadata["page_boundaries"] = page_boundaries - # Close document - doc.close() - if progress_callback: await progress_callback(100, 100, "Processing complete") diff --git a/nextcloud_mcp_server/vector/processor.py b/nextcloud_mcp_server/vector/processor.py index cd2b329b..47138344 100644 --- a/nextcloud_mcp_server/vector/processor.py +++ b/nextcloud_mcp_server/vector/processor.py @@ -237,7 +237,15 @@ async def process_document( for attempt in range(max_retries): try: - await _index_document(doc_task, nc_client, qdrant_client) + indexed = await _index_document(doc_task, nc_client, qdrant_client) + + # A permanent parse failure returns False: it was already + # recorded (document_parse_failed_total + the registry's + # document_parse_total{error}) and the placeholder marked + # "failed". It is not an indexing event and not retryable, so + # don't count it as a successful upsert/indexed document. + if indexed is False: + return # Record successful processing metrics duration = time.time() - start_time @@ -534,8 +542,8 @@ async def _index_document( # on a pathological PDF) returns success=False rather than # raising -- there is nothing to index and retrying would just # fail again. Mark the placeholder "failed" so the scanner stops - # re-queuing it (until the file changes) and return without - # indexing empty content. + # re-queuing it (until the file changes) and return False so the + # caller skips the success metrics (it was not indexed). if not result.success: reason = result.metadata.get("parse_failed_reason", "error") record_document_parse_failed(reason) @@ -561,7 +569,7 @@ async def _index_document( doc_task.doc_id, exc_info=True, ) - return + return False content = result.text file_metadata = result.metadata