Pico to Pico Subscriptions Lesson

Learning Objectives

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

  • Explain what a pico subscription is

  • Understand pico-to-pico subscriptions

  • Use the developer tools to manage pico subscriptions

  • Programmatically link picos using a subscription and have them interact. 

Motivation

In the previous lesson, you learned about creating pico parent-child relationships. However, in distributed systems, not everything is a parent-child relationship. In fact, there are many systems which are based off of peer-to-peer relationships.

Each node in a peer-to-peer system needs some way to communicate with a different node independent of a parent child hierarchy. For example, a flower store may wish to create a relationship with a delivery driver, so that the store can broadcast a new delivery request, and the driver can bid on delivering that request. This is where more general relationships, which we call subscriptions, come in. 

Subscriptions are a two-way link from one pico to another independent of the parent-child hierarchy. This lesson will show you how to create these subscriptions among picos so that they may communicate directly with each other rather than through some other medium.

Prerequisites 

You should have already completed the following lessons:

Contents

Video Tutorial

Subscriptions

A subscription has a name and two channels, one from each pico participating in the subscription to the other. Each side in the subscription can be given a role to further identify the purpose of the subscription relationship. 

In this diagram, from the perspective of pico A, the top channel is the one on which it receives incoming events and queries, so A knows this channel as Rx, and the bottom channel is the one on which pico A transmits events and queries to pico B, so A knows this channel as Tx. Conversely, pico B knows the top channel as Tx and the bottom channel as Rx. Each pico has its own perspective on the subscription.

Subscriptions may also include a role for each pico. From the perspective of pico A, its role in the subscription is known as Rx_role while pico A knows the role of pico B as Tx_role. On the other hand, from pico B's perspective, the role of pico A is Tx_role and its own role is Rx_role. Putting yourself in a pico's place, you can think of Rx_role as my role and Tx_role as the other's role.

Managing Subscriptions Manually with the Testing tab

The experimental setup

In this section, we'll continue the registration example. Here the owner pico has two child picos, one belonging to Jane, a student, and the other being the registration system.

What we need is a relationship between Jane's pico and each section in which she is registered.

The subscriptions which we will create to enable this pico to pico communication are shown here as dashed magenta lines. Here there is a relationship between Jane's pico and the pico for American Heritage 100 section one.

Prepare for this section by (re-)creating the registration system that you built in the previous lesson.

  1. Install this ruleset into your section collection pico: https://raw.githubusercontent.com/Picolab/pico_lessons/master/subscriptions/app_section_collection.krl

  2. Create a pico for a student

  3. Be sure the io.picolabs.subscription ruleset is installed in the student pico and in the section picos you have created

Initiating subscriptions by introduction

To establish a subscription relationship, wrangler will set up a new channel in each of the two picos. To bootstrap this process, we need to use an existing channel in each of the picos.

Finding a well-known channel

Start by determining the ECI of the "well-known" subscription channel of the section pico. Visit the section pico, select the ECI which is tagged "#wellknown_rx #tx_rx" and for the io.picolabs.subscription ruleset, click on the "wellKnown_Rx" button. In the Results section, you will see the channel. Double-click on the "id" value and copy it. We'll use it in the actual introduction step.

Setting up an invitation

Next, select your student pico, and go to its Testing tab, using its well-known ECI. Find the form for the wrangler:subscription event and provide some values:

  1. Paste the ECI of the section pico into the box labelled "wellKnown_Tx"

  2. Provide this pico's role: in the box labelled "Rx_role" enter "student"

  3. Provide the other pico's role: in the box labelled "Tx_role" enter "class"

  4. Provide a name for the subscription. For example, "Jane–A HTG 100-1"

  5. For the type, enter "subscription"

  6. Since both picos are running in the same pico-engine, you can leave the box labelled "Tx_host" blank

  7. Also leave the password blank (it is used with auto configuration, discussed elsewhere)

When you are ready, you screen should look like this, and you can click the "wrangler:subscription" button:

Creation of an inbound channel

Now, let's examine the situation. First, go to the student pico's Channels tab. Notice that there is a new channel created for this subscription. This is this pico's Rx channel (and as we'll see later also the other pico's Tx channel):

Outbound and Inbound subscriptions

Next, visit the Testing tab, and select the ECI of the new subscription channel. Click on the "outbound" button:

The subscription is considered "outbound" because it has been proposed to the other pico (identified by its well-known ECI), but not yet accepted by it. To manually accept the subscription, visit the other pico, the section pico.

Now if the subscription is "outbound" in this pico, it must be "inbound" in the section pico, right? To verify, visit the Testing tab of the section pico, and using its new Rx channel, click on the "inbound" button. Compare this view of the subscription with the outbound view in the student pico.

