RPCs and responses

In this tutorial, we will see how to initialize PyMargo, register an RPC function, and call it from a client.

PyMargo server

The following code initializes a PyMargo Engine for use as a server, then prints the address at which the server can be contacted. It exposes a “hello” RPC that takes a firstname and a lastname, prints them, and returns a string.

from pymargo.core import Engine, Handle

def hello(handle: Handle, firstname: str, lastname: str):
    print(f"Hello {firstname} {lastname}!")
    handle.respond(f"Good bye {firstname}")

if __name__ == "__main__":
    with Engine("tcp") as engine:
        engine.register("hello", hello)
        print(f"Service running at {engine.address}")
        engine.enable_remote_shutdown()
        engine.wait_for_finalize()

The Engine’s constructor takes the protocol as the first argument. Additional arguments may be provided:

  • mode: pymargo.client or pymargo.server, to indicate whether the engine will be used as a client or a server (defaults to server);

  • use_progress_thread: a boolean indicating whether to use a progress thread or not;

  • num_rpc_threads: the number of Argobots ES for servicing RPCs. Note that because of the GIL, using a value other than 0 will not necessarily result in better concurrency;

  • config: a dictionary or a JSON-formatted string representing a Margo configuration.

We then call engine.register to associate an RPC name with a function. Since in Python everything is an object, you can pass any callable to the engine.register function, including methods bound to instances, or instances of objects with a __call__ method.

The register function also accepts the following optional arguments:

  • provider_id: an integer equal or greater than 0 and less than 65535. This argument is used when working with providers.

  • disable_response: a boolean indicating that the RPC will not send a response back.

The engine.address call inside our print statement returns an Address object corresponding to the address of our server. This address is here converted into a string when printed.

The call to enable_remote_shutdown on the engine will allow a remote process to ask our server to shutdown.

Looking at the hello function, the first argument must be a Handle object. The rest of the arguments and their numbers can be anything, as long as their types are pickleable. The Handle object is used to call respond, among other things. handle.address can be used to get the address of the sender. Any object can be passed to respond, again as long as its type is pickleable. Here, our function takes two argument (firstname and lastname) of expected type str, and will respond with a string.

Note that the Engine is used as a context manager (with statement), this context will automatically call finalize on the engine when exiting, hence we have to explicitely block on a wait_for_finalize call to avoid exiting the context and start servicing RPCs.

PyMargo client

The following code is the corresponding client.

import sys
from pymargo.core import Engine
import pymargo

if __name__ == "__main__":
    with Engine("tcp", mode=pymargo.client) as engine:
        hello = engine.register("hello")
        address = engine.lookup(sys.argv[1])
        response = hello.on(address)("Matthieu", lastname="Dorier")
        assert response == "Good bye Matthieu"
        address.shutdown()

In this code, we initialize the Engine with mode=pymargo.client. We then use the engine’s lookup function to convert a string address (taken from sys.argv[1]) into an Address object.

We call register with only the name of the RPC. This function returns a RemoteFunction object. By calling on(Address) on this object, we get a CallableRemoteFunction with a __call__ operator that we can call. Said address is obtained by calling engine.lookup on a string address (here taken from sys.argv[1]). The code shows how arguments (positional and keywords) will be sent to the server when passed to the __call__ operator of the CallableRemoteFunction. Here firstname is provided as positional, while lastname is provided as a keyword argument. The returned value is the object passed to handle.respond in the server.

Finally, address.shutdown() sends a specific message to our server asking it to shutdown.

Important

Do not use the argument names blocking and timeout when defining RPCs (or if you do, make sure you use them as positional arguments when invoking the RPC). These argument keys are used by PyMargo to control the behavior of the call (more on that later).

Note

Python installs a signal handler to catch keyboard interrupts, but this signal handler cannot run when the server is blocked on wait_for_finalize or on C/C++ code more generally. Hence Ctrl-C won’t kill your server. Due to the interactions between the GIL and Argobots, it is also possible that a program does not terminate after an exception is raised. To kill such a program, you will have to resort to kill -9.