Pico-Based Systems Lesson

Learning Objectives

After completing this lesson you will be able to do the following:

  • Explain picos and the parent-child relationship

  • Use the UI to create and delete child picos.

  • Create and delete child picos programmatically.

  • Install a ruleset in a child pico both manually and programmatically.

Prerequisites

You should have completed the following lessons

You should read:

Contents

Pico Children

One of the important features of programming with picos is creating children. Solving programming problems with picos usually involves creating a system of picos that cooperate to solve the problem. This lesson introduces the concepts of pico-based systems and pico life-cycle management.

The pico that is created when your pico-engine started for the first time is called the "root" pico. 

  • Every pico except the root pico has a parent. 

  • You can create as many child picos as you like. 

  • Picos can be the children of other child picos and so on. 

Creating Children Using the UI

You can use the UI to manage child picos. 

Start by clicking on the "About" tab for your root pico and then clicking the "add child pico" button, having provided a display name and, if you wish, a different color. In this lesson and the next, we'll be considering a system of picos which implement a registration system, so let's call the child "Registration Pico".

Here is a screenshot just before clicking the button:

And here us the screen after the button is clicked. Notice the new child pico has a (randomly assigned) event channel identifier (ECI).

Visiting a Child Pico

Click on the link of the child pico to see the "About" tab for this new child pico.

Notice that, while the root pico doesn't have a parent, this pico does, and you can navigate to its parent by clicking on the parent link. It has no children, yet.

For practice, create a child pico for the Registration Pico, naming it "Section Collection Pico" and giving it a different color.

Seeing the parent-child relationship

Click on the "Close" button in the upper-right corner to see the layout of the pico collection so far. It should look something like this, after you reload the page move the picos around a bit:

Deleting child picos using the UI

You may have noticed in the "About" tab, beside the ECI of the picos you have created, that there is a link labelled "delete". This allows you to manually delete the pico. 

For practice, create another child pico in one of the picos and then delete it. Currently, the UI does not allow you to delete a pico which has children (until you first delete all of its children, etc. recursively).

Deleting a pico cannot be automatically un-done. Any channels, installed rulesets, and state will be deleted with the child and can only be recovered by recreating it from scratch.

Creating Children Programmatically Using Wrangler

One of the hallmarks of picos is that they can create other picos programatically. In this section, we'll write rules that manage the pico lifecycle.

The following diagram gives an overview of the application we will use as a running example in this and the next few lessons. It shows parent-child relationships only. In the next lesson we'll learn about other kinds of relationships among picos.

The "Root Pico" is created when a pico-engine starts for the first time. We have already created the Registration and Section Collection picos manually.

However, the two other kinds of picos (for individual sections and students) will be created programmatically, as needed. The diagram shows only a few of each, but in practice there could be thousands of each kind. BYU, for example, teaches over 8000 sections per semester.

Our next step here will be to write a KRL ruleset for the "Section Collection Pico". The purpose of this ruleset will be to manage its children, and serve as a sort of directory for them.

This pico will expect events from other picos or from the outside world (a class management system, for example) informing it that a pico is needed for a particular section. When the rule evaluates, it will create the section pico and see that it is properly initialized, and will then record the ECI of the new section pico for future use. We'll complete the process of connecting the section pico to student picos which need it in the next lesson on subscriptions. For example, student A might need a direction communication channel with section CS462-1, and this (pair of) channels is called a "subscription".

Create a new ruleset named, say, app_section_collection, that includes this rule:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 rule section_needed { select when section needed pre { section_id = event:attr("section_id") exists = ent:sections && ent:sections >< section_id } if exists then send_directive("section_ready", {"section_id":section_id}) notfired { ent:sections := ent:sections.defaultsTo([]).union([section_id]) raise wrangler event "new_child_request" attributes { "name": nameFromID(section_id), "backgroundColor": "#ff69b4" } } }

Let's take this opportunity to review the various parts of a rule definition:

Line 1 names the rule, in this case section_needed

Line 2 allows the pico-engine to select which events will cause this rule to be evaluated: those with domain section and name needed

Lines 3-6 constitute the rule's prelude which binds local* names to values

Line 4 binds the local name section_id to the event attribute named section_id for convenience.

