CEP In Flink (2) - Matching CEP Rules
How Apache Flink matches CEP rules.
Translated from Chinese with AI · Read the original
The previous post explained that Flink’s implementation follows this paper. Let’s first examine the data structures built on that theory.
Data Structures
ShareBuffer
After parsing, a CEP rule is essentially a nondeterministic state machine. During matching, each state corresponds to one or more elements. Consider three states, omitting their where conditions:
Pattern.begin("A").followedBy("B").followedBy("C")The diagram is:

This structure is called SharedBuffer. Each event is represented by a NodeId composed of an EventId and pageName, the state name. Each NodeId maps to a SharedBufferNode containing edges to preceding events. Reference counting controls its lifetime. The reason for this structure is discussed below.
NFAState
Another important structure is NFAState, which retains the current matching state. Its two key variables are:
private Queue<ComputationState> partialMatches; // 正在进行的匹配private Queue<ComputationState> completedMatches; // 完成的匹配For each new event, partialMatches is traversed to see whether any partial match can become a completed match.
Matching Process
Given these structures, the process is fairly predictable. On receiving an event, traverse partialMatches, compute each match’s next state, and add the current event to SharedBuffer. Reaching a final state turns a partial match into a completed match.
final PriorityQueue<ComputationState> newPartialMatches = new PriorityQueue<>(NFAState.COMPUTATION_STATE_COMPARATOR);final PriorityQueue<ComputationState> potentialMatches = new PriorityQueue<>(NFAState.COMPUTATION_STATE_COMPARATOR);
// iterate over all current computationsfor (ComputationState computationState : nfaState.getPartialMatches()) { final Collection<ComputationState> newComputationStates = computeNextStates( sharedBuffer, computationState, event, event.getTimestamp());
if (newComputationStates.size() != 1) { nfaState.setStateChanged(); } else if (!newComputationStates.iterator().next().equals(computationState)) { nfaState.setStateChanged(); }
//delay adding new computation states in case a stop state is reached and we discard the path. final Collection<ComputationState> statesToRetain = new ArrayList<>(); //if stop state reached in this path boolean shouldDiscardPath = false; for (final ComputationState newComputationState : newComputationStates) {
if (isFinalState(newComputationState)) { potentialMatches.add(newComputationState); } else if (isStopState(newComputationState)) { //reached stop state. release entry for the stop state shouldDiscardPath = true; sharedBuffer.releaseNode(newComputationState.getPreviousBufferEntry()); } else { // add new computation state; it will be processed once the next event arrives statesToRetain.add(newComputationState); } }
if (shouldDiscardPath) { // a stop state was reached in this branch. release branch which results in removing previous event from // the buffer for (final ComputationState state : statesToRetain) { sharedBuffer.releaseNode(state.getPreviousBufferEntry()); } } else { newPartialMatches.addAll(statesToRetain); }}In NFA.java, computeNextStates determines the next states for the current partial match. isFinalState and isStopState then classify and handle them, while nodes in SharedBuffer are released.
We can simplify computeNextStates to the following:
final OutgoingEdges<T> outgoingEdges = createDecisionGraph(context, computationState, event.getEvent())final List<StateTransition<T>> edges = outgoingEdges.getEdges();final List<ComputationState> resultingComputationStates = new ArrayList<>();for (StateTransition<T> edge : edges) { switch (edge.getAction()) { case IGNORE: // ...处理ignore break; case TAKE: // ...处理take break; }}createDecisionGraph evaluates the current event against the transition graph described in CEP In Flink (1), producing eligible outgoingEdges. Each edge has an action and targetState. Traversing them creates new states returned to the previous code fragment.
Matching Optimization
As events arrive, more partial matches coexist. To avoid duplicating events, SharedBuffer gives each SharedBufferNode versioned SharedBufferEdges pointing to different preceding NodeIds, as shown:

Events arrive in the order e0 -> e1 -> e2 -> e3. Here, e3 follows both e1 and e2, distinguished by version numbers. Reality is slightly more complex, for example version 1.0 is omitted from the diagram, but the overall optimization follows the same idea.