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
Bug
ThreadedDoclingParseDocumentBackendbuilds itsdoc_keyviaDoclingThreadedPdfParser.load(),which hashes the input from the stream's current position:
When a
page_rangeis requested,_resolve_threaded_page_numbers()first opens the sameBytesIOwith pypdfium2 to read the page count and never rewinds it. By the timeparser.load()runs, the stream is already advanced, so the key is computed over asuffix of the document rather than the whole document.
Reproduction
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 viaPageParseResult.doc_key:DoclingThreadedPdfParseris documented as "a threaded PDF parser that decodes pages frommultiple 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 collideon the same key.
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 constructionacross concurrently-converted documents for no reason other than reading an integer that
docling-parse can report itself (
PdfDocument.number_of_pages()).Environment