Send a Message Between Actors

Introduction

While standalone Actors are useful, one can also network multiple Actors to generate more complex workflows. Actors have the ability to send messages to other Actors, allowing developers to chain together workflow steps, each contained in a separate Actor.

In this section of the tutorial, we will deploy a simple upstream-messenger Actor that sends a message to the hello-world-actor that we deployed in the previous section. We will demonstrate that the downstream hello-world-actor runs after it receives the message from upstream-messenger.

Create a New Actor Named upstream-messenger

First, let’s write the code that our new Actor will run on execution. Instead of manually writing these files, we can simply adapt one of the provided Actor templates. To view all available Actor templates, issue:

$ tapis actors init -L
+--------------------+--------------------+------------------------------------------------------------+----------+
| id                 | name               | description                                                | level    |
+--------------------+--------------------+------------------------------------------------------------+----------+
| default            | Default            | Basic code and configuration skeleton                      | beginner |
| echo               | Echo               | Echo message                                               | beginner |
| hello_world        | Hello World        | Say Hello, World!                                          | beginner |
| sd2e_base          | sd2e_base          | Default reactor context for docker://sd2e/reactors:python3 | beginner |
| tacc_reactors_base | tacc_reactors_base | Default actor context for docker://sd2e/reactors:python3   | beginner |
+--------------------+--------------------+------------------------------------------------------------+----------+

Each template is a project directory for a different type of Actor. For this Actor, let’s use the default template:

$ tapis actors init --template default --actor-name upstream-messenger
+-------+---------------------------------------------------------------+
| stage | message                                                       |
+-------+---------------------------------------------------------------+
| setup | Project path: ./upstream_messenger                            |
| setup | CookieCutter variable name=upstream-messenger                 |
| setup | CookieCutter variable project_slug=upstream_messenger         |
| setup | CookieCutter variable docker_namespace=taccuser                   |
| setup | CookieCutter variable docker_registry=https://index.docker.io |
| clone | Project path: ./upstream_messenger                            |
+-------+---------------------------------------------------------------+
$ cd upstream_messenger
$ find -L .
.
./requirements.txt
./Dockerfile
./project.ini
./message.jsonschema
./default.py
./.gitignore
./secrets.jsonsample
./config.yml

We see that the tapis actors init command has initialized an Actor project directory for us, and it already contains many files that we could have written by hand such as the Dockerfile or the Python source code in default.py.

The only file that was not provided by the template is secrets.json. Let’s make an empty one now:

echo '{}' > secrets.json

Edit Actor Source

The Actor we just created doesn’t do much; it just says “hello world,” like the hello-world-actor we deployed previously. Let’s change its behavior so it does something more interesting, like message another Actor. We will use the Python API command sendMessage to implement this. Using your favorite text editor, edit the default.py script so it looks like:

import os
from agavepy.actors import get_context, get_client


def main():
   """Main entrypoint"""
   context = get_context()
   m = context['raw_message']
   print("Actor received message: {}".format(m))

   # Get an active Tapis client
   client = get_client()

   # Pull in the downstream Actor ID from the environment
   downstream_actor_id = context['DOWNSTREAM_ACTOR_ID']
   # alternatively:
   # downstream_actor_id = os.environ['DOWNSTREAM_ACTOR_ID']

   # Using our Tapis client, send a message to the downstream Actor
   message = 'greetings, hello-world-actor!'
   print("Sending message '{}' to {}".format(message, downstream_actor_id))
   response = client.actors.sendMessage(actorId=downstream_actor_id, body={"message": message})
   print("Successfully triggered execution '{}' on actor '{}'".format(response['executionId'], downstream_actor_id))


if __name__ == '__main__':
   main()

All we’ve done is add a block of code that calls the Tapis/Agave API so that it sends a message to another Actor. Notice that we are mimicking the CLI workflow from before:

Action

CLI

Python API

Get an authenticated
Tapis client
tapis auth init

client = get_client()

Using the client,
send message to an
Actor
tapis actors submit


client.actors.sendMessage()


Using the client,
submit HPC job to
Tapis Application
tapis jobs submit


client.jobs.submit()


In fact, the CLI is making the same calls to the Python API under the hood!

Notice that we haven’t actually defined which Actor ID we want to send the message to. Per best practice, we’ve chosen not to “hard code” the Actor ID into default.py, but rather read it from the Actor environment, which we access via context['DOWNSTREAM_ACTOR_ID'] or alternatively os.environ['DOWNSTREAM_ACTOR_ID']. To set the DOWNSTREAM_ACTOR_ID, we need only define it in the Actor environment when we deploy in the next step. The downstream Actor is the hello-world-actor we deployed previously, and we can retrieve its ID using the CLI:

$ tapis actors list
+---------------+--------------------+-------+-------------------------------+--------------------------+--------+--------+
| id            | name               | owner | image                         | lastUpdateTime           | status | cronOn |
+---------------+--------------------+-------+-------------------------------+--------------------------+--------+--------+
| MqqbarbazBB8x | hello-world-actor  | eho   | tacc/hello-world:latest       | 2021-08-24T19:13:44.036Z | READY  | False  |
+---------------+--------------------+-------+-------------------------------+--------------------------+--------+--------+

We will need this Actor ID (MqqbarbazBB8x in my case, yours will be different) when we deploy in the next section.

Deploy Actor

Our new upstream-messenger Actor is now ready to deploy. Just like before, we want to:

  1. Build the Docker image

  2. Push the Docker image

  3. Register the Docker image as a new Actor

