Recently we somewhat quietly added a BIG new feature to KRL: explitic events. Using an explicit event, one rule can raise an event for another rule. Explicit events are raised in the rule postlude like so
raise explicit event foo [for] with fizz = "bazz" and fozz = 4 + x;
If the optional for
clause isn't given, the event is raised for the current ruleset, otherwise it's raised for the named ruleset. The with
clause allows the developer to add event parameters to the explicit event. The right-hand side of the individual bindings in the with
clause can be any KRL expression. Like any other postlude statement, explicit events can be guarded:
raise explicit event foo with fizz = "bazz" and fozz = 4 + x if (flipper == "two");
The event in the preceding example will only be raised if the variable flipper
has the value two.
Explicit events allow KRL programmers to chain rules together. Rule chaining is good for modularization, preprocessing, and abstraction as we'll show in the following sections. We'll first discuss event intermediary patterns in general and then go through several example patterns.
KRL Event Patterns
KRL is a rule language, a style that's unfamiliar to most programmers. Consequently, it's useful to see patterns and idioms for common operations. Additionally, KRL is an event processing language. As such, events are at the core of what happens inside a KRL program. That means that understanding how to process and manipulate events is important.
One thing that most event intermediary patterns have in common is that they usually take no action. You'll see that the noop()
action is prevalent in the examples below. There is a (complex) event expression, sometimes some data manipulation in the prelude, and finally one or more events raised in the postlude.
As currently implemented, events in KRL have several limitations that can limit the ability of KNS to serve as an event intermediary in certain situations.
-
KRL currently limits an explicit event to be raised for one ruleset--either the current ruleset (the default) or one given in the
for
clause. - There is no way at present to pass all of the event parameters from the event expression to an explicit event when it is raised. The developer must explicitly (no pun intended) pass an event parameters that necessary in any following steps.
We will be taking steps to remove these limitations in future releases of KRL.
Event Logging
One of the simplest intermediary patterns is the event logging pattern. The intermediary rule looks for the expected event scenario, calls a logging statement (either using the built-in log
command in KRL or by making an HTTP post) and then passes the event on using an explicit event.
Here's an example:
rule logger_rule is active { select when phone outboundconnected http:post("http://example.com/mylogger.cgi") with with number = event:param("phonenumber") always { raise explicit event outboundconnected with phonenumnber = event:param("phonenumber") and time = event:param("time"); } } rule use_phone { select when explicit outboundconnected ... }
In this example, the rule is logging the event and some data from it before passing the event on (as an explicit event). This might be useful for debugging or billing. Note that Kynetx terms of service explicitly disallow the use of calls to other systems for purposes of smuggling people's private data.
Abstract Event Expressions
Sometimes you will have a complex event expression (one that uses compound event expressions) that you need to use in more than one rule. Good programming practice dictates that you abstract that complex event expression so that if it changes, you don't have multiple places to remember to update it. Additionally, giving a complex event expression a name can facilitate program readability. Explicit events give us the means to accomplish complex event expression abstraction.
Here's an example:
rule called_first is active { select when phone outboundconnected before mail received from "@apple.com" noop(); always { raise explicit event called_first with msg = event:param("msg"); } } ... rule use_called_first_1 is active { select when explicit called_first ... } ... rule use_called_first_2 is active { select when explicit called_first ... }
Notice that the first rule raises an explicit event with the name "called_first" whenever it saw a particular event pattern. Two later rules uses the "called_first" explicit event. If the complex event expression is changed or updated the two rules will both respond appropriately. When used like this, we call "called_first" an abstract event expression.
Event Preprocessing
Sometimes the data from an event (in the event parameters) will need to be preprocessed before it is used. Based on the results of that preprocessing, you may want to do different things.
Here's an example:
rule pinentered is active { select when mail received pre { msg = event:param("msg"); from = event:param("from"); item = datasource:pds({"key":from}); relevant_data = msg.query("li[type=#{item}]"); } noop(); always { raise explicit event mail_received with from = event:param("from") and to = event:param("to") and msg = relevant_data } }
In this example the message from a mail that's been received is preprocessed using the query
operator to retrieve just those portions that are HTML <li>
elements with an attribute with the type
equal to a value that it retrieves from a datasource using the from address of the message.
This is an example of a complex mapping step that might need to be done for several rules. Using explicit events we can pull it out into a single place where it can be more easily maintained and tested.
Event Stream Splitting
Related to the idea of event preprocessing is the notion of event stream splitting. The previous example shows event parameter preprocessing. We can use the event parameters to split the event stream and send it in two different directions depending on the result of a test on the data. Often preprocessing will be done in support of splitting the event stream.
Here's an example:
rule pinentered is active { select when webhook pinentered pre { pinattempt = event:param("Digits"); phone = datasource:pds({"key":"phone"}); pin = phone.pick("$..value.pin"); } if pinattempt == pin then noop(); fired { raise explicit event correct_pin } else { raise explicit event bad_pin } } rule badpin is active { select when explicit bad_pin ... } rule correctpin is active { select when explicit correct_pin ... }
In this example the data in the event parameter Digits
is compared with data retrieved from another data source (datasource:pds
). If they're equal, the explicit event correct_pin
is raised, otherwise the explicit event bad_pin
is raised. Each of these rules continue processing as necessary. In this case none of the event data from the original event is passed on with the new events, but that need not be the case.
App Controller Ruleset
An app, in Kynetx nomenclature, consists of one or more rulesets. Complex apps consist of multiple rulesets. We've built some that use a dozen or so and I expect to see apps that use many more than this. One of the problems in building complex apps that comprise multiple rulesets is keeping track of the control points in the app--what events are causing what to behave. Developers often want a single place in the code where they manage control flow.
Using the patterns outlined above, you can create a controller ruleset that is the main entry point for the app and controls the rules that get executed in other rulesets. Here are a few of the advantages of using a controller ruleset in your app:
- routing - Each complex event pattern that the app responds to is represented in the controller ruleset. Each of these event patterns raises an explicit event that other rules in the app respond to (event abstraction).
- authentication - Kynetx Marketplace offers developers a way to charge for their apps. But only one ruleset can be listed in marketplace as the "app." An event controller solves this problem by being the one point of control and this the place where authentication is controlled as well.
- normalization - preprocessing event parameters in the controller app provides a normalized version of data and can serve to insulate the rest of the app from changes in outside event sources and endpoints.
Conclusion
Explicit events open up the use of event intermediaries in KRL and significantly expand the viability of complex apps built from multiple rulesets. Explicit intermediary rulesets like a controller greatly reduce the cognitive complexity of large apps. The example above are just a few of the interesting patterns I've noticed. As you notice others, I hope you'll let me know so I can collect and share them.