Fix parallel crafting jobs all completing on a single produced output - #226
Fix parallel crafting jobs all completing on a single produced output#226rubensworks wants to merge 1 commit into
Conversation
Every crafting interface registers its own insert pre-consumer on the network's ingredient channel, and all of them observe the same insertion. Because a pending crafting job output is resolved without consuming the instance, the instance passed on to the next pre-consumer unchanged, so one produced item resolved the pending output of every parallel job at once. Those jobs then reported completion before their outputs were actually in storage. As soon as the last of them left the crafting network, a crafting writer saw neither a running job nor enough of the requested ingredient, and scheduled the whole job all over again. This showed up as a rare failure of testItemsCraftIronIngotsParallelFromDepPartial, which crafted 10 iron ingots instead of 5, but it applies to any parallelised crafting job. The observer now only resolves pending outputs with the part of an insertion that no other crafting interface has accounted for yet. Crafting results that a crafting interface first offers to its own jobs carry that accounting along into the network insertion, so they can not be accounted for a second time there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EypmxuPjaJ4cFGh2DvZYbf
|
CI (
Two
With both applied locally, Generated by Claude Code |
Important
Depends on CyclopsMC/IntegratedDynamics#1720 and does not compile until that is merged and published. Two dependency bumps in
gradle.propertiesare deliberately left out of this PR, because the Integrated Dynamics build number does not exist yet:integrateddynamics_versionto the build produced by that PRneoforge_versionto at least21.1.228, which that branch of Integrated Dynamics requiresBoth were applied locally to run everything below.
What was going wrong
integratedcrafting:gametestsitemscraft_testitemscraftironingotsparallelfromdeppartialfailed rarely and randomly. Reproduced by running 40 copies of it per game test run, over 12 runs: 1 failure in 480, with the chest ending oninstead of the expected 6 raw iron and 5 iron ingots. It crafted a whole second batch of 5 ingots.
Root cause
Every crafting interface registers its own
PendingCraftingJobResultIndexObserveras an insert pre-consumer, and all of them observe the same insertion. Resolving a pending crafting job output does not consume the instance (it still has to end up in storage), so the instance was passed on to the next pre-consumer unchanged, and every one of them resolved its own job's pending output from that one item.In this test that means the first iron ingot returned by any of the five furnaces marked all five parallel jobs as finished. Those jobs then left the crafting network within an update interval, while the other four ingots were still on their way, and
CraftingAspectWriteBuilders.PROP_CRAFTre-triggers as soon as it sees neither a running job nor enough of the requested ingredient in storage. So it scheduled the job again, which ate the leftover raw iron.It is a race only because the remaining ingots normally arrive before the last job is removed from the network. The over-crafting itself is not specific to this test: it applies to any parallelised crafting job.
Fix
PendingCraftingJobResultIndexObservernow only resolves pending outputs with the part of an insertion that no other crafting interface has accounted for yet, using the pre-consumer API added in CyclopsMC/IntegratedDynamics#1720.PartTypeInterfaceCraftingBaseoffers a crafting result to its own jobs before flushing it into the network, soflushIngredientToNetworknow carries that accounting into the network insertion as well, otherwise the same double-accounting would happen on that path.Testing
TestPendingCraftingJobResultIndexObserverruns two crafting interfaces with a parallel job each through the real pre-consumer chain and asserts one produced output resolves exactly one of them, while still ending up in storage. It fails on the code before this fix ("the output does not resolve the second job: Expected: <1> but: was <0>") and passes after../gradlew buildpasses../gradlew runGameTestServerpasses, all 62 tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01EypmxuPjaJ4cFGh2DvZYbf
Generated by Claude Code