intent: the posting rewrite is one transaction - a refused line leaves the previous post standing (#7132) - #7167
Merged
Merged
Conversation
…s the previous post standing (#7132) The #7071 rewrite path deleted every stale item row, updated the header and then saved the derived lines, each store call its own transaction. A failure on the Nth line - now easy to reach, since #7069's required-value check throws a ValidationException from the item save - left the journal entry with a header and a partial set of lines. Unlike the half-post case there is no second event to self-heal from: the source has already reached its moment and raises nothing further, so the ledger ended up WORSE than the stale but balanced state the rewrite set out to fix. The handler's whole write phase now runs in one UnitOfWork.run(...) - the rows a rewrite replaces, the header, every derived line - as Generate.java.template has since #7069: either the post says what the source derives now, or it still says what it said before. Across STEPS the consistency model is unchanged and deliberately not transactional; a bad post is unwound by a correcting entry (reverses:), not a rollback. Mechanically this only hoists currentItems out of the refresh branch (both branches assign it once, so the block can close over it) and moves the delete/update/save into the block. The decision half - the cell-by-cell comparison, the redelivery no-op, the amendable guard - stays outside: it only reads, and it returns before anything is written. Covered by IntentEngineIT and IntentEmissionCoverageIT, both asserting by POSITION that the delete, the header write and the line writes sit inside the block - a header written outside it with the lines inside would still "mention UnitOfWork". IntentEmissionCoverageIT additionally compiles the generated handler with the real javac batch. Fixes #7132 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
delchev
added a commit
that referenced
this pull request
Sep 10, 2026
…eaves nothing (#7179) (#7245) Posts.java.template's per-item mode (posts: with forEach:) had the same multi-write shape PR #7167 fixed in the document-shaped posting and no unit of work: one save per derived row, one transaction each. A row the target repository refused - a validation, a constraint, a required column the derived row leaves null - left the rows before it durable. That is worse here than in the posting, because this mode's idempotency guard is coarser: it asks whether ANY row back-references the source, so the partial set read as a finished post and every redelivery afterwards was a no-op. The missing rows were never written by anything. The half-post was PERMANENT. The rows one source event derives are now mapped in memory first and written inside one UnitOfWork.run(...): all of them commit or none does, so a failed tick leaves nothing and the redelivery writes the whole set. That also makes the existing guard exact rather than needing to be replaced - with atomicity the back-reference is present for a whole post and absent for no post, and there is no partial set left for it to misread. The single-row mode keeps its one save: one repository call is already one transaction. Verification is a new IntentPostsAtomicityIT, the runtime half this mode never had - it publishes the app (which compiles the generated handler) and drives the sequence the issue describes: a goods issue whose second line carries no quantity derives a movement the target refuses, the ledger must hold NOTHING, the line is then repaired and the event redelivered, and the ledger must hold the WHOLE post. Against the unfixed template that run ends with exactly one durable row and stays there. IntentEngineIT covers the emission by POSITION (derivation before the unit, the sole save site inside it, no unit in the single-row mode) - the flat mode had no rendering coverage at all until now. Fixes #7179 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
The defect
The #7071 rewrite path in
Posting.java.templatedeleted every stale item row, updated the header and then saved the derived lines - each store call its own transaction. A failure on the Nth line (now easy to reach: #7069's required-value check throws aValidationExceptionfrom the itemsave) left the journal entry with a header and a partial set of lines.Unlike the header/items half-post case there is no guaranteed second event to self-heal from - the source has already reached its moment and raises nothing further - so the ledger ended up worse than the stale but balanced state the rewrite set out to fix.
The fix
The handler's whole write phase runs in one
UnitOfWork.run(...), asGenerate.java.templatehas since #7069: the rows a rewrite replaces, the header and every derived line either all become durable or none of them does. Either the post says what the source derives now, or it still says what it said before.Mechanically it is small:
currentItemsis hoisted out of therefreshbranch (both branches assign it exactly once, so the block can close over it) and the delete / update-or-save / line-save move into the block. The decision half stays outside on purpose - the cell-by-cell comparison, the redelivery no-op and the amendable guard only read, and they return before anything is written.Across steps the consistency model is unchanged and deliberately not transactional: the source's own commit, this handler's post and a reversal are separate events, and a bad post is unwound by a correcting entry (
reverses:), not a rollback. The prose that claimed "no cross-step transaction" as a property of the post itself is corrected in the template header, the module guide and the docs index.Verification
IntentEngineIT- 71/71 green. Asserts by position that the stale-row delete, the header write and the line writes all sit inside the block; a header written outside it with the lines inside would still "mention UnitOfWork".IntentEmissionCoverageIT- green. Same positional assertions, and it compiles the generated handler with the realjavacbatch, which is what proves the lambda's captures are legal.mvn formatter:validateclean.Docs: dirigible-io/dirigible-io.github.io#239.
Noted, not fixed here
Posts.java.template's per-item mode has the same shape and is arguably worse: it writes N flat rows, one transaction each, and its idempotency guard is "any row back-references this source" - so a failure on row 3 leaves rows 1-2 and every redelivery afterwards is a no-op, making the half-post permanent. Out of scope for this issue, which names the posting rewrite; worth its own.Fixes #7132
🤖 Generated with Claude Code