Wrangler

Note: Since the pico-engine NEXT changes the major version number from 0 to 1, the KRL language will experience breaking changes. Code that ran in 0.52.4 will have to be changed to run correctly in 1.0.0.

Overview

The rulesets which make up Wrangler are a kind of operating system for picos. Every pico that is created within a pico-engine has these three rulesets installed:

  • io.picolabs.wrangler for managing pico life cycle, rulesets, and channels

  • io.picolabs.subscription for managing subscriptions between arbitrary picos

  • io.picolabs.pico-engine-ui for holding information needed by the developer UI

This page documents the wrangler ruleset, and deals with parent/child relationships, rulesets within a pico, and creation and deletion of channels.

Pico life cycle

The engine creates the first pico, the root pico, when it starts for the first time. Any pico can create a child pico. A pico can have one of its children deleted. It is not possible to delete the root pico.

Creating a child pico

This always begins with a pre-existing pico which will become the parent of the new pico, its new child pico. Somehow, the wrangler:new_child_request event needs to be sent to, or raised within, the parent pico.

The process is fairly involved, and is diagrammed below. This illustrates the case where the developer using the developer UI provides a name and background color in the About tab of the parent-to-be. Notice that the UI sends the engine_ui:new event to the pico. That event checks the event attributes, supplying defaults as needed, and raises the wrangler:new_child_request event (with required attribute name), to which a rule in the wrangler ruleset reacts as shown here:

The rule creates the new child pico (shown by a green arrow), and sends it a sequence of events:

  1. the engine_ui:setup event, fielded by the engine-ui ruleset which creates a new channel suitable for use by the UI, and returns that channel’s ID which is bound to the name newUiECI

  2. the wrangler:pico_created event, fielded by a wrangler rule, which saves the name, and installs the subscription ruleset (shown by a green arrow)

  3. the engine_ui:box event, fielded by the engine-ui ruleset which stores away the name and background color so that the new pico can be correctly rendered in the developer UI

Finally that rule raises a wrangler:new_child_created event for the parent pico. And, the child pico, after it has finished with its work, sends a wrangler:child_initialized event back to the parent pico.

Note that in the diagram, these are “received” in the parent pico with a big “X” meaning that there are no rules in the wrangler ruleset which select on this events. Developer-supplied KRL rulesets which initiate a child creation are free to include rules which react to one or both of these events.

While a pico exists

Between the time a pico is created and the time it is deleted, we may need to get information about it, its parent, and its children. Wrangler includes functions which produce this information. In KRL terms, wrangler both provides (when used as a module) and shares (with the outside world) these functions.

Finding parent pico

Wrangler has a function, parent_eci, which returns the ECI that the pico can use to reach its parent pico.

Finding child picos

Wrangler has a function, children, which returns an array of maps. Each map has information about the pico’s child picos.

For example, from the Section Collection pico, this array looks like this:

1 2 3 4 5 6 7 8 9 10 11 12 [ { "eci": "ckj63invt009kpq2r37jn0qe1", "name": "Section C S 462-1 Pico", "parent_eci": "ckj63invt009jpq2rdftl1or8" }, { "eci": "ckj693euj003dm62rauobdtth", "name": "Section C S 462-2 Pico", "parent_eci": "ckj693euj003cm62r3ncf9a5p" } ]

Finding information about this pico

Wrangler has a function, myself which returns a map with information about this pico. For example, again for the Section Collection pico:

1 2 3 4 5 { "name": "Section Collection Pico", "id": "ckj63hxd20081pq2r9mvgb8r8", "eci": "ckj63hxd20083pq2r4ljj66ls" }

Deleting a pico

A pico can only be deleted by its parent pico.

Deleting a child pico

This means that the root pico cannot be deleted, since it doesn’t have a parent. However, it is possible to destroy all picos, including the root pico, by removing the pico-engine database. Assuming the standard location of the pico-engine home folder, this can be done with this command:

1 rm -rf ~/.pico-engine/db

This command should be used only a last resort, because all picos, not just the root pico will be destroyed. When the pico-engine is started again, a new root pico will be created.

There is no way to undo the deletion of a pico. When it is deleted all of its state (entity variables), channels, subscriptions, etc. will be gone.

A rule running in a parent pico can delete a child pico by raising the wrangler:child_deletion_request with an attribute named “eci” which is the ECI given by the children function.

A pico can delete itself

Wrangler reacts to the wrangler:ready_for_deletion event by finding this pico’s child ECI and sending its parent the wrangler:child_deletion_request event.

The KRL programmer may find it useful to finish up activities in progress, release resources, etc. and then request deletion in this manner.

Repertoire of parent/child creation/deletion events

Events to which wrangler reacts

event

raised by

attributes

event

raised by

attributes

wrangler:new_child_request

UI or KRL application

required name, optional backgroundColor, optional additional attributes to be passed through

wrangler:pico_created

Wrangler (not for use by KRL programmers) to the new child pico

name (of child pico)

wrangler:child_deletion_request

UI or KRL application

required eci of family channel from parent to child

wrangler:ready_for_deletion

KRL application

none (implicit)

Events which wrangler raises

event

to/from

attributes

event

to/from

attributes

wrangler:new_child_created

internal to parent pico

passed through from initialwrangler:new_child_request

and the eci of the new child pico

wrangler:pico_initialized

internal to child pico

wrangler:child_initialized

from child pico to parent pico

wrangler:child_deleted

internal to parent pico

passed through from initial wrangler:child_deletion_request

The KRL programmer may choose to write rules that react to these raised events.

Pico Channels

A pico can have as many inbound channels as it needs. See Managing Channels for more information. A pico can only create and delete channels for itself.

Creating a new channel

Synchronous creation

Wrangler provides an action, createChannel, for creating a new channel. This requires three arguments: an array of strings, named “tags”, a map describing the new channel’s event policy, named ”eventPolicy”, and a map describing the new channel’s query policy, named ”queryPolicy”.

For example, this KRL code creates a channel when the app_section_collection ruleset is installed into a pico (see the Pico to Pico Subscriptions Lesson for context):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ruleset app_section_collection { ... global { ... tags = ["app_section_collection"] eventPolicy = { "allow": [ { "domain": "section", "name": "*" }, ], "deny": [] } queryPolicy = { "allow": [ { "rid": meta:rid, "name": "*" } ], "deny": [] } } rule initialize_section_collection_pico { select when wrangler ruleset_installed where event:attr("rids") >< meta:rid if ent:section_collection_pico_eci.isnull() then wrangler:createChannel(tags,eventPolicy,queryPolicy) setting(channel) fired { ent:section_collection_pico_eci := channel{"id"} ent:sections := {} } } ... }

The channel is created in line 19, with the specified tags and policies. The action returns a map representing the channel. The ECI of the channel is its ”id” which, in this case, is stored in an entity variable in line 21.

Asynchronous creation

Wrangler provides a rule which reacts to a wrangler:new_channel_request event, which must be given attributes for tags, eventPolicy, and queryPolicy.

Once the new channel has been created, wrangler raises the wrangler:channel_created event with one attribute, channel, bound to the map describing the channel.

Here is a way to replace the initialize_section_collection_pico rule that would also create the needed channel (assuming no change to the global block), and initialize the entity variables:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 rule initialize_section_collection_pico { select when wrangler ruleset_installed where event:attr("rids") >< meta:rid if ent:section_collection_pico_eci.isnull() then noop() fired { raise wrangler event "new_channel_request" attributes { "tags":tags, "eventPolicy":eventPolicy, "queryPolicy":queryPolicy, } } } rule finish_initialization_of_section_collection_pico { select when wrangler channel created pre { channel = event:attr("channel") } fired { ent:section_collection_pico_eci := channel{"id"} ent:sections := {} } }

Finding channels

Wrangler provides a function, channels, for obtaining channels.

If called with no arguments, the function will return an array of all channels for this pico.

If called with tags, the function will return an array of all channels with those tags. The tags can be specified either as an array of strings, or as a comma-separated string.

For example, assuming your ruleset is using io.picolabs.wrangler with alias wrangler, this code will find the family channel by which your pico is known to its parent:

1 2 my_child_channel = wrangler:channels("system,child") my_child_eci = my_child_channel.head().get("id")

Line 1 binds an array of channels with those tags to the name my_child_channel. Since there should only be one such channel, the array operator head() will return that channel, whose ”id” is the ECI of the channel. That ECI will be bound to the name my_child_eci (line 2).

Deleting a channel

When a channel is no longer needed, or if you wish to rotate in the same channel with a different ECI, you may need to delete a channel.

Synchronous deletion

Wrangler provides an action, deleteChannel, to delete an existing channel. The only argument is the ECI of the channel to be deleted.

Asynchronous deletion

Wrangler has a rule that reacts to the wrangler:channel_deletion_request event, expecting a single attribute, eci, containing the ID of the channel to be deleted.

Once the new channel has been deleted, wrangler raises the wrangler:channel_deleted event with one attribute, eci, bound to the ECI of the deleted channel.

Rotating a channel

You create a channel for a specific purpose, and communicate its ECI to an application which will use it to send events and/or queries to your pico. If someone discovers the ECI and begins to use it maliciously, you can respond by creating a different channel (and hence a different ECI) for the same purpose. Then you would communicate the new ECI to legitimate users so that they can continue operation. Malicious users will be shut out because the old ECI will no longer be recognized by the pico-engine.

One way to create a new channel that duplicates a specific channel, given the tags of the pre-existing channel:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 rule dupChannel { select when rotate need_duplicate_channel pre { tags = event:attrs{"tags"} channel = wrangler:channels(tags).head() } if channel then noop() fired { raise wrangler event "new_channel_request" attributes { "tags":channel{"tags"}, "eventPolicy":channel{"eventPolicy"}, "queryPolicy":channel{"queryPolicy"}, "old_eci":channel{"id"}, } } } rule ackChannel { select when wrangler channel_created pre { channel = event:attrs{"channel"} old_eci = event:attrs{"old_eci"} } if channel then send_directive("new channel",{"eci":channel{"id"}}) fired { raise wrangler event "channel_deletion_request" attributes { "eci":old_eci, } } } rule ackDeletion { select when wrangler channel_deleted send_directive("channel deleted",{"eci":event:attrs{"eci"}}) }

Note in line 5, the use of the array operator head() to convert the array of channels returned by the wrangler:channels() function into a single channel map.

Raising the wrangler:new_channel_request event (lines 9-14) includes both the required attributes and one additional attribute, named old_eci (line 13), which will be needed later and which is passed through the rule in wrangler, when it raises the wrangler:channel_created event.

Raising the rotate:need_duplicate_channel event to these rules will result in directives:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "directives": [ { "type": "directive", "name": "new channel", "options": { "eci": "ckjcc1uwq008g2n2r64km5sqx" } }, { "type": "directive", "name": "channel deleted", "options": { "eci": "ckjcc0dtg007p2n2r9ere1kr8" } } ] }

giving a new ECI and confirming deletion of the old.

Pico Rulesets

Picos have behavior and state thanks to rulesets installed in them. Wrangler provides functions to install a ruleset into the current pico.

The ruleset is the unit of compilation for KRL, and is stored somewhere on the Internet, identified by an HTTP URL, or on the local machine, identified by a FILE URL.

Ruleset installation

Wrangler reacts to the wrangler:install_ruleset_request to install a specified ruleset into the pico. It takes two forms:

  1. When given one attribute, url, it installs the KRL code at that URL into the pico.

  2. When given attributes absoluteURL and rid, it resolves a URL substituting the given rid as a KRL filename based on the absoluteURL. It installs the KRL code at that URL into the pico.

The second form is useful when the KRL programmer has several rulesets which operate together in a pico-based system. It allows the bootstrapping of the system by installing a single ruleset which then installs all of the others using simple RIDs instead of absolute URLs.

In either form, wrangler raises the wrangler:ruleset_installed event, passing rids, an array of RIDs containing the RID of the ruleset just installed.

The KRL programmer can react to this event, knowing it to be the first event the pico will receive after the installation. This is one way to initialize the state of the pico wrt this ruleset. A common idiom looks like this:

1 2 3 4 5 rule initialize_state { select when wrangler ruleset_installed where event:attrs{"rids"} >< meta:rid // initialize state, etc. }

Note that it is important to select this rule conditionally (in the where clause of line 3) so that it only evaluates for the expected RID. The pico will get a chance to react to the event every time any ruleset is installed in this pico, so it is important to only react for this one (i.e. meta:rid).

Finding installed rulesets

Wrangler provides a function, installedRIDs, which returns an array of the RIDs installed in the pico.

Information from the meta block

Wrangler provides a function, rulesetMeta, which when given a RID as its rid argument, returns a map containing all of the information provided in the meta block of the ruleset.

For example, wrangler:rulesetMeta(“hello_world”) (which refers to the ruleset shown in the Pico Engine Quickstart page), would return this map:

1 2 3 4 5 6 7 8 9 { "name": "Hello World", "description": "\nA first ruleset for the Quickstart\n", "author": "Phil Windley", "logging": true, "shares": [ "hello" ] }

Ruleset flushing

The KRL source code for a ruleset is a resource stored somewhere on the Internet accessible using HTTP or HTTPS (or a file on your local machine accessible using the file:// protocol). When you know that the resource (or file) has changed, you need to let the pico know that it needs to be “flushed”.

Rulesets installed in a pico can be flushed manually in the “Rulesets” tab of the developer UI. When it is necessary for your KRL application to do this programmatically, wrangler can help.

Flushing all rulesets in a pico

Wrangler provides a rule which reacts to the wrangler:rulesets_need_flushing event. It will cause the pico-engine to recompile all rulesets whose code has changed. It does this by looking at the hash of KRL retrieved from each ruleset’s URL, and if that has changed, it will be recompiled.

Note that this affects all picos which have installed a ruleset from that same URL. Only the rulesets in this pico will be checked and recompiled.

When the operation is complete, wrangler will raise the wrangler:rulesets_flushed event with no attributes.

Ruleset uninstallation

Wrangler reacts to the wrangler:uninstall_ruleset_request, with a required attribute, rid, by uninstalling that ruleset.

It raises the wrangler:ruleset_uninstalled event, passing through all attributes of the original uninstall event. The KRL programmer can write a rule which selects on that event, as needed.

picoQuery

Wrangler provides and shares a function named picoQuery which is a user friendly way to make a request between picos. picoQuery uses ctx:query() if the pico is on the same host and HTTP if it is not. When using HTTP, it provides cleanup and returns the error if the returned HTTP status was not 200.

The skyQuery() function in Wrangler has been deprecated. skyQuery() only did HTTP calls. Pico engine v1.X has tightened up channel policies, so that HTTP can’t be used on family channels (channels between parents and children). And making HTTP calls to picos on the same server is inefficient. Since picoQuery() and skyQuery() use the same parameters in the same order, you can like switch to using picoQuery() merely by changing the name. The skyQuery() function remains available for backward compatibility.

picoQuery() is mainly used to programmatically call functions inside of other picos from inside a rule. However, deadlocks are possible due to its synchronous nature (e.g. do not let two picos query each other simultaneously). See Accessing a Function Shared by Another Pico for more information.

Parameters

Optional parameters are italicized.

Parameter

Datatype

Description

Parameter

Datatype

Description

eci

String

The ECI to send the query to

mod

String

The RID of the ruleset to send the query to

func

String

The name of the function to query

params

Map

The parameters to be passed to the function on the target pico. Given as a map with parameter name as the key and argument as the value.

_host

String

The host of the pico engine being queried.
Note this must include protocol (http:// or https://) being used and port number if not 80.
For example "http://localhost:3000", which also is the default.

_path

String

The sub path of the url which does not include mod or func.
For example "/sky/cloud/", which also is the default.

_root_url

String

The entire URL except eci, mod , func.
For example, dependent on _host and _path is
"http://localhost:3000/sky/cloud/", which also is the default. Defaults to meta:host (the pico engine itself).

If _host is missing or the same as the result returned by meta:host, picoQuery() will use ctx:query() instead of HTTP.

Returns

Success: the result of the function queried for.

Failure: a map of error information which contains the error and other optional information:

1 2 3 4 5 6 {     "error":"general error message",     "picoQueryError":"The value of the 'error key', if it exists, of the function results"     "picoQueryErrorMsg":"The value of the 'error_str', if it exists, of the function results"     "picoQueryReturnValue":"The function call results" }

This map represents a breaking change. The previous map returned skyQueryError instead of picoQueryError, for example. If your code depends on these, you will need to update to use the new error map.

Example

1 2 3 4 5 6 7 8 9 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(); // Check that it did not return an error fired {   // process using answer }