Note that the "Id" is the only field which has the same value on both ends of the subscription. This is because it is the ID of the subscription itself. There is only one subscription, but it has two ends. The "Rx_role" and "Tx_role" are swapped, as are the two ECIs, "Tx" and "Rx". You will note that the initiating pico does not yet have access to the other pico's ECI. This is because the subscription hasn't yet been accepted.

Accepting an inbound subscription

Double click on the "Id" value, scroll up until you see the form for the wrangler:pending_subscription_approval event, past in the ID value and click the button.

Then scroll back to the top and click on the "established" button to see that the subscription is now established.

Verifying an established subscription

Finally, visit the student pico's Testing tab, and verify that it too sees the subscription as established.

Notice that the student pico has two established subscriptions, both with the same role. Double check that the "Id" value is the same in this pico's (second) subscription and the section pico's subscription. The other values are swapped, reflecting the opposite perspective each end of the subscription has. You can also verify that the second "Rx" value is the same as the ECI selected in the Testing tab.

Managing Subscriptions Programmatically

In the previous section, you learned how to create a subscription manually, using the Testing tab. Here we will learn how to write KRL code to create an inbound subscription and accept it.

It is possible to initiate a subscription with a rule running in a pico which already has in its possession the ECI's of the two picos which need to be related by the subscription.

The well-known channel

Every pico has the io.picolabs.subscription ruleset installed. This ruleset, on installation, creates a well-known channel with a limited policy. This is by design so that the ECI of that channel can be widely shared, as it can only be used to initiate subscriptions.

In this example, we will write code in a ruleset for the student picos which will create a subscription to the Section Collection pico. Then we will use that to create a subscription between the student pico and the section picos in which they wish to enroll.

Our first problem will be to find the well-known ECI of the section pico we want. When we did this manually, we just went to the section pico and found its well-known ECI in the developer UI. But to use this well-known (so named) ECI programmatically, we have to be able to find it! It needs to be published somewhere, where we can get at it.

One way to do this is to have a new pico inform its parent pico of its well-known ECI, and then other picos can get it from there.

Recording well-known channels IDs for section picos

Here is a modification to a rule that is already in the app_section ruleset to do this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 rule pico_ruleset_added { select when wrangler ruleset_installed where event:attr("rids") >< meta:rid pre { section_id = event:attr("section_id") parent_eci = wrangler:parent_eci() wellKnown_eci = subs:wellKnown_Rx(){"id"} } event:send({"eci":parent_eci, "domain": "section", "type": "identify", "attrs": { "section_id": section_id, "wellKnown_eci": wellKnown_eci } }) always { ent:section_id := section_id } }

The new lines are: 6, where we get the family channel ECI for our parent (the Section Collection pico); 7, where we get the well-known ECI from the subscription ruleset (be sure to include a use module io.picolabs.subscription alias subs line in the meta block); 10-16 which send a section:identify event to the parent pico.

And, then this new rule in the app_section_collection ruleset to react to that event:

1 2 3 4 5 6 7 8 9 rule accept_wellKnown { select when section identify section_id re#(.+)# wellKnown_eci re#(.+)# setting(section_id,wellKnown_eci) fired { ent:sections{[section_id,"wellKnown_eci"]} := wellKnown_eci } }

So now, the sections function of that ruleset will show us this kind of structure:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "C S 462-1": { "eci": "ckit4ayd20051qb2r8icddvwh", "wellKnown_eci": "ckit4aydf0054qb2radgwbbj2" }, "C S 462-2": { "eci": "ckit4b5it005dqb2r3534e5l3", "wellKnown_eci": "ckit4b5j6005gqb2rawjb3ucs" }, "A HTG 100-1": { "eci": "ckit4bbt6005pqb2rfkid1bdc", "wellKnown_eci": "ckit4bbti005sqb2ra8spfeqk" } }

And the well-known ECIs for the sections are now better known. And when we need them, we can ask the Section Collection pico for them by name (aka the section id).

Recording the well-known channel ID of the Section Collection pico

Similarly, we can have the Section Collection pico report its well-known ECI to the Registration pico.

The code is left as an exercise for the reader. It's a bit simpler, because there's only one such pico known to the Registration pico.

Creating student picos

Now, we follow the journey of well-known ECIs another step.

We have the Registration pico create a student pico when a student "arrives" to register. Here's the rule (for the app_registration ruleset):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 rule allow_student_to_register { select when student arrives name re#(.+)# setting(name) pre { backgroundColor = event:attr("backgroundColor") || "#CCCCCC" } event:send({"eci":wrangler:parent_eci(), "domain":"wrangler", "type":"new_child_request", "attrs":{ "name":name, "backgroundColor": backgroundColor, "wellKnown_Rx":ent:sectionCollectionECI } }) }

Here, the rule selects on the event (lines 2-3), provides a default color (line 5), and sends an event to its parent pico, asking for the creation of a new student pico (lines 7-14).

The well-known ECI of the Section Collection pico is shared with the new student pico by virtue of line 12. You may need to change this if you stored the ECI in a different entity variable.

Subscription between student and Section Collection

With a well-known ECI in hand, we can write code to request/propose a subscription from the new student pico to the Section Collection pico. We do this by reacting to the wrangler:ruleset_installed event in the app_student ruleset:

1 2 3 4 5 6 7 8 9 10 11 12 rule capture_initial_state { select when wrangler ruleset_installed where event:attr("rids") >< meta:rid if ent:student_eci.isnull() then wrangler:createChannel(tags,eventPolicy,queryPolicy) setting(channel) fired { ent:name := event:attr("name") ent:wellKnown_Rx := event:attr("wellKnown_Rx") ent:student_eci := channel{"id"} raise student event "new_subscription_request" } }

We create a channel for the newly created student pico in line 5 (guarded by line 4 so we don't do it twice), and store away the incoming well-known channel for future use in line 9. Then, we raise an event so that we will create a new subscription with the Section Collection pico. We'll see later how we will use that subscription once it is established.

It is a separate rule so that we can raise the event if and when we need a new subscription:

1 2 3 4 5 6 7 8 9 10 11 rule make_a_subscription { select when student new_subscription_request event:send({"eci":ent:wellKnown_Rx, "domain":"wrangler", "name":"subscription", "attrs": { "wellKnown_Tx":subs:wellKnown_Rx(){"id"}, "Rx_role":"registration", "Tx_role":"student", "name":ent:name+"-registration", "channel_type":"subscription" } }) }

Here the student pico sends an event to the Section Collection pico, using its ECI (line 3). So it is as if were were in the Testing tab of that pico entering the rest of the fields for the wrangler:subscription event. That is, the invitation is being done from the perspective of the Section Collection pico, even though it is being sent by the student pico. So, the wellKnown_Tx is the student pico's well-known ECI (line 6), and the role of the Section Collection pico is "registration" while the other role is "student" (line 7). Once we have sent the event, the student pico will have a new channel

and also a new inbound subscription:

Programmatically accepting an inbound subscription

You'll remember that the next manual step was to copy the "Id" value and paste it into the form for the wrangler:pending_subscription_approval event. We can do that with a rule that selects on the wrangler:inbound_pending_subscription_added event:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 rule auto_accept { select when wrangler inbound_pending_subscription_added pre { my_role = event:attr("Rx_role") their_role = event:attr("Tx_role") } if my_role=="student" && their_role=="registration" then noop() fired { raise wrangler event "pending_subscription_approval" attributes event:attrs ent:subscriptionTx := event:attr("Tx") } else { raise wrangler event "inbound_rejection" attributes event:attrs } }

The event is raised to us when the inbound subscription arrives. We check it out to make sure it is what we expect (lines 3-7) and if so, we raise the wrangler:pending_subscription_approval event (lines 9-10) and the subscription become established at both ends, and we save away the ECI for use with the Sections Collection pico (line 11). In case it is not an inbound subscription we are expecting, we raise the wrangler:inbound_rejection event to refuse it (lines 13-14).

Writing rules that use subscriptions

Now that we have a relationship with the Section Collection pico, we (a student pico) can use it to add a class.

Student pico requests to add a class

Here is a rule for the app_student ruleset:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 rule add_a_class { select when section add_request section_id re#(.+)# setting(section_id) pre { already_enrolled = classes() >< section_id } if not already_enrolled then event:send({"eci":ent:subscriptionTx, "domain":"section", "name":"add_request", "attrs":{ "wellKnown_Tx":subs:wellKnown_Rx(){"id"}, "section_id":section_id, "name":ent:name } }) }

We first check to ensure that we don't already have an enrollment for that section (line 7). Then we send (in lines 8-15) a section:add_request event to the Section Collection pico (line 8, using the saved ent:subscriptionTx ECI) and passing our own well-known ECI (line 11) along with the section ID and our name.

Notice that we use the same event, section:add_request for student picos and for the Section Collection pico. We are considering a single application, registration, but the code is distributed across several rulesets for the various kinds of entities.

Section Collection pico passes it on to the section pico

Here is a rule for the app_section_collection ruleset, which is installed in the Section Collection pico. It will react the event that student picos send to it:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 rule introduce_section_to_student { select when section add_request pre { section_id = event:attr("section_id") name = event:attr("name") } event:send({"eci":ent:sections{[section_id,"wellKnown_eci"]}, "domain":"wrangler", "name":"subscription", "attrs":{ "wellKnown_Tx":event:attr("wellKnown_Tx"), "Rx_role":"section", "Tx_role":"student", "name":name+"-"+section_id, "channel_type":"subscription", "section_id":section_id } }) }

This rule binds short names to two incoming attributes in its prelude (line 3-6). Then it sends a wrangler:subscription event to the appropriate section pico (lines 7-15), using the saved well-known ECI for that section (line 7), and passing along the student pico's well-known ECI (line 10), appropriate roles for the subscription (line 11), a name and channel_type (line 12), and passes through the section_id (line 13).

Section pico proposes a subscription

The section pico will react to this event by proposing a subscription to the student pico. We don't need to add any code to the app_section ruleset, because this event is handled by the io.picolabs.subscription ruleset.

Student pico accepts the subscription adding the class

This rule for the app_student ruleset will see the inbound subscription and automatically accept it:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 rule auto_accept_add { select when wrangler inbound_pending_subscription_added Rx_role re#^student$# Tx_role re#^section$# pre { section_id = event:attr("section_id") } if section_id then noop() fired { ent:classes{event:attr("Id")} := section_id raise wrangler event "pending_subscription_approval" attributes event:attrs last } }

Lines 3-4 declare that the rule is to be selected only when there are attributes Rx_role and Tx_role which have the values "student" and "section" respectively. The local name section_id is bound (line 6) to the attribute value of the same name, and if there is one (line 8), then we record (in line 10) both the "Id" of the subscription and the section ID in an entity variable named ent:classes, a map whose keys are subscription identifiers and whose values are section IDs. Then, lines 11-12 accept the inbound subscription, completing the relationship between this student pico and that section pico.

Special notice to line 13, which ends handling of the wrangler:inbound_pending_subscription_added event. This is very important. The auto_accept_add rule must be placed in the ruleset before the auto_accept ruleset (see its definition earlier (the section entitled "Programmatically accepting an inbound subscription")). Both of these rulesets will select on that event, and if this one fires, the other one must not be evaluated. That is the purpose of the last keyword, meaning that this is the last rule which is to be evaluated for the event.

Maintaining a class list

The entity variable ent:classes is a map whose values are the classes we have added. So this function will provide our current class list (in the app_student ruleset):

1 2 3 classes = function(){ ent:classes.values() }

Be sure to share the function name in the meta block of the ruleset.

Deleting subscriptions

To delete a subscription, either of the picos which participate in the subscription can raise the event wrangler/subscription_cancellation including either the attribute named "Tx" or the subscription ID. For example, when a student wants to drop a class, it might use this rule (in the app_student ruleset):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 rule drop_a_class { select when section drop_request pre { section_id = event:attr("section_id") Id = ent:classes .filter(function(x){x==section_id}) .keys() .head() } if Id then noop() fired { clear ent:classes{Id} raise wrangler event "subscription_cancellation" attributes {"Id":Id} } }

When a student pico receives a section:drop_request event, this rule will be selected (as declared in line 2). Line 4 binds a local name to the passed in attribute, section_id. Lines 5-8 compute the subscription identifier that corresponds to that section: operating on the ent:classes entity variable (line 5), we first filter, retaining those entries in the map whose value equals the passed in section ID (line 6), giving us a smaller map containing only those entries from which we get an array of the keys of that smaller map (line 7), from which we retain only the first entry (line 8). Assuming we find such a value, it will be the identifier of the subscription between our pico and the corresponding section pico. We ask for the subscription to be deleted (lines 12-13), severing the relationship between the student and the section. Finally, we delete the entry from our ent:my_classes map.

The raised event triggers a rule in the io.picolabs.subscription ruleset in this pico. This rule takes care of deleting both sides of the subscription.

Managing Subscriptions in the UI

We will create a new pico named "Delta" to illustrate this section. Notice that it begins with the normal complement of channels, including a well-known channel for subscriptions, "ckja93foz0016uq2r17dt782z":

It also shows a "Subscriptions" tab, which initially looks like this, with its well-known channel prominently displayed:

Now, by double-clicking on the ECI of the well-known channel, the developer can select it and then copy it. Now, it takes two picos to make a subscription, so let's visit another pico, say Beta, and its Subscriptions tab, which shows that it already has one established subscription:

We create a new subscription by filling in the fields of the New Subscription form, with the copy/pasted ECI of Delta's well-known channel, "greek" as both Tx_role and Rx_role, with the name "Delta-Beta" and a channel_type of "subscription". When you click the "Add" button, you will have an "Outbound" subscription along with the "Established" subscription.

Meanwhile, back in the Delta pico, we see the "Inbound" subscription:

Clicking on the checkbox opens it up and we see the details of the proffered subscription, along with an "Accept" button:

Notice that Delta has a new channel, with tags "delta-beta, subscription" and the ECI "ckjc0hor000ko122r8ghp56ty" (which corresponds to the "Rx" field of the subscription):

Clicking on the "Accept" button establishes the subscription:

Closing the pico, we see the two subscriptions, as magenta lines, in the topological view of the developer UI: