Skip to content

BpmnEventRegistryEventConsumer silently discards events when no subscription exists yet (fire-and-wait race condition) #4189

@pedrogalan

Description

@pedrogalan

Note: I'm not entirely sure whether this is a bug or a feature request — the current behaviour may be by design, but it leads to silent data loss and hung process instances. Happy to be guided by the maintainers.

Describe the bug

When using an async HTTP Service Task followed by an Intermediate Catching Event, there is a race condition where the Kafka message can arrive before Flowable has registered the event subscription.

With flowable:async="true" flowable:parallelInSameTransaction="false", the token moves to the Catching Event in a separate transaction picked up by the job executor asynchronously. If the external service publishes its response to Kafka before the job executor runs, the message arrives with no subscription yet registered. BpmnEventRegistryEventConsumer silently discards it — eventHandled() returns false, no exception, no log. The process instance waits forever.

t=0ms     Async HTTP Task fires
t=50ms    External service publishes Kafka message
t=70ms    Message consumed → no subscription found → silently discarded
t=800ms   Job executor moves token to Catching Event → waits forever

This is hard to reproduce locally (fast job executor) but common under production load.

Expected behavior

When no subscription is found, BpmnEventRegistryEventConsumer should provide a hook for callers to act on it — so that messaging infrastructure (e.g. Spring Kafka non-blocking retry) can re-deliver the message after a delay, by which time the subscription will exist.

Code

BPMN pattern that triggers the issue:

<serviceTask id="httpTask" flowable:async="true" flowable:parallelInSameTransaction="false" flowable:type="http"/>

<intermediateCatchEvent id="waitForResponse">
  <extensionElements>
    <flowable:eventType>myEvent</flowable:eventType>
    <flowable:eventCorrelationParameter name="correlation_id" value="${correlation_id}"/>
  </extensionElements>
</intermediateCatchEvent>

<sequenceFlow sourceRef="httpTask" targetRef="waitForResponse"/>

Current workaround — subclassing BpmnEventRegistryEventConsumer:

class NoSubscriberEventConsumer(
    processEngineConfiguration: ProcessEngineConfigurationImpl,
) : BpmnEventRegistryEventConsumer(processEngineConfiguration) {

    override fun eventReceived(eventInstance: EventInstance): EventRegistryProcessingInfo {
        val result = super.eventReceived(eventInstance)
        if (!result.eventHandled()) {
            throw NoEventSubscriptionFoundException("No subscription found for key '${eventInstance.eventKey}'")
        }
        return result
    }
}

Suggested fix

Add a protected template method called when findEventSubscriptions() returns empty:

// no-op by default — backwards compatible
protected void handleNoSubscriptionsFound(EventInstance eventInstance) { }

This is broker-agnostic, fully backwards compatible, and gives applications a clean extension point to wire in their own recovery mechanism.

Additional context

  • Flowable version: 7.2.0
  • Database: PostgreSQL
  • Spring Boot via flowable-spring-boot-starter
  • Kafka inbound channels (.channel files with "type": "kafka")

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions