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.