intent: a gated checks: refusal crosses the delegate that does the posting (#7371) - #7419
Merged
Merged
Conversation
…sting (#7371) The walk that takes the async boundary off everything between a completing user task and a check-gated status setter stopped at an authored service task, on the reasoning that its own completion is what the person waited for. Measured on base-inventory's six posting flows - userTask -> decision -> custom posting delegate -> gated setter - that boundary commits the completion and the gate then refuses a detached job: 200 with an empty body, the task consumed, the document still DRAFT, and once the job's retries are exhausted an instance stranded with no task in anyone's Inbox and no way back short of an administrator retriggering the dead letter. So the walk now crosses authored service tasks as well, and the flowable:class delegate path - which hard-coded flowable:async - honours the flag like the ${JavaTask} one. The whole stretch runs in the completing transaction: the step's own work rolls back with the refusal and the 400 carries the authored message out of the task completion. It still stops where the transaction cannot span: a second user task or a wait is its own wait state, and a step declaring retry: keeps its boundary because a Flowable failed-job retry cycle re-runs a JOB and there is no job without one. (An onError: route needs none - IntentStepResilience converts a synchronous first-and-final failure exactly as it does an exhausted asynchronous one.) A gate reachable only across a retrying step is therefore still a background incident, and the generator now says so in a WARN naming the gate, the task and the two ways out, rather than leaving it invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines
+607
to
+611
| LOGGER.warn( | ||
| "Process [{}]: the check-gated status write of step [{}] is reachable from user task [{}] only across a step declaring `retry:`," | ||
| + " whose async boundary commits the task completion - a refusal there will dead-letter instead of reaching the person who acted." | ||
| + " Drop the `retry:` from that step, or move it off the route between the task and the gate.", | ||
| process.getName(), gate, userTask.getName()); |
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 cause
completingTransactionNodeswalks back from a check-gated status setter to the user tasks thatreach it and strips the
flowable:asyncboundary off everything on the way, so the gate'sValidationExceptionis raised in the completing thread and leaves as a 400. The walk stopped atany step that was not a
decision- an authored service task in between was treated as realbackground work whose completion nobody waits on.
Measured, that reading is wrong for the step that sits between the decision and the setter.
base-inventory's six posting flows areuserTask post -> decision -> apply (custom delegate) -> activate (gated setter):apply's boundary commits the completion, the setter then runs as adetached job, and the gate refuses that. The poster gets
200with an empty body, the task isconsumed, the document is still DRAFT, and once the job's retries are exhausted the instance is
stranded -
activityId: null, no task in anyone's Inbox, no way back short of an administratorretriggering the dead letter.
The
flowable:classemission path (appendDelegateServiceTask, the one adelegate:step takes)also hard-coded
flowable:async, so even a marked node would have kept its boundary there.The change
gateReachingStepscrosses authoredserviceTasksteps too, via a newjoinsTheCompletingTransaction. The whole stretch from the completing task to the gate runs inone transaction: the step's own work rolls back with the refusal, and the 400 carries the
authored message out of
POST /services/inbox/tasks/<id>.appendDelegateServiceTaskhonours theasyncflag like the${JavaTask}path already did.waitis its ownwait state,
endis the end, and a step declaringretry:keeps its boundary - a Flowablefailed-job retry cycle re-runs a JOB and there is no job without one, so crossing it would
silently drop the declared re-attempts. (An
onError:alone needs no job:IntentStepResilienceconverts a synchronous first-and-final failure exactly as it does an exhausted asynchronous one,
so a step that only routes its failure is crossed like any other.)
one case this cannot repair. Nothing about that is visible in the generated output, so the
generator now logs a WARN naming the process, the gate, the user task and the two ways out
(drop the
retry:, or move the retrying work off the route), rather than leaving it silent.This is the issue's option 3, narrowed to the shape option 1 cannot reach.
Not changed: the
transitions[]path (already structurally correct - the endpoint writes thecolumn in the request thread), the user-task-owned
setRelationFieldshape (#7014, alreadysynchronous), and the response mapping in
BpmInboxEndpoint(aValidationExceptionin the causechain already becomes 400 with the message as the body - it simply never got one on this shape).
Verification
IntentCheckGateAcrossADelegateIT- the reported flow, reduced, driven end to endthrough the real engine:
post->postDecision->apply(a clientJavaDelegatebound withdelegate:) ->activate(gateditemsMin). Asserts the outermost facts, not the XML: thecompletion answers 400 with the authored message, the stored
Statusis still 1, the task isstill the poster's (same task id), the very same completion then succeeds once a line is
added, and the ungated
cancelarm of the same decision still completes with no line at all.Confirmed to fail without the generator change: reverted
BpmnIntentGenerator.javatoorigin/master, rebuilt, re-ran -Expected status code <400> but was <200>, which is theissue's symptom verbatim.
IntentCheckGateRefusalIT(the intent: a checks: refusal after Approve parks the process in dead-letter - the message reaches Monitoring, not the user, and only an admin can retry #7063/intent: a gate reachable through two decision routes keeps its async boundary on the second route #7139 shapes) green on the fixed build.IntentEngineIT(81) +IntentEmissionCoverageIT(1) green - the emission and resiliencecoverage over the shapes this walk touches.
engine-intentunit suite green (143 test classes).CheckGateBpmnTestgained three cases andflipped one:
authoredWorkBetweenTheTaskAndTheGate...now asserts the synchronous emission withthe issue's rationale, plus base-inventory's decision+
flowable:classshape and the retrying-stepcontrol that must keep its boundary.
mvn formatter:validategreen with the formatter cache wiped.Not run: the full integration suite, and no fleet regen -
base-inventoryneeds a pin bump and aregen to pick this up.
Fixes #7371
🤖 Generated with Claude Code