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.