Using a child pico to perform an asynchronous task

Compatibility

This page has not been updated for version 1.0 of the pico-engine

Background

A pico is a persistent compute object, but it doesn't have to persist for a great amount of time to be useful. Here we will show how easy it is to spin up a pico to perform a particular task and then go away.

Motivation

As of this writing, the HTTP library is entirely synchronous. Sometimes, though, a rule will want to make an HTTP post but does not want to wait for the response. We expect that an asynchronous option will soon be provided. In the meantime, we can spawn a child pico to perform the post.

Implementation

Shown here is the rule (with much elided) showing the HTTP post as its action:

  rule handle_connections_request {
    select when sovrin connections_request
    pre {
      ...
      se = ...
      pm = ...
    }
    if se then
      http:post(se,body=pm) setting(http_response)
  }

Lines 8-9 do the HTTP post as a conditional action, waiting for the response.

In this application, we can't wait for the HTTP response. This is because the system to which we sent the post sends nothing of interest in the response, but instead sends us another event (a sovrin:connections_response event), which this same pico will need to handle.

Refactor for DRY

The first step was to refactor the code to use a new rule to post the HTTP request, as shown here:

  rule handle_connections_request {
    select when sovrin connections_request
    pre {
      ...
      se = ...
      pm = ...
    }
    if se then noop()
    fired {
      raise sovrin event "new_ssi_agent_wire_message" attributes {
        "serviceEndpoint": se, "packedMessage": pm
      }
    }
  }

After refactoring, lines 8-13 do the equivalent conditional action, but note, still waiting for the response. Another advantage of this refactoring is that we don't repeat ourselves (DRY) because many other rules in the same ruleset also did the same kind of HTTP post.

And then a new rule was added to the same ruleset, shown here:

  rule send_ssi_agent_wire_message {
    select when sovrin new_ssi_agent_wire_message
    pre {
      se = event:attr("serviceEndpoint")
      pm = event:attr("packedMessage")
    }
    http:post(
      se,
      body=pm,
      headers={"content-type":"application/ssi-agent-wire"}
    ) setting(http_response)
    fired {
      ent:last_http_response := http_response;
      klog(http_response,"http_response")
    }
  }

Since we learned at the same time that we needed a special HTTP header on all such requests, we included it (line 10) during the refactoring. Note that this new rule also saves the HTTP response in an entity variable and logs it. All of the rules which now chain to this new rule benefit from the ability to look at the response, and further changes can be limited to this one place.

The ruleset was extensively tested after the refactoring, giving us confidence in this new rule.

Using a child pico

Next we replaced the raising of the sovrin:connections_response event with an event to Wrangler to create a new child pico:

  rule handle_connections_request {
    select when sovrin connections_request
    pre {
      ...
      se = ...
      pm = ...
    }
    if se then noop()
    fired {
      ent:connReq := ent:connReq.defaultsTo(0) + 1;
      raise wrangler event "new_child_request" attributes {
        "name": "connReq" + ent:connReq, "rids": "org.sovrin.wire_message",
        "serviceEndpoint": se, "packedMessage": pm
      }
    }
  }

The new raised event is very similar, but needs a couple of extra attributes. The new child pico needs a name (which we compute in a way to avoid duplicates for reasons discussed later). We also have Wrangler install a new ruleset in the child pico (also discussed later). Otherwise the code is very similar to our refactored rule.

The child pico ruleset

The key idea is that, once the child pico has been created, we have it immediately perform the HTTP post. Since the post is done by the child pico, the fact that it has to wait for the response doesn't hold up the parent pico, so its handle_connections_request rule completes and the event that triggered it also completes, leaving the parent pico free to handle subsequent events.

Here is the new ruleset, which is installed in the child pico:

ruleset org.sovrin.wire_message {
  ...
  rule on_installation {
    select when wrangler ruleset_added where event:attr("rids") >< meta:rid
    pre {
      wire_message = event:attrs()
      se = wire_message{"serviceEndpoint"}
      pm = wire_message{"packedMessage"}
    }
    if wire_message then noop()
    fired {
      raise sovrin event "new_ssi_agent_wire_message" attributes {
        "serviceEndpoint": se, "packedMessage": pm
       }
    }
  }
  rule send_ssi_agent_wire_message {
    select when sovrin new_ssi_agent_wire_message
    ...
  }
}

The new ruleset contains the familiar send_ssi_agent_wire_message rule, unchanged from before, which can also be removed from the other ruleset because we will always create the child pico to do this work. And, we have tested the rule, so any problems would come from it running in a different pico, rather that some flaw in the rule itself.

When Wrangler creates a new child pico, and installs this new ruleset in it, the wrangler:ruleset_added event is raised in the child pico. The rule in lines 3-16 reacts to this event, recovers the original attributes sent to create the child pico (lines 6-8) and chains to the send_ssi_agent_wire_message rule (lines 12-14).

Decommissioning the child pico

The changes shown so far work, in that when an HTTP post is needed, a child pico is created and it sends the post request and waits for the response. The only problem is that after a few rounds of testing of the main ruleset, its pico now has several child picos (each needing a unique name) which are no longer needed. In initial testing, we just removed these manually, after checking the logs and entity variables for the expected HTTP responses.

We can easily remove the child picos automatically:

ruleset org.sovrin.wire_message {
  ...
  rule on_installation {
    ...
  }
  rule send_ssi_agent_wire_message {
    ...
    fired {
      ent:last_http_response := http_response;
      klog(http_response,"http_response");
      raise sovrin event "mission_accomplished"
        if http_response{"status_code"}.match(re#^2\d\d$#)
    }
  }
  rule done_so_cease_to_exist {
    select when sovrin mission_accomplished
    pre {
      eci = wrangler:parent_eci()
    }
    if eci then
    event:send({"eci": eci, "domain": "wrangler", "type": "child_deletion",
      "attrs": {
        "id": meta:picoId.klog("id"),
        "name": wrangler:name().klog("name")}
    })
  }
}

We chain from the second rule to a new rule in lines 11-12, provided the HTTP response was successful (in the 200 range).

The new done_so_cease_to_exist rule (lines 15-26) reacts to the raised event by getting a channel to the parent pico (line 18) and sending it the wrangler:child_deletion event (lines 21-25).

Another possibility would be to report success (or failure for that matter) to the parent pico and let it delete the child pico.

Copyright Picolabs | Licensed under Creative Commons.