Repository navigation
🐛 sphinx-needs: under -j N, a need id defined in two workers renders its card once, on the document that kept it - #2106
Merged
Conversation
…its card once (#2087) The parallel variants fail at this commit: both pages carry the card, the assembled singlehtml and latex documents carry it twice, and needextract copies the losing directive's content. The serial variants, the included-file pin and the upgrade-safety pin hold already.
…om its pickled doctree Sphinx does not rewrite the loser's page when only the winner's document changed, so the pin deletes that page's output: the stale node in the pickled doctree is then rendered against the re-created need.
…its card once (#2087) A need node now carries the docname of the need it renders, and format_need_nodes drops a node whose need the merge recorded on another document: the loser of a duplicate id read by a different worker. The decision reads the node's own tag, not the name doctree-resolved passes (the root document for singlehtml and latex) nor node.source. A node without the tag, from a doctree pickled by an earlier release, renders as before. merge_data no longer lets a worker's copy of a need node replace the node of an id the environment already holds, so needextract copies the content of the need that was kept. The doctree snapshot of test_basic_doc gains the docname attribute on its two Need nodes.
…ent losing an assembled build (#2087) A need created through add_need with docname=None that the merge keeps must still drop the other document's copy; that variant fails at this commit (both cards render). The root document as the loser in singlehtml and latex is pinned deterministically: root_doc sorts last, so it is read after the other definition serially and in the second chunk under -j 2.
…node renders (#2087) The rule also required the kept need to have a docname, which kept the other document's copy when the kept need was created through add_need with docname=None. A need's own node always carries its need's docname, so the tag compared with it is enough. The comments now say an untagged node comes from a doctree pickled before this change, and that a hidden need has no node in the registry.
…d tests overlap and merge (#2087) Sphinx merges a worker that has already finished before it forks the next one, so on a loaded machine the first chunk could be merged before the second was forked: the second worker then refused page_b's directive as a serial build does, and every -j 2 case failed on the warning. Each -j 2 project's conf.py now holds a file barrier on source-read and env-merge-info: the first chunk cannot finish before the second is being read, and the chunk that is to merge second waits for the other's merge. A wait not met within a minute fails the build. With the merge order fixed, the main html case and the docname-less case run both orders: page_b's chunk merged first keeps page_b's need, and for the docname-less case pins the known two-card gap. The sleep and the skip are gone.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2106 +/- ##
=======================================
Coverage 92.38% 92.39%
=======================================
Files 136 136
Lines 19309 19320 +11
=======================================
+ Hits 17839 17850 +11
Misses 1470 1470
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Neednode now carries the docname of the need it renders (node["docname"], set in_create_need_node).format_need_nodesdrops a node whose need is recorded on a different document, before thehidebranch, with no new warning (the merge already warnedneeds.duplicate_id). The node's tag alone decides, so a kept need created withadd_need(…, docname=None)still drops the other document's copy. A node without the tag (a doctree pickled before this change) renders as before.merge_datano longer lets a worker's copy of a need node replace the node of an id the environment already holds, soneedextractcopies the content of the need that was kept.Why
Serially, a second directive with an existing id is refused (
needs.create_need) and returns no node. Under-j N, two documents read by different workers each create the need; the merge keeps the first one merged and warnsneeds.duplicate_idabout the other, but the losing document's doctree still held its node, andformat_need_nodesrendered everyNeednode fromneeds[id]: the losing page showed a card with the kept need's title and fields over the dropped directive's content, and the page carried a secondid="<ID>". Reproduced 3/3 with plainreqdirectives (indexandpage_b, read in chunksindex, pad_0, pad_1 | pad_2, pad_3, page_b). Separately, the merge's node registry was a plaindict.update, so aneedextractof the id copied the loser's content (5/5).The decision reads the node's own tag, not the document name
doctree-resolvedpasses (forsinglehtmlandlatexthat is the root document, for the one assembled tree) and notnode.source(not a reliable statement of the node's document). The node is replaced before any writer sees it, so the attribute reaches no output; the doctree snapshot oftest_basic_docgains it on its twoNeednodes. No environment-version bump: the currentENV_DATA_VERSIONis itself unreleased, so every upgrade from a release re-reads everything anyway.A related pre-existing problem found on the way is filed separately as #2100: deleting the document that kept the need drops the id from
needs.jsonand crashes the next write of the loser's page.ubCode parity
Nothing to match in this PR: ubCode's needextract already shows the kept need's content (
rust/ubc_parser_ctrl/src/extract_cards.rs:1246-1249). For need directives ubCode differs in the other direction: it renders a card at every directive site of a duplicate id (the demoted registration is hydrated by id with the winner's data,rust/ubc_parser_ctrl/src/hydration.rs:2538, and its anchor minted from the node,rust/ubc_ast/src/render_html/special.rs~1545), recorded there as a "registered residual". Filed as useblocks/ubcode#3893.Tests
New
tests/test_parallel_duplicate_id.py, every case serial and-j 2(each-j 2project has six documents, so Sphinx 7 reads it in parallel too; the-j 2variants skip whereparallel_availableis False). Each-j 2project'sconf.pyholds a small file barrier onsource-readandenv-merge-info, so its two chunks are always read concurrently and merged in a chosen order: without it, Sphinx can merge the first chunk before forking the second, which then refuses the duplicate as a serial build does (reproduced with a delay between Sphinx'sadd_taskcalls; the barrier fails loudly after 60 s rather than hanging). The assertions take the winner from the warning:html), with either document's chunk merged first;singlehtmlandlatex: one card in the assembled document, and a need defined only onpage_bstill renders, the case a rule on thedoctree-resolveddocname fails; and the root document as the loser (root_doc = "zz_root", so it sorts last);add_needwith no docname: the other document's copy is still dropped (and, merged in the other order, the known unwarned two-card case of a docname-less loser, pinned as it is);needextractshows the kept need's content;-j 2rebuilds, touching the winner (the loser's page written from its pickled doctree) or the loser (refused as in serial): one card after each;included fragment renders on the including page;The parallel cases fail before the fix and pass after it.
test_parallel_execution.py,test_needs_builder.pyandtest_complex_doc.pypass, and none of them changed.Closes #2087