Activiti 6: An Evolution of the Core Engine
This article describes the changes with regards to the Activiti core engine and how version 6 differs from version 5. All the material here, including the code examples were presented back in June in Paris, but I believed it was good to also have it written down in blog form for people that don’t like watching Youtube movies (although I think it’s pretty interesting �� )
If you don’t like the theoretical background on the Actviti 6 design, do scroll down to see some nice Activiti 5 vs Activiti 6 examples!
The Activiti engine is now five years old. In those five years, we’ve seen Activiti grow enormously and today it’s used all across the world in all kinds of companies and use cases. Five years is a very long time in IT, but the architectural choices we’ve made with Activiti proved to be the right ones. In the past five years, we’ve also learned a ton of how Activiti is used (and maybe (ab)(mis)used).
We’ve identified four key parts where we believe the engine could be improved. Rather than to hack all that stuff in piece by piece, we decided to do them all at once and get it all nice and clean in the code. That is the Activiti 6 engine. As such, version 6 is not a rewrite or new engine, as a matter of fact it’s fully compatible with version 5. Activiti version 6 is an evolution of the core engine, taking in account all we have learned in the past five years and what we (the devs, the community and customers) believe that is needed to cope with the use cases of the next five years, in the cleanest and simplest way possible.
Anyway, enough fluff, let’s dive into those four problem areas.
Problem 1: Model Transformation
In Activiti version 5, a process definition was parsed internally to another model (called the Process Virtual Machine model). People that have gone deep into the engine, will have seen classes such as ActivityImpl, ExecutionImpl, ProcessDefinitionImpl. That model is then interpreted and executed by ‘atomic operations‘ in the core engine.
The problem with that approach is that the model did not match 1-1 with the BPMN model and even with some concepts described in the BPMN spec. And, as with any translation of models between software layers, what happens is that model details leak between layers. For example, look at this class: the low level model leaks into the behavior class. You could argue that’s ok, but it does mean the developer of that behavior class needs to get a mental model in his head that does not match with the stuff learned from the BPMN spec.
The same code in version 6 looks like this (could be written a lot shorter, but this is for clarity): here, we reason about sequence flow on the current BPMN element. It turns out that writing such behaviors (as we do for the core constructs of BPMN we support) is a lot easier when it maps 1-1 with the BPMN spec. The code becomes a lot more readable and understandable and you can reason about very complex structures with much more ease.
Problem 2: Execution Tree
One the core goals when starting with version 5 was to get the number of inserts to the database as low as possible. Which we did: we (over) optimized and reused execution entities (the representation of a process instance that is executed) as much as possible.
The consequence of that mantra is that in many places you can now find if-else-constructions that are very specifically optimising a certain use case or pattern. The problem is that in version 5, it’s very hard to look at the execution tree in the database and know exactly how that maps to the BPMN structure. Which is crucial if you want to do dynamic process definitions and/or instances without hacking.
In version 6, we opted for clear and simple guidelines for the structure of the execution tree, making it simple to look at an execution tree and know exactly where and how the process instance is being executed.
Problem 3: Competing Stacks of Operations
Every API call in Activiti boils down to the manipulation of the runtime execution structure by applying changes to the runtime model and feeding that into the core engine where it gets interpreted by the ‘atomic operations’.
That’s a good approach, except in the very early days the stack of these operations was not designed to be central. Rather, there could be multiple stacks of such operations existing at the same time. To make a long story short, this means that certain patterns can’t be executed on the v5 engine.
In version 6, we implemented a central agenda that is used to plan all the core operations. This fixes those patterns and at the same time makes process execution also way easier to follow and debug.
Problem 4: Persistence Code
Code grows organically. Five years ago, we tried to keep persistent logic nicely contained … but over the last five years much of the persistence code has gone everywhere. This makes it impossible to swap out the persistence layer with a custom implementation.
Of course, you could hack your way around all that (people on the forum have done it :) ), but it’s really dirty and not maintainable in the long run.
In version 6, we separated cleanly the persistence code (and the entity logic) into different, pluggable interfaces. Actually, that work has just recently been completed. Expect a blog post about it soon.
A Few Examples
These are just a few examples of what was not possible on the Activiti 5 engine, but does work on v6. Again, we probably could get it all working with a lot of hacking on the v5 engine, but it would not have been the nice and clean solution that we now have in the v6 engine.
Example 1: ‘Tricky Inclusive Merge’
This was an example I found on the blog of our partner BP3: assume that task A and B have been completed. At that point C completes and the exclusive gateway conditions make it go to E. Then E is completed. At that point D should become active and the process instance can finish.
In version 5, what happens is that the process instance never completes, as D is never reached. The reason for that is that the executions that are dormant after reaching the inclusive merging gateway will never be activated again.
In version 6, we fixed this (and similar patterns) by introducing a kind of ‘background ActivityBehavior’, making it possible for certain constructs (such as the inclusive gateway) to have background behavior, in this case verifying if the conditions to execute the merge are fullfilled.
Example 2: ‘Loops and Tail Recursion’
In this example, the service task is visited 1000 times before the end event is reached.
In Activiti 5, this leads to a StackOverflowException (and many people on the forum have reported it!).
In Activiti 6, this runs as expected. The reason for this is the centralized agenda for operations, implemented using tail recursion and a run loop .
Example 3: ‘Concurrency after a Boundary Event’
Don’t let the size of the example scare you, the embedded subprocesses here actually don’t matter (it’s just there to impress �� ).
The real problem here is the boundary event which has two parallel outgoing sequence flow for the timer boundary event. In Activiti 5, such a pattern was not possible (you have to use a parallel gateway). In Activiti 6, this ran without actually any work, as the direct BPMN model interpretation does not leave any other way of executing it
I want to try it out for myself!
Good news for you! We’ve release a first beta of Activiti 6 last week!
If you’re interested in running the new UI for Activiti 6, Tijs has written an excellent post about it here.
Reference: | Activiti 6: An Evolution of the Core Engine from our JCG partner Joram Barrez at the Small steps with big feet blog. |