Skip to content

cancel() on a WAITING instance clears instance metadata before onWorkflowCancelled is published #1749

Description

@edeandrea

What happened

When WorkflowInstance.cancel() (or cancelFuture()) is called on an instance that is WAITING on a listen task, WorkflowMutableInstance closes and clears the instance metadata (additionalObjects) before it publishes onWorkflowCancelled. A WorkflowExecutionListener that keeps per-instance state in metadata (addMetadataIfAbsent / findMetadata) therefore can't find that state when the cancelled event arrives.

This is how it surfaced in quarkus-flow's OpenTelemetry listener: the workflow.execute span of the cancelled instance was never ended or exported (quarkiverse/quarkus-flow#1058).

Why (7.35.2.Final, unchanged on main)

  1. cancel() -> internalCancel() sets status to CANCELLED and, outside the lock, calls toCancel.forEach(t -> t.cancel(true)). For a WAITING instance that is the future ListenExecutor registered through addCancelable.
  2. Cancelling that future completes the start pipeline right away on the calling thread: AbstractTaskExecutor.handleException publishes onTaskCancelled, WorkflowMutableInstance.handleException sees the CancellationException and publishes nothing, then .whenComplete(this::cleanUp) closes every AutoCloseable in additionalObjects and clears the map.
  3. Only after internalCancel() returns does cancel() call publishStatusChange(..., CANCELLED) and then onWorkflowCancelled. By then findMetadata returns Optional.empty().

For an instance cancelled while RUNNING (e.g. during a wait) the order is the opposite: cancelCheck fails the next task later, so onWorkflowCancelled is published before cleanUp. So whether listeners can see metadata on cancel depends on what the instance was doing when it was cancelled.

Reproducer

https://github.com/edeandrea/quarkus-flow-reproducers (issue1058). In short:

var instance = definition.instance(input);   // workflow: a single listen task for an event that never comes
instance.start();
await().until(() -> instance.status() == WorkflowStatus.WAITING);
instance.cancel();
// a listener's onWorkflowCancelled(ev): ev.workflowContext().instanceData().findMetadata(KEY, ...) is empty

Expected

Instance metadata stays available until the terminal lifecycle event (onWorkflowCompleted / onWorkflowFailed / onWorkflowCancelled) has been published, for every cancel path, the same way it already is for completion and failure.

Possible fix

Either of these, both inside WorkflowMutableInstance:

  • Publish before cancelling the futures: in cancel() / cancelFuture(), publish the status change and onWorkflowCancelled first, then cancel the collected cancelables (e.g. internalCancel() returns the futures to cancel and the caller cancels them after publishEvent(...) completes).
  • Publish from the pipeline: have handleException publish onWorkflowCancelled when it sees a CancellationException, so it runs before whenComplete(this::cleanUp) on every path, and stop publishing it from cancel() / cancelFuture(). This also covers the RUNNING path consistently, but cancelFuture() would then need to complete only once the pipeline has published.

Happy to send a PR for whichever you prefer.

Downstream workaround

quarkus-flow is working around it by ending the span from its metadata object's close() when the instance status is already CANCELLED (quarkiverse/quarkus-flow#1058). That workaround could be removed once this is fixed upstream.

Activity

  1. added 9 commits that reference this issue on Oct 8, 2026
    b064913
    5fbf2a0
    b695725
    0982b96
    6e056c1
    4dcd56a
    7e3e36d
    860bacc
    aa1ebc0
  2. added a commit that references this issue on Oct 8, 2026
    c8f334a
  3. added theissue type on Oct 8, 2026
  4. added 2 commits that reference this issue on Oct 8, 2026
    14c3f41
    f8da8d7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions