Skip to content

Threaded docling-parse backend derives its document key from a partially-read stream #3894

Description

@DanielNg0729

Bug

ThreadedDoclingParseDocumentBackend builds its doc_key via DoclingThreadedPdfParser.load(),
which hashes the input from the stream's current position:

# docling_parse/pdf_parser.py
hasher = hashlib.sha256(usedforsecurity=False)
while chunk := path_or_stream.read(8192):   # reads from the CURRENT offset
    hasher.update(chunk)
path_or_stream.seek(0)
key = f"key={hasher.hexdigest()}"

When a page_range is requested, _resolve_threaded_page_numbers() first opens the same
BytesIO with pypdfium2 to read the page count and never rewinds it. By the time
parser.load() runs, the stream is already advanced, so the key is computed over a
suffix of the document rather than the whole document.

Reproduction

from io import BytesIO
from docling.backend.docling_parse_backend import ThreadedDoclingParseDocumentBackend
from docling.datamodel.base_models import InputFormat
from docling.datamodel.document import InputDocument
from docling.datamodel.settings import DocumentLimits

stream = BytesIO(Path("tests/data/pdf/sources/2206.01062.pdf").read_bytes())
InputDocument(
    path_or_stream=stream,
    format=InputFormat.PDF,
    backend=ThreadedDoclingParseDocumentBackend,
    filename="2206.01062.pdf",
    limits=DocumentLimits(page_range=(2, 3)),
)
# the stream handed to parser.load() is at offset 4_299_973, not 0

Impact

The document content itself is still correct — the C++ loader seeks to 0 before reading —
so this is currently a latent defect rather than a visible corruption. It matters because
the key is the identity used for page_count(), unload(), and result routing via
PageParseResult.doc_key:

  • DoclingThreadedPdfParser is documented as "a threaded PDF parser that decodes pages from
    multiple documents in parallel"
    . Today docling creates one parser per document, so a
    collision cannot surface. Once a parser instance is shared across documents, two streams
    positioned at EOF both hash to e3b0c44298fc1c14… (the SHA-256 of empty input) and collide
    on the same key.
  • The same document keys differently depending on the offset it happened to be left at,
    which makes the key useless as a cache or dedup identity.

Secondary observation

The page-count probe is the only remaining use of pypdfium2 in the threaded docling-parse
path, and it takes the module-global pypdfium2_lock. That serialises backend construction
across concurrently-converted documents for no reason other than reading an integer that
docling-parse can report itself (PdfDocument.number_of_pages()).

Environment

  • docling 2.113.0
  • docling-parse 7.8.1
  • python 3.13

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions