Skip to content

fix(bpm): report the step an instance is parked on while an async retry is pending (#7193) - #7212

Merged
delchev merged 1 commit into
masterfrom
fix-7193-active-activity-ids
Sep 9, 2026
Merged

fix(bpm): report the step an instance is parked on while an async retry is pending (#7193)#7212
delchev merged 1 commit into
masterfrom
fix-7193-active-activity-ids

Conversation

@iliyan-velichkov

Copy link
Copy Markdown
Contributor

Fixes #7193.

Root cause

Flowable's JobRetryCmd calls executionEntity.setActive(false) when it moves a failed asynchronous job to the timer-job table, and FindActiveActivityIdsCmd collects only activity ids of executions whose isActive() is true. So from the first failed attempt on, the execution is inactive and the pending job is the only remaining evidence of where the instance sits - while getProcessInstanceActiveActivityIds queried nothing but dead-letter jobs, which it maps to negative.

Because BpmnIntentGenerator emits its steps flowable:async, an intent-generated process therefore answered "no step is running" for its whole life: nothing for the Processes viewer or the Monitoring diagram to overlay, and no way for an application to tell its own users which step a record's process is on.

The change

Executable and timer jobs now count towards positive alongside the active executions, merged by the higher of the two counts rather than by their sum. That matters: ContinueProcessOperation marks the execution active before it creates the async job, and a boundary event's own child execution stays active beside its timer job - so summing would render every pending asynchronous step, and every boundary timer, as a "2" badge on both diagram overlays. Dead-letter jobs stay negative, exactly as before, and a job bound to no flow element (an asynchronous variable write, a process-instance migration) no longer becomes a literal "null" key in the answer.

Two sites sharing the same root cause come with it:

  • getProcessInstanceImage highlighted from the same getActiveActivityIds call, so GET /bpm-processes/diagram/instance/{id} had nothing to mark either.
  • The Monitoring shell's Retry action - moveDeadLetterJobToExecutableJob hands the job back to the executor without reactivating the execution, so a click made both badges disappear and the step looked finished.

And ProcessInstanceData.activityId (the issue's second symptom, "activityId": null on GET /bpm-processes/instances) is resolved the same way instead of read off the root process-instance execution, where Flowable always leaves it unset. The field is singular, so it carries the sole occupied activity and stays null for a fan-out, where /active is the answer.

Deliberately out of scope: createSuspendedJobQuery() (suspension moves both job tables, but the platform exposes no suspend/activate path, so no instance is ever in that state) and getProcessDefinitionActiveActivityIds, which queries executions without the isActive filter and therefore already reports parked steps.

One knowing limitation, noted in a code comment rather than passed over: max undercounts if a single element ever hosts both a waiting token and a parked one - an asynchronous task behind a loop or a fan-in. Counting exactly would take a per-activity execution query on an endpoint both UIs poll.

Tests

BpmProviderFlowableActiveActivitiesTest drives a real in-memory Flowable engine with the async executor off, so there are no threads and no sleeps: ManagementService#executeJob runs the attempt on the calling thread and Flowable's own failed-job listener then applies the retry decision in its own transaction. Five states: a pending job (guarding the double count), parked between retry attempts, dead-lettered, retried out of the dead letter, and a plain wait state.

BpmActiveActivitiesIT publishes a throwing client delegate on a flowable:async step with an R5/PT1H cycle (load-bearing - without a cycle the executor dead-letters the job within half a minute and the state under test would expire mid-test), waits for the parked timer job, and asserts that job's exception message carries the delegate's own text, so the test cannot pass on a job that never resolved the compiled class. It then asserts both HTTP answers. Untagged, so it runs in the per-PR smoke gate; ~21 s.

Both suites were confirmed to fail without the production change and pass with it. Also green locally: the module's full unit suite, and JavaBpmnIT / BpmTaskLabelKeyIT / JavaDelegateInjectionIT / BpmnModelApiIT (the integration tests exercising the changed instance mapping). formatter:validate is clean and the -P release javadoc build reports no errors.

🤖 Generated with Claude Code

…ry is pending (#7193)

`getProcessInstanceActiveActivityIds` - and so
`GET /services/bpm/bpm-processes/instance/{id}/active` - answered `{}` for a
live instance waiting on an asynchronous job that had already failed once.
Flowable's `JobRetryCmd` deactivates the execution when it moves the failed job
to the timer-job table, and `getActiveActivityIds` collects active executions
only, so the position was evidenced by the job alone - while the method's only
job query was for dead-letter jobs. Since `BpmnIntentGenerator` emits its steps
`flowable:async`, an intent-generated process read as "no step is running" for
its whole life: nothing for the Processes viewer or the Monitoring diagram to
overlay, and no way for an application to tell its users which step a record is
on.

Executable and timer jobs now count towards `positive` alongside the active
executions, merged by the higher of the two counts rather than their sum (a
token that has not failed yet is active and job-bearing, so adding them would
report every pending step twice). Dead-letter jobs stay `negative`, and a job
bound to no flow element no longer becomes a literal "null" key.

Two sites sharing the root cause come with it: the instance diagram PNG
highlights the occupied activities instead of only the active ones, and the
Monitoring Retry action - which hands a dead-lettered job back to the executor
without reactivating the execution - stops making the step look finished.
`ProcessInstanceData.activityId` is resolved the same way instead of read off
the root process-instance execution, where Flowable always leaves it unset.

Covered by `BpmProviderFlowableActiveActivitiesTest`, which drives a real
in-memory engine synchronously through pending, parked, dead-lettered and
retried states, and by `BpmActiveActivitiesIT`, which parks a published
`flowable:async` step between retry attempts and asserts both HTTP answers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@iliyan-velichkov iliyan-velichkov self-assigned this Sep 9, 2026
@delchev
delchev merged commit bf025b7 into master Sep 9, 2026
10 checks passed
@delchev
delchev deleted the fix-7193-active-activity-ids branch September 9, 2026 07:45
delchev added a commit that referenced this pull request Sep 10, 2026
…ies (#7266)

#7212 made the listing resolve each instance's activity id instead of
reading the always-null one off the instance, at three queries apiece -
an execution query and the two job queries. The Monitoring shell's
Overview polls the listing with no key and no limit every 30 s, so a
tenant with a few hundred running instances paid 3N statements a poll
for a column the listing renders as one badge.

The resolution is now done for the whole listing at once: one execution
query over all the listed ids, and the two job queries over the current
tenant - Flowable's job queries take a single process-instance id, so
the tenant is the narrowest batch filter there is - grouped by process
instance in memory. The evidence and the answer are unchanged, down to a
step parked between retry attempts that only its own timer job still
names; the query count no longer grows with the listing, which the new
engine test pins by reading H2's own query statistics.

The single-instance path keeps its targeted queries.

Also: the two in-memory engine tests meant DB_CLOSE_DELAY=-1, not 1000.

Fixes #7250

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

getProcessInstanceActiveActivityIds returns an empty map while an async job is pending, so no intent-generated process can report its current step

2 participants