Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Fix typo in diagram

...

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 "Owner Pico" is created when a pico-engine starts for the first time. We have already created two of these picos, manually. You could go ahead, if you like, and also create the "Student Collection Pico" using the UI.

However, the two other kinds of picos (for individual sections and students) will be created programmatically by the respective collection picos, as needed. The diagram shows only two of each, but in practice there could be thousands of each kind, with each kind managed by the corresponding collection pico.

Our next step 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 the "Registration Pico" (which orchestrates the entire application) 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 send an event back to the registration pico informing it of the ID and ECI of the new section pico when it is ready for use. We'll complete the process of connecting the section pico to other 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".

A very similar ruleset will be needed for the "Student Collection Pico" and this will be left as an exercise.

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

Code Block
linenumberstrue
  rule section_needed {
    select when section needed
    pre {
      section_id = event:attr("section_id")
      exists = ent:sections >< section_id
      eci = meta:eci
    }
    if exists then
      send_directive("section_ready", {"section_id":section_id})
    fired {
    } else {
      ent:sections := ent:sections.defaultsTo([]).union([section_id]);
      raise pico event "new_child_request"
        attributes { "dname": nameFromID(section_id), "color": "#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 type "needed"

Lines 3-7 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"

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

Line 6 binds the local name "eci" to the ECI of the event that is currently being processed (very meta)

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

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

Lines 12-14 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 12 assigns a new value to the entity variable, "union-ing" in the new "id". If the entity variable has not yet been assigned a value, it will start out as an empty array.

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

* local in the sense of being lexically (and temporally) scoped to this rule (execution)

** 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


Code Block
    nameFromID = function(section_id) {
      "Section " + section_id + " Pico"
    }

Define a "__testing" object, such as this one

Code Block
    __testing = { "events":  [ { "domain": "section", "type": "needed", "attrs": [ "section_id" ] } ] }

in the "global" block of your ruleset. 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 refresh 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 ruleset from URL" button beside your ruleset URL.


Use this UI to send the event for section_id "CS462-1" and verify that the id has been added to the entity variable by refreshing the "Rulesets" tab.

Minimize the section collection pico. This will reveal the newly-created child pico.

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:

Code Block
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.

Applications variable accesses are also synchronized safely.

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.pico ruleset as "wrangler". This is done by adding this line to the meta block:


Code Block
use module io.picolabs.pico alias wrangler

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



Code Block
showChildren = function() {
  wrangler:children()
}


This function is nothing but 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 visible via the Sky Cloud API for the pico. We also have to declare that our ruleset shares the function in the meta block.

This allows the functions named in the provides pragma to be seen in the pico's query API.  Now we can call showChildren() using the "Testing" tab, assuming we have updated the ruleset's __testing object.

Maintaining information about child picos

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

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

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

it will need to look like this

Code Block
{
  "CS462-1": {
    "id": "citvkd71y0006hgs0hmyconfb",
    "eci": "citvkd71z0007hgs0yp2vc6ab"
  },
  "CS462-2": {
    "id": "citvkufyg0000jzs06hzze6fb",
    "eci": "citvkufyh0001jzs02fzb3kok"
  }
}

so that we are storing the ID and 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.

Code Block
  rule collection_empty {
    select when collection empty
    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:

Code Block
  rule section_already_exists {
    select when section needed
    pre {
      section_id = event:attr("section_id")
      exists = 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.

Code Block
  rule section_needed {
    select when section needed
    pre {
      section_id = event:attr("section_id")
      exists = ent:sections >< section_id
    }
    if not exists
    then
      noop()
    fired {
      raise pico event "new_child_request"
        attributes { "dname": nameFromID(section_id),
                     "color": "#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. Then, wrangler is written to send an event back to the parent pico, using the code shown here. Note that the code shown below already exists in the wrangler ruleset. Do not add it into your ruleset.

Code Block
      event:send(
        { "eci": parent_eci, "eid": child-initialized,
          "domain": "pico", "type": "child_initialized",
          "attrs": event:attrs() })

The event attributes that it sends have these keys: "parent" (an object with the "id" and "eci" of the parent pico), "new_child" (an object with the "id" and "eci" of the newly created child pico), and "rs_attrs" (the attributes that you sent when you requested that the child pico be created). In this wrangler code, your pico is the one which has an ECI bound to the name parent_eci. So, add a rule in your ruleset to accept the pico child_initialized event. Your code might look something like this.

Code Block
  rule pico_child_initialized {
    select when pico child_initialized
    pre {
      the_section = event:attr("new_child")
      section_id = event:attr("rs_attrs"){"section_id"}
    }
    if section_id.klog("found section_id")
    then
      noop()
    fired {
      ent:sections := ent:sections.defaultsTo({});
      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 "collection/empty" event to re-initialize the ent:sections entity variable.

Now, create a couple of section picos, and verify that you are persisting full information about each one. We have left the writing of a function named sections as an exercise (be sure to add it to the shares and make an entry for it in the __testing structure).


Installing a ruleset in a child pico

Wrangler is prepared to respond to an event, pico/new_ruleset, which can be used to install a ruleset in a pico.

Why would we want to do this? To manage and use the section picos, other picos will need to send them events. The desired behavior in response to such events will be peculiar to section picos (as opposed to picos with different responsibilities), and so should be expressed in a ruleset. This ruleset needs to be installed in each section pico.

Make an empty ruleset named, say, "app_section". This can be accomplished in the "Engine Rulesets" page, by entering app_section in the box with placeholder "ruleset id", and clicking the "add" button. The page will construct the KRL source code for an empty ruleset of that name and register it. At this point, it won't be installed in any picos, but you will do that later, 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. Of course, this ruleset would have additional functionality and you would want to keep it in your repository.

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 our pico child_initialized rule (replacing the noop()).

Code Block
    event:send(
       { "eci": the_section.eci, "eid": "install-ruleset",
         "domain": "pico", "type": "new_ruleset",
         "attrs": { "rid": "app_section", "section_id": section_id } } )

An alternate way to install a ruleset, which is especially useful if it would be the first time the ruleset would be registered, is to specify the URL containing its source code.  The example below assumes that the ruleset which created the pico was itself  installed from a URL (which you pass along as "base"). You don't need to specify a complete URL, but may use a relative URL, much like you do in a web page****.

Code Block
    event:send(
       { "eci": the_section.eci, "eid": "install-ruleset",
         "domain": "pico", "type": "new_ruleset",
         "attrs": { "base": meta:rulesetURI, "url": "app_section.krl", "section_id": section_id } } )


The event is sent to the child pico, and is handled by the ruleset running Wrangler (as implemented by the ruleset "io.picolabs.pico", which is automatically installed in every pico which the node pico engine 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 pico event "ruleset_added" 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 pico event "ruleset_added" by automatically creating a child pico. Your ruleset may wish to respond to the pico event "child_initialized" by installing an appropriate ruleset into the new child pico. Finally, that ruleset might respond to the pico event "ruleset_added" by sending a your_domain event "ready" to its parent. Here is the sequence of events, starting from the moment you use the UI to install "your_ruleset.krl" in your pre-existing (soon to be) parent pico. (Notice that Wrangler (aka io.picolabs.pico) automatically installs the io.picolabs.visual_params ruleset in every pico which it creates.)

The sequence diagram below is meant to be informative, but we will not be writing code for this kind of pattern in this lesson.

Image RemovedImage Added

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, describe the event in the __testing structure and send the event manually from the section collection pico's "Testing" tab.

This could be done by a rule like this one:

Code Block
  rule section_offline {
    select when section offline
    pre {
      section_id = event:attr("section_id")
      exists = ent:sections >< section_id
      eci = meta:eci
      child_to_delete = childFromID(section_id)
    }
    if exists then
      send_directive("section_deleted", {"section_id":section_id})
    fired {
      raise pico event "delete_child_request"
        attributes child_to_delete;
      ent:sections{[section_id]} := null
    }
  }

 The writing of the function "childFromID" is left as an exercise; it should return a map of the form: 

Code Block
{"id": <child's id>, "eci": <child's eci>}

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

Next, see 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 complete form. 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 (that is, the io.picolabs.pico ruleset), to this rule. Notice that the wrangler rule named pico_new_ruleset only requires an attribute named rid, and passes through all of the attributes that it receives, without examining them otherwise.

Code Block
  rule pico_ruleset_added {
    select when pico ruleset_added where rid == meta:rid
    pre {
      section_id = event:attr("section_id")
    }
    always {
      ent:section_id := section_id
    }
  }

**** 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 "pico/new_ruleset" 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 pico event "new_ruleset".