Exercise 2: A proper phonebook Mochi component

Important

This exercise (as well as Exercise 3) does not need the thallium-tutorial-exercises repository used in Exercise 1.

Important

If you come from Exercise 1, make sure to deactivate the Spack environment before starting this exercise (we will build a new one, which we will also use in Exercise 3). This can be done with spack env deactivate in any terminal in which you had activated the environment. You can check whether an environment is active using spack env status.

In this exercise, we will use the Thallium microservice template to develop a proper phonebook microservice.

Click on the green “Use this template” button and select “Create a new repository”. Give the repository a name (e.g. “phonebook”). Put the repository on “private” if you wish, then click on “Create repository from template”.

Click on Settings > Actions > General, and set the Workflow permissions to “Read and write permissions”, then click Save in the Workflow Permissions section (there are save buttons for each section that will save only the modifications made to that section).

Go back to the root of the code (in your browser), and edit initial-setup.json. Change “alpha” to a service name of your choosing (this name will also be used as namespace for your API, so choose something short, e.g. YP, for yellow pages. In the following, we will assume this is the name you used). Change “resource” to the name of the resource we are going to manage, here “phonebook”. Click on the green “Commit changes” button.

Wait a little and refresh the page. You might see a brown dot indicating a workflow action is in progress. Or you might get a 404, which means the workflow completed: as part of the GitHub workflow that sets up your code, it will delete initial-setup.json.

Note

Other github workflows will run to test your code and upload a coverage report to codecov.io whenever you push commits to GitHub. These workflows will not work properly if you have made the repository private, so you may receive emails from GitHub about some failed workflows. Simply ignore them.

Clone your code in the mochi-tutorial directory on your machine, then create a Spack environment and build the code like you did in Exercise 1, using the spack.yaml file at the root of your new project (spack env create ex2 spack.yaml, spack env activate ex2, spack install). Create a build directory, cd into it, and build the code (cmake .. -DENABLE_TESTS=ON then make). You may want to use the flag -DENABLE_TESTS=ON when calling cmake to make sure that the tests are also built. You can run the tests with make test.

Note

The cmake will download Catch2 as part of the build process, make will build it.

The template provides the implementation of two RPCs, “hello” and “sum”. These RPCs are defined in a way that is very different from Exercise 1. Instead of using lambdas, they are defined as member functions of the ProviderImpl class in src/ProviderImpl.hpp.

Note

The architecture of this project uses the PImpl technique (pointer to implementation), where user-facing classes (e.g. Client, PhonebookHandle, etc.) only have a pointer field along with public member functions, and the actual data associated with an instance is in a private class (e.g. ClientImpl, PhonebookHandleImpl). While this technique adds a small overhead in function calls, it also better decouples the API of your service from its actual implementation.

include/YP/Client.hpp contains the Client class, which will be used to register the RPCs and contact servers. There is nothing to modify in this file.

include/YP/PhonebookHandle.hpp declares a PhonebookHandle class, which represents a phonebook managed by a remote server. This file is also where the relevant client interface will be defined. Go ahead and add declarations for an insert and a lookup member functions.

src/ClientImpl.hpp contains the definition of the ClientImpl structure, which hides the actual data associated with a Client instance. Go ahead and add two tl::remote_procedure fields to represent the insert and lookup RPCs, and initialize them in the constructor.

src/PhonebookHandle.cpp contains the implementation of the ResourceHandle’s member functions. Go ahead and implement the insert and lookup functions. You can copy the computeSum function and adapt it accordingly.

Note

The template proposes that all the RPCs wrap their result in a RequestResult<T> instance. You can view this class like a variant that will either store a T instance or an error string if the operation failed. If you implement your own service, feel free to handle errors differently.

Note

The template handles non-blocking RPCs by adding an optional AsyncRequest pointer to each member function. Feel free to remove it (along with the code path that deals with it) from your insert and lookup functions. You can circle back to them later if you want, as an exercise.

At this point, feel free to compile your code to make sure it builds fine. You won’t be able to test it yet since there is no server-side implementation of our RPCs, so let’s focus on the server library next.

include/YP/Backend.hpp contains the definition of a backend, i.e. the abstract class that a phonebook implementation must inherit from. Add the proper insert and lookup pure virtual methods to this structure, following the model of the computeSum function.

src/dummy/DummyBackend.hpp and src/dummy/DummyBackend.cpp contain a “dummy” implementation of such a backend in the form of a DummyPhonebook class. Add an std::unordered_map<std::string,uint64_t> field to this class, as well as the insert and lookup functions and their implementation.

src/ProviderImpl.hpp contains the implementation of our provider. While it still follows the Pimpl idiom, with the Provider class containing a pointer to a ProviderImpl instance, you will notice that RPC functions are actually defined as member functions of the ProviderImpl class. This is because in Thallium, providers can expose their own member functions as RPC. Go ahead and add the two tl::remote_procedure fields for your insert and lookup RPCs. Don’t forget to add their initialization in the constructor and to deregister them in the destructor!

Still in src/ProviderImpl.hpp, implement the two insertRPC and lookupRPC member functions by taking example on the computeSumRPC member function.

At this point, you can make sure your code builds fine. Your microservice is ready! If you have time, feel free to look into the tests folder, in particular the PhonebookTest.cpp file, and edit it (replacing calls to the computeSum RPC) to try out your new functionalities and test them (make test).

In practice, the next steps at this point would be to (1) add more tests, (2) remove everything related to the “hello” and “sum” RPCs (because obviously a phonebook is not a calculator), and (3) implement more complex backends by copying the code of the “dummy” backend and changing it to use external libraries or more complicated implementations.