Remember to replace the DOWNSTREAM_ACTOR_ID with the appropriate Actor ID from above, and the placeholder taccuser with your DockerHub username.

$ docker build -t taccuser/upstream-messenger:0.0.1 .
$ docker push taccuser/upstream-messenger:0.0.1
$ tapis actors create --repo taccuser/upstream-messenger:0.0.1 \
                      -n upstream-messenger \
                      -d "Sends message to another actor" \
                      -e DOWNSTREAM_ACTOR_ID=MqqbarbazBB8x
+----------------+-----------------------------------+
| Field          | Value                             |
+----------------+-----------------------------------+
| id             | MDfoobar7AOwx                     |
| name           | upstream-messenger                |
| owner          | taccuser                          |
| image          | taccuser/upstream-messenger:0.0.1 |
| lastUpdateTime | 2021-08-26T20:33:20.320620        |
| status         | SUBMITTED                         |
| cronOn         | False                             |
+----------------+-----------------------------------+

If deployment was successful, we should now see our new Actor:

$ tapis actors list
+---------------+--------------------+-------+-----------------------------------+--------------------------+--------+--------+
| id            | name               | owner | image                             | lastUpdateTime           | status | cronOn |
+---------------+--------------------+-------+-----------------------------------+--------------------------+--------+--------+
| MqqbarbazBB8x | hello-world-actor  | eho   | tacc/hello-world:latest           | 2021-08-24T19:13:44.036Z | READY  | False  |
| MDfoobar7AOwx | upstream-messenger | eho   | taccuser/upstream-messenger:0.0.1 | 2021-08-24T20:23:07.619Z | READY  | False  |
+---------------+--------------------+-------+-----------------------------------+--------------------------+--------+--------+
$ tapis actors show -v MDfoobar7AOwx
{
   "id": "MDfoobar7AOwx",
   "name": "upstream-messenger",
   "description": "Sends message to another actor",
   "owner": "eho",
   "image": "enho/upstream-messenger:0.0.1",
   "createTime": "2021-09-21T20:35:33.39Z0",
   "lastUpdateTime": "2021-09-21T20:35:33.39Z0",
   "defaultEnvironment": {
      "DOWNSTREAM_ACTOR_ID": "MqqbarbazBB8x"
   },
   "gid": 859336,
   "hints": [],
   "link": "",
   "mounts": [],
   "privileged": false,
   "queue": "default",
   "stateless": true,
   "status": "READY",
   "statusMessage": " ",
   "token": true,
   "uid": 859336,
   "useContainerUid": false,
   "webhook": "",
   "cronOn": false,
   "cronSchedule": null,
   "cronNextEx": null,
   "_links": {
      "executions": "https://api.tacc.utexas.edu/actors/v2/MDfoobar7AOwx/executions",
      "owner": "https://api.tacc.utexas.edu/profiles/v2/eho",
      "self": "https://api.tacc.utexas.edu/actors/v2/MDfoobar7AOwx"
   }
}

Send Message to upstream-messenger Using CLI

Once the upsteam_messenger Actor is READY, we can trigger a new execution by sending it a message:

$ tapis actors submit -m 'hello, upstream-messenger!' MDfoobar7AOwx
+-------------+----------------------------+
| Field       | Value                      |
+-------------+----------------------------+
| executionId | MDanexec7AOwx              |
| msg         | hello, upstream-messenger! |
+-------------+----------------------------+

As usual, we check the status of the execution, and show the logs when it finishes:

$ tapis actors execs show MDfoobar7AOwx MDanexec7AOwx
+-----------+-----------------------------+
| Field     | Value                       |
+-----------+-----------------------------+
| actorId   | MDfoobar7AOwx               |
| apiServer | https://api.tacc.utexas.edu |
| id        | MDanexec7AOwx               |
| status    | COMPLETE                    |
| workerId  | wZvworker1KmQ               |
+-----------+-----------------------------+
$ tapis actors execs logs MDfoobar7AOwx MDanexec7AOwx
Actor received message: hello, upstream-messenger!
Sending message 'greetings, hello-world-actor!' to MqqbarbazBB8x
Successfully triggered execution '5P7foobarrrA6' on actor 'MqqbarbazBB8x'

Check Execution of Downstream hello-world-actor

The goal of this tutorial was to send a message to upstream-messenger and have it trigger an execution on hello-world-actor. Let’s check the status of the execution and inspect the logs:

$ tapis actors execs show MqqbarbazBB8x 5P7foobarrrA6
+-----------+-----------------------------+
| Field     | Value                       |
+-----------+-----------------------------+
| actorId   | MqqbarbazBB8x               |
| apiServer | https://api.tacc.utexas.edu |
| id        | 5P7foobarrrA6               |
| status    | COMPLETE                    |
| workerId  | DJPworkerzKlN               |
+-----------+-----------------------------+
$ tapis actors execs logs MqqbarbazBB8x 5P7foobarrrA6
Logs for execution 5P7foobarrrA6
 Actor received message: hello, hello-world-actor!

Conclusion

Congratulations! We have successfully deployed a workflow that sends a message between two Actors. Of course, real-world multi-Actor workflows will send much more useful information than “hello, world.” In practice, messages contain file paths, names of analyses to run, and other metadata. It is also possible for one Actor to send messages to multiple other Actors, allowing for a single action such as a file upload to trigger many downstream processes, such as file management, running analyses, logging, and more.