KRL Patterns: Building Event Intermediaries



The Silver Jubilee Bridge

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.

  1. 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.
  2. 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.