Flink CEP

Liao Jiayi Liao Jiayi #Apache Flink CEP

A source-code-oriented introduction to Flink CEP, revisiting my earlier notes while preparing an online walkthrough.

Translated from Chinese with AI · Read the original

My earlier CEP articles are available here.

Those articles were fairly rough. While preparing an online session on the CEP source code, I decided to write down my understanding from an implementation perspective.

OnBoarding

As enterprise analytics becomes more sophisticated, event analysis is moving beyond offline computation and simple counts toward more timely, precise, and complex analysis. Complex event processing is complex because events are related, and those relationships take many temporal forms.

Let us begin with a test from CEPITCase.java. I have simplified testSimplePatternCEP():

@Test
public void testSimplePatternCEP() throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Event> input = env.fromElements(
new Event(1, "barfoo", 1.0),
new Event(2, "start", 2.0),
new Event(3, "foobar", 3.0),
new SubEvent(4, "foo", 4.0, 1.0),
new Event(5, "middle", 5.0),
new SubEvent(6, "middle", 6.0, 2.0),
new SubEvent(7, "bar", 3.0, 3.0),
new Event(42, "42", 42.0),
new Event(8, "end", 1.0)
);
Pattern<Event, ?> pattern = Pattern.<Event>begin("start").where(new SimpleCondition<Event>() {
@Override
public boolean filter(Event value) throws Exception {
return value.getName().equals("start");
}
}).followedBy("middle").subtype(SubEvent.class).where(
new SimpleCondition<SubEvent>() {
@Override
public boolean filter(SubEvent value) throws Exception {
return value.getName().equals("middle");
}
}
)
.followedBy("end").where(new SimpleCondition<Event>() {
@Override
public boolean filter(Event value) throws Exception {
return value.getName().equals("end");
}
});
DataStream<String> result = CEP.pattern(input, pattern).flatSelect((p, o) -> {
StringBuilder builder = new StringBuilder();
builder.append(p.get("start").get(0).getId()).append(",")
.append(p.get("middle").get(0).getId()).append(",")
.append(p.get("end").get(0).getId());
o.collect(builder.toString());
}, Types.STRING);
List<String> resultList = new ArrayList<>();
DataStreamUtils.collect(result).forEachRemaining(resultList::add);
assertEquals(Arrays.asList("2,6,8"), resultList);
}

The pattern is fairly intuitive: find a sequence whose names are start, middle, and end. With this basic usage in mind, we can unpack CEP step by step.

Parsing Rules

Before considering Flink’s implementation, we can draw the relationships implied by this pattern. Data can be in four states:

  • The start condition has not been met.
  • start has been met, but middle has not.
  • start and middle have been met, but end has not.
  • start, middle, and end have all been met.

We name these states start, middle, end, and Final State. Their relationships are shown below.

cep-rule1

This makes the rule easier to understand. It also introduces the theoretical basis of flink-cep: the NFA, or nondeterministic finite automaton, in Efficient Pattern Matching over Event Streams. Nondeterministic means that the next state is not uniquely determined. Flink implements most of the paper’s core ideas, though features such as matching multiple rules are still missing. Let us examine how the source code represents an NFA.

cep-nfa-uml

Flink uses NFA.class for the model, State.class for states such as start, middle, and end, and StateTransition.class for their relationships. A StateTransition corresponds to an edge in the diagram: sourceState and targetState identify its endpoints, and condition determines when the transition occurs. Transitions have three possible actions:

public enum StateTransitionAction {
TAKE, // 获取当前满足条件的事件
IGNORE, // 忽略当前满足条件的事件
PROCEED // 自然转换
}

PROCEED may be less intuitive; I will explain it later with an optional example.

We now have a basic understanding of NFAs and their representation in Flink. The UML diagram shows that State.class already contains its StateTransitions, so let us examine how these states and transitions are built. The source has many if/else branches for special cases; here we will focus on the example in the onboarding section.

The Pattern objects are nested, with the outermost object representing the final state. Unwrapping them therefore constructs the NFA in reverse order.

/**
* Creates all the states between Start and Final state.
*
* @param sinkState the state that last state should point to (always the Final state)
* @return the next state after Start in the resulting graph
*/
private State<T> createMiddleStates(final State<T> sinkState) {
State<T> lastSink = sinkState;
while (currentPattern.getPrevious() != null) {
if (currentPattern.getQuantifier().getConsumingStrategy() == Quantifier.ConsumingStrategy.NOT_FOLLOW) {
//skip notFollow patterns, they are converted into edge conditions
} else if (currentPattern.getQuantifier().getConsumingStrategy() == Quantifier.ConsumingStrategy.NOT_NEXT) {
省略 ................
} else {
lastSink = convertPattern(lastSink);
}
// we traverse the pattern graph backwards
followingPattern = currentPattern;
currentPattern = currentPattern.getPrevious();
省略 ...........
}
return lastSink;
}

Indeed, the code recursively unwraps Pattern and calls convertPattern to parse each part. Ignore the special cases for NOT_FOLLOW and NOT_NEXT for now. convertPattern calls the following function:

private State<T> createSingletonState(final State<T> sinkState,
final State<T> proceedState,
final IterativeCondition<T> takeCondition,
final IterativeCondition<T> ignoreCondition,
final boolean isOptional)

It first derives possible next states and their conditions from currentPattern, including its where condition and ConsumingStrategy, then calls createSingletonState to create the state. The example therefore produces the following NFA:

flink-nfa-1

We can now describe more complex cases, such as optional and times.

// TODO

Matching Rules

// TODO

Producing Output

// TODO

Things to Watch When Using CEP

// TODO