Layering rulesets in a pico

Once we have created a pico, we can begin to add rulesets to it. Each ruleset we install adds another  layer of behavior and persistent state.

Every pico comes with three rulesets installed, the first three shown above.

The next three were installed to implement the stateful web app described in the previous post.

In this post, we will add a ruleset that maintains a history of the colors we have selected. We'll call it fav-color-history and give it an index page. So, it will be requested by a URL like

https://DOMAIN:PORT/sky/cloud/ECI/fav-color-history/index.html

with its own event channel identifier.

The design for the page is simple:

Managing the state of a pico

The state of the pico, the data that it holds and maintains, is organized by ruleset. The source code of the first three rulesets is in this folder of the pico engine repo.

The io.picolabs.pico-engine-ui ruleset

This ruleset holds data for the developer UI, including the pico's name, x and y coordinates, width, height, and background color.

The io.picolabs.wrangler ruleset

The data held by this ruleset includes the pico's name*, its unique identifier, an ECI for its parent pico, and the ECI used by its parent pico to send messages to it. The two channels identified are of a special kind, called "family channels", that can be used internally, but not within a URL.

The io.picolabs.subscription ruleset

Although not used in these sample applications, this ruleset holds arrays of relationships that this pico has with other picos.

Layered rulesets

Of the rulesets we discussed in the previous post, the first two do not have any entity variables, and so they do not maintain state for the pico. They provide functions that can be called by rulesets that use them as modules.

The fav-color-sample ruleset, on the other hand, does maintain state: the name and code of the currently selected color.

Functions vs Rules

A ruleset defines both functions (in its global block) and rules (directly inside the ruleset). Both function calls and incoming events are queued for the pico. Each one is considered in turn, and runs to completion, so code written to define them runs in a single thread (eliminating the need for critical sections).

Functions

A ruleset provides functions that can be called by other rulesets that use this one as a module.

A ruleset shares functions that can be called by the outside world, either over HTTP, or more directly by another pico.

Functions which are neither provided nor shared can only be used internally in the same ruleset.

Rules

Rules, as defined in a ruleset, cannot be called. They are not functions.

When a pico receives an event, either from the world outside it, or raised from within, all of the rules in all of its rulesets are examined to see whether the event is salient for them. That is, should this rule react to this incoming event? If so, the rule is added to a schedule of rules to be evaluated.

Once the schedule has been determined, the rules are evaluated one at a time. If a rule raises an event (to the same pico) that may add more rules to the end of the schedule. When the schedule becomes empty, the reaction is complete for the event. Then the next thing on the pico's queue (either a query (function call) or an event) will be considered.

Another ruleset, layered on

To maintain history of colors selected, we could create a new ruleset, and give it a rule that selects on the same event, fav_color : fav_color_selected.

The new ruleset could sort of eavesdrop on the pre-existing ruleset. But then, it would have to reimplement the code to compute the color name from the fav_color attribute (as done in the pre block of the recordFavColor rule in the pre-existing ruleset).

Raising a terminal event

Instead, we will modify the pre-existing ruleset to have it raise a terminal event. So called because it is the last thing done by a rule, even though we have no use for it in the given ruleset.

Raising a terminal event is considered a best practice. An extended example that motivates this can be found in Phil Windley's blog post, "Tweeting from KBlog: An Experiment in Loose Coupling".

The change to the rule in the pre-existing ruleset is shown here (highlighted in green), in context:

ruleset fav-color-sample {
  ...
  rule recordFavColor {
    select when fav_color fav_color_selected
      fav_color re#^(\#[a-f0-9]{6})$# setting(fav_color)
    pre {
      colorname = colors:colormap()
        .filter(function(v){v==fav_color}).keys().head()
      || "unknown"
    }
    fired {
      ent:colorname := colorname
      ent:colorcode := fav_color
      raise fav_color event "fav_color_recorded" attributes {
        "colorcode":fav_color,
        "colorname":colorname,
      }
    }
  }
}

There is no penalty in a ruleset to raise an event that you don't consume yourself.

The rule shown above actually records the favorite color selection. So, it makes sense to raise an event saying that this has happened. Notice that the event attributes include all relevant data, including data calculated by the rule.

Consuming a terminal event

Now that this event, fav_color : fav_color_recorded, is available, we can write a new ruleset to consume it:
ruleset fav-color-history {
  ...
  rule recordFavColor {
    select when fav_color fav_color_recorded
    fired {
      ent:history{time:now()} := event:attrs
    }
  }
}

Now the new ruleset maintains a map from timestamp to whatever the event generator supplies as attributes (we expect it to be a map of the color name and code).

The source code for the new ruleset can be found here.

Notes

* An alert reader will have noticed that there are two rulesets that hold the name of a pico. This is an issue because it means that under certain circumstances those names could diverge.


No comments:

Post a Comment