Execution Listeners – a powerful tool
Have you ever used an Execution Listener to hook into the flow of a Process Instance in order to call Java code? If you’ve ever worked with Camunda Platform, then chances are you have. Execution Listeners are very versatile and quite powerful, simply because literally every single flow element in a Process Definition will become an Execution during automation. This allows the attachment of Execution Listeners to every single flow element, even the less prominent ones like sequence flows (arrows) or gateways.
The usecases for Execution Listeners may be manifold: you may want to check if certain conditions are met after a boundary event occurs. Or you might want to update some sort of status after passing an activity. Or maybe you just want to write data into an underlying data model after the correlation of a message. None of these aspects are really part of the modeled business process, so it seems like a good idea to “hide” them with the use of Execution Listeners.
But beware! When Execution Listeners are used in certain constellations, they will cause unexpected, unwanted behavior.
Handle with care!
Let’s look at the following Process Model:
The Execution Listeners will be called at the end of their respective executions, doing checks or operations. If an error should occur during those calls, the transaction will be rolled back and the token will remain in place. So far, so good.
But let’s say the Process Model changes, so that the whole process may now be canceled at any time:
The addition of the new Event Sub Process should not really influence the existing activities or their respective Execution Listeners. But unfortunately it does, quite a bit! This is because canceling the entire process instance will cause the existing tokens to be removed from their activities. Which, in turn, triggers the appended Execution Listeners. This is because Camunda Platforms Process Engine does not differentiate between Executions that were ended successfully and those that were canceled. They both just “ended”.
What’s the worst that could happen?
Assuming a token is residing at the ‘Do something’ user task, a cancellation will trigger its Execution Listener. Since there has been no user input, the check will fail, the transaction will be rolled back and the cancellation will not actually happen. In fact, it is now impossible to do the cancellation, because the User Tasks seemingly unrelated Execution Listener will prevent it fully. Same goes for executions waiting at the Receive Task:
This whole issue regarding the lack of differentiation between cancelled and completed executions is far from new. If you go to Camunda Cockpit and try to delete a deployment of a Process Defition, you will see a box allowing you to „Skip Custom Listeners“. By default, the box is ticked, because these deletions will cancel Process Instances, which will, in turn, call the Execution Listeners code, resulting in completely unforeseeable behaviour or, in the best case scenario, a failure to delete the deployment:
The same problem can come up when deleting a Process Instances or during its migration.
So this issue has been around for quite a while, is known well enough for Camunda to implement their own workarounds and can, potentially, cause pretty hefty damage. So let’s look at ways to get out of this mess.
An easy solution? Better safe than sorry!
For User Tasks, there actually is an easy way out: it would make a lot more sense to declare the Listener as a Task Listener instead of an Execution Listener. That way one can have it trigger solely at the Tasks completion and not at its cancellation, since User Tasks have their own lifecycle and actually distinguish between those two scenarios.
But what about the Receive Task, ‘Wait for Message’ in our example? What works for the User Task is just not an option here. In fact, every End Execution Listener appended to any Wait State Activity other than User Tasks is in risk to get triggered at the wrong time, causing who knows what.
We encountered this problem at our project at Südleasing GmbH and ended up with the following solution: as mentioned earlier, Camunda Platforms Process Engine does not differentiate between Executions that were ended successfully and those that were canceled. But the engine does recognize the fact, that an execution is canceled and stores it at the respective Java object. And since an Execution Listener naturally has access to the execution, it can check for a possible cancellation by calling the isCanceled-method.
So you could just call this for every piece of code that is called by Execution Listeners. Seems a bit cluncky, though; so why not take this opportunity to do some aspect-oriented programming? Let’s make it an annotation!
The implementation (in this case done with Spring Boots AOP-tools) can be simple enough: it just needs to check, whether the execution was cancelled. If so, the method execution is skipped.
Slap that annotation onto every class that is used as an Execution Listener and you’re good to go. Don’t think the problem applies to you? You might want to do it anyway! It does not break anything and saves you trouble down the road when a seemingly unrelated change causes crazy issues, because noone remembered the questionable way Camunda Platform handels Execution Listeners on canceled Executions. Of course there may be scenarios in which you want listeners to trigger even upon cancellation. But in that case this annotation will still help you make a concious decision on this matter.
Sourcecode and Shoutout
Interested in the code or some tests demonstrating the issue? You can check out all of that on GitHub. Feel free to use it in your project or leave a comment!
And as always: we’re happy to support you with our BPM Technology Consulting.
Special thanks goes out to our partners at Südleasing GmbH, especially Christian Dußler, who allowed us to use these snippets in this post, thus giving us the possibility to contribute to the Camunda community.