Skip to content

Fix nondeterministic PDF markdown with batch_size > 1 (#2319) - #2320

Open
Kylinny wants to merge 1 commit into
unclecode:mainfrom
Kylinny:fix-pdf-page-number-race-2319
Open

Kylinny wants to merge 1 commit into
unclecode:mainfrom
Kylinny:fix-pdf-page-number-race-2319

Conversation

@Kylinny

@Kylinny Kylinny commented Sep 30, 2026

Copy link
Copy Markdown

Fixes #2319.

Hi, and thank you @rtorrero-lw for the excellent report — the diagnosis and local repro made the root cause easy to confirm.

Bug

With the default batch_size=4, NaivePDFProcessorStrategy.process_batch() produced different markdown for the same PDF on different runs. In the issue's repro (12 pages, each with a Title Case line that clean_pdf_text() only bolds on page 1), 30 runs gave 5 distinct outputs locally (bolding landed on page 1, on no page, or on other pages); batch_size=1 was always stable.

Root cause

Each worker thread wrote its page number to the shared self.current_page_number attribute and _process_page() read it back after text extraction — by which time another worker could have overwritten it. The same stale value also fed PDFPage.page_number and the extracted image filenames (page_{n}_img_{m}, which could even collide across threads).

Fix

Pass the page number down explicitly instead of storing it on self, exactly as suggested in the report: _process_page(page, image_dir, page_num + 1) and _extract_images(page, image_dir, page_number). The now-unused current_page_number attribute is removed. Invariants preserved:

  • 1-based page numbering is unchanged on both the serial process() and batched process_batch() paths (verified: both produce identical per-page markdown/html).
  • clean_pdf_text() / clean_pdf_text_to_html() signatures are untouched; the page-1-only formatting now provably applies only to page 1.
  • Remaining self reads inside worker threads (extract_images, save_images_locally, …) are all read-only after __init__.

Tests

New tests/unit/test_pdf_page_number_race.py (hand-crafted multi-page PDFs, no new test dependencies):

  • test_process_batch_is_deterministic — 10 runs with batch_size=4 give byte-identical output
  • test_page_one_only_formatting_in_batch — author-line bolding appears on page 1 and only page 1
  • test_page_numbers_sequential_in_batch — PDFPage.page_number matches page position under threads
  • test_process_page_uses_given_page_number (parametrized) — _process_page honors its argument
  • test_process_and_process_batch_agree — serial/batch parity

All 7 new cases fail on the unfixed code and pass with the fix; the existing test_pdf_html_escaping.py PDF tests still pass.

Before / after (issue's repro, 30 runs, batch_size=4)

  • Before: 5 distinct outputs over 30 runs (bolding on pages [1], [], [1, 2], [1, 4], [1, 6])
  • After: 1 distinct output over 30 runs — bolding on page 1 only, every time

Happy to adjust anything on review. Thanks again for the great catch!

Pass the page number explicitly to _process_page() and _extract_images()
instead of sharing it through self.current_page_number, which worker
threads in process_batch() raced on. The race misapplied page-1-only
formatting, corrupted PDFPage.page_number, and could collide extracted
image filenames across pages.

Add regression tests covering batch determinism, page-1-only formatting,
sequential page numbering, and serial/batch parity.

Signed-off-by: Kylinny <j.yao@wustl.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Same PDF gives different markdown on each run when batch_size > 1 (threads share current_page_number)

1 participant