Line 5 binds the local name exists to the result of the Boolean expression. The operator >< is the membership** operator

Lines 7-8 constitute the rule's condition/action block and sends a directive if exists is bound to true

Lines 9-13 constitute the rule's postlude. In a postlude, we can mutate entity variables and raise events

Lines 10-12 will execute when the rule's condition is not*** met. That is, when the id passed in is not already in the entity variable (see line 5)

Line 10 assigns a new value to the entity variable by taking the union of the sections variable and new id. If the entity variable has not yet been assigned a value (i.e. it is null), it will start out as an empty array.

Lines 11-12 raises an event, with domain wrangler and type child_creation. This event is expected by the ruleset named io.picolabs.wrangler using the same rule that runs when you create a child pico manually.

* local in the sense of being lexically scoped to this rule.

** the membership operator is described in the page Predicate Expressions

*** when the rule's condition evaluates to true, we say the rule has "fired", otherwise, when the condition evaluates to false, the rule has "not fired"

Notice that the rule uses a function named nameFromID which is simply defined, in the global block of the ruleset, as

1 2 3 nameFromID = function(section_id) { "Section " + section_id + " Pico" }

Be sure the ruleset shares it in the meta block. With this in place, once you have validated and installed your ruleset, you'll be able to use the Testing tab of the Section Collection Pico and have a UI for adding section picos by sending events to your pico, which will be handled by your ruleset.

To install your ruleset in the section collection pico, visit its Rulesets tab, enter the URL to its KRL source code, and click the Install button. Next create a channel with a policy that allows events with domain section and queries for this ruleset. Then you can visit the Testing tab and begin creating section picos.

Close the section collection pico and its tabs. This will reveal the newly-created child pico, which you can position and re-size to obtain a layout like this:

You can position it at will, and note that it will remain connected to its parent by a parent-child link.

Congratulations! You can now create and delete child picos manually, using the UI. You also know how to write KRL code to create child picos as needed.

What about race conditions?

An astute reader will have noticed this pattern, shown here with some excerpts as a kind of pseudo-code:

1 2 3 4 exists = ent:sections >< section_id // is this id in the array? if exists then ... // if so, fine else // if not, ent:sections := ent:sections.union([section_id]) // add this id to the array

This looks a lot like a critical section which would need careful attention. What if several events are received concurrently, for the same section id?

However, this is taken care of for us by the design of KRL. When an event triggers a rule, that rule will run to completion, and no other event for the pico will be considered until it has finished. Not only does a pico encapsulate its state, it also synchronizes demands on it. In this case, all of the actions in the code block shown above will complete as an atomic operation. Only then will the pico consider the next event on its event queue. We describe this by saying that picos are “single-threaded” even though the engine is concurrent (i.e. multiple picos can be simultaneously considering events at any given time).

Listing the Children

We can use the Wrangler function children() to get a list of the children. First, we'll need to declare our intention to use the io.picolabs.wrangler ruleset using the alias wrangler. This is done by adding this line to the meta block:

1 use module io.picolabs.wrangler alias wrangler

Here's a function that uses it within the app_section_collection ruleset:

1 2 3 showChildren = function() { wrangler:children() }

This function is just a wrapper. You don't need to wrap Wrangler functions to use them within the ruleset, but we do if we want to make them usable outside the pico. We also have to declare that our ruleset shares the function in the meta block.

Also create a function to show the value of the ent:sections entity variable:

1 2 3 sections = function() { ent:sections }

Now we can use showChildren and sections using the "Testing" tab.

Querying Children

You may need to query children. The Events and Queries lesson showed how to use HTTP to query a pico. But we wouldn’t use that method to query another pico from KRL. Wrangler provides a function for doing this called picoQuery(). picoQuery() uses ctx:query() to make the query if the pico is hosted on the same engine as the pico making the query. Otherwise it uses HTTP.

Assuming that we have this statement in the meta block of our ruleset:

1 use module io.picolabs.wrangler alias wrangler

We can use picoQuery()  provided by Wrangler, as shown in the following rule fragment:

1 2 3 4 5 6 7 8 9 10 11 rule query_rule { pre { eci = eci_to_other_pico; args = {"arg1": val1, "arg2": val2}; answer = wrangler:picoQuery(eci,"my.ruleset.id","myFunction",{}.put(args)); } if answer{"error"}.isnull() then noop(); fired { // process using answer } }

More information can be found in the Wrangler documentation for picoQuery() and in Accessing Functions Shared by Another Pico.

Maintaining information about child picos

So that we can interact with section picos, we will need access to their event channel identifiers.

As we do this, let's change our representation from a simple list (an Array) of section identifiers. Instead, let's use a map containing JSON objects named by the section identifier. To be concrete, instead of ent:sections having a simple array of string section identifiers like

1 [ "CS462-1", "CS462-2" ]

it will need to look like this

1 2 3 4 5 6 7 8 { "CS462-1": { "eci": "citvkd71z0007hgs0yp2vc6ab" }, "CS462-2": { "eci": "citvkufyh0001jzs02fzb3kok" } }

so that we are storing the ECI of each of the child section picos.

Did you have a rule in your app_section_collection ruleset to empty out the list of sections? If not, create one, or modify yours so that it treats the empty collection as an empty object, instead of an empty array.

1 2 3 4 5 6 rule initialize_sections { select when section needs_initialization always { ent:sections := {} } }

Using two rules for the same event

There are two possibilities when a section pico is needed. It might already exist, or we might need to create it. Let's use two rules. Replace your section_needed rule with this one for the case where the section already exists:

1 2 3 4 5 6 7 8 9 rule section_already_exists { select when section needed pre { section_id = event:attr("section_id") exists = ent:sections && ent:sections >< section_id } if exists then send_directive("section_ready", {"section_id":section_id}) }

And, let's use this rule for the case where the section pico needs to be created.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 rule section_already_exists { select when section needed pre { section_id = event:attr("section_id") exists = ent:sections && ent:sections >< section_id } if not exists then noop() fired { raise wrangler event "new_child_request" attributes { "name": nameFromID(section_id), "backgroundColor": "#ff69b4", "section_id": section_id } } }

This is quite similar to the first version of this rule. The differences are that, first, we are not yet recording the information about the new section pico, because it has not yet been created so that information is not available. The second difference is that we are passing the section id as an additional event attribute.

Wrangler will then create the new pico, and record the parent-child relationship between the Section Collection Pico and its new child pico. Wrangler is written to raise an event with domain wrangler and name new_child_created that has the following attributes.

1 2 3 4 5 6 7 8 { // attributes provided by wrangler "eci": <new child's family eci>, // attributes you provided in the child_creation request, passed through "name": <the original provided name YOU gave>, "backgroundColor": <your provided color string>, "section_id": <section_id you provided> }

Info

For more information on the wrangler operating system and different events you can listen in on and what additional attributes you can provide, check out the wrangler documentation. You can also view the Wrangler repo to see the code.

You can add a rule in your app_section_collection ruleset to react to the wrangler new_child_created event. Your code might look something like this.

1 2 3 4 5 6 7 8 9 10 11 12 rule store_new_section { select when wrangler new_child_created pre { the_section = {"eci": event:attr("eci")} section_id = event:attr("section_id") } if section_id.klog("found section_id") then noop() fired { ent:sections{section_id} := the_section } }

Having made these changes, you can go into the UI and in the Testing tab of the Section Collection Pico clear the sections, and in the About tab manually delete any picos you had created previously. You should also trigger the section needs_initialization event to re-initialize the ent:sections entity variable.

Now, create a couple of section picos, and verify that you are persisting the information about each one.

Installing a ruleset in a child pico

Wrangler is written to respond to an event, wrangler install_ruleset_request, which can be used to install a ruleset in a pico.

Why would we want to install other rulesets into each section pico? Rulesets represent the desired behavior in response to events that we would expect from that pico. Picos can be digital twins of real world things/objects, meaning they digitally represent some physical thing (in this case they represent sections and students). So, in simpler terms, if we want a Section Pico to accurately model a real section, we install the app_section ruleset into it that provides the desired functionality. The same could be done for the Student Picos, perhaps with a ruleset called app_student.

Make an empty ruleset named app_section (you could name it any reasonable thing you like). Next, we will install it in section picos, first manually, and then programmatically.

Manually

You can install a ruleset in a pico manually, using the "Rulesets" tab of a pico. Install your new ruleset manually in each of the section picos you created earlier.

Programmatically

Now, let's add code to our app_section_collection ruleset to instruct our new section pico to install its ruleset programmatically. Sometime after each section Pico is created, and before we need to send it the first event specific to sections, we would send this event to install the ruleset in it. This is an "action" which will be placed in the action part of the rule selected by the  wrangler new_child_created event (replacing the noop()).

1 2 3 4 5 6 7 8 9 10 11 12 event:send( { "eci": the_section.get("eci"), "eid": "install-ruleset", // can be anything, used for correlation "domain": "wrangler", "type": "install_ruleset_request", "attrs": { "absoluteURL": meta:rulesetURI, "rid": "app_section", "config": {}, "section_id": section_id } } )

See the P.P.P.S. below for some discussion about the absolute URL.

The event is sent to the child pico, and is handled by the ruleset running Wrangler (as implemented by the ruleset io.picolabs.wrangler, which is automatically installed in every pico which wrangler creates).

As it becomes clear which events must be handled by section picos, corresponding rules can be added to the app_section ruleset.

Now, create a couple of section picos (using the section:needed button in the Testing tab), and verify that each of them has the app_section ruleset (refresh the Rulesets tab of the new section pico).

After wrangler has installed the ruleset in the child pico, it will raise the event "wrangler ruleset_installed" with the appropriate rid attribute. You could create a rule in your app_section ruleset to respond to this event, by storing the section identifier in an entity variable. We will leave this change as an exercise (a simple possible answer is given below in the P.P.S.). 

Automating child creation and rule installation

When you install a ruleset into a pico, you may wish to respond to the event "wrangler ruleset_installed" by automatically creating a child pico. Your ruleset may wish to respond to the event wrangler new_child_created by installing an appropriate ruleset into the new child pico. Finally, that ruleset might respond to the event "wrangler ruleset_installed" by sending an event to its parent. 

Deleting children programmatically using Wrangler

When our app_section_collection pico is notified that a section pico has gone off-line, we can delete it. We'll see in subsequent lessons just how (and why and by whom) this event might be sent. For now, send the event manually from the section collection pico's Testing tab.

This could be done by a rule like this one:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 rule section_offline { select when section offline pre { section_id = event:attr("section_id") exists = ent:sections >< section_id eci_to_delete = ent:sections{[section_id,"eci"]} } if exists && eci_to_delete then send_directive("deleting_section", {"section_id":section_id}) fired { raise wrangler event "child_deletion_request" attributes {"eci": eci_to_delete}; clear ent:sections{section_id} } }

Congratulations! You now know how to delete a child pico programmatically, by sending an event to the parent pico, to which Wrangler reacts and removes the child pico.

Next steps

Next, see the Pico to Pico Subscriptions Lesson, where you will learn how to facilitate communication between picos related in ways other than the parent/child relationship.

P.S. In this lesson, we built the app_section_collection ruleset piece by piece. Follow the link to see it in its entirety. You may find it interesting to compare this with the ruleset you created as you worked through this lesson.

P.P.S. Here is a rule that could be installed in the app_section ruleset, to respond to the event raised by wrangler after this ruleset has been installed in a section pico. Follow the identifier section_id from the app_section_collection ruleset where is originates, into wrangler (which installs the ruleset), and then raises the event which brings us to this rule. Notice that the wrangler rule passes through all of the attributes that it receives, without examining them otherwise.

1 2 3 4 5 6 7 8 9 10 rule pico_ruleset_added { select when wrangler ruleset_installed where event:attr("rids") >< meta:rid pre { section_id = event:attr("section_id") } always { ent:section_id := section_id } }

P.P.P.S. You are used to specifying relative URLs in HTML. Relative URLs are resolved (as specified in RFC-3986) in the same way by the Wrangler rule which reacts to the "wrangler:install_ruleset_request" event.

We don't need to specify a complete URL for page2.html, because it is understood that the browser will resolve it to its complete value, "https://b1conrad.github.io/krl-sample/page2.html", based on the URL from which page1 was originally loaded. In a browser, the base is implicitly understood (and is available to JavaScript as "location.href"). With Wrangler, we pass the base, which is available to KRL as "meta:rulesetURI", as an attribute for the wrangler install_ruleset_request event.