Modern C++ micro-serivce + REST API, Part II

This is the second part of my previous story: Modern C++ micro-service implementation + REST API where I basically discuss the simplicity you can achive when implementing a micro-service using Modern C++, simplicity that challenges node.js’s with the extra benefit of great performance at no expense in productivity, so if you haven’t read it and you are interested on C++ micro-serivces on your next backend implementation you might want to read that story first.

In this occasion we’ll talk about asynchronous request processing and how to handle a request using pplx::task. There are a lot already covered about pplx::task, wrote by very competent folks so the idea here is to show you how to use it in the context of our C++ micro service, to implement REST APIs and a bit of more detail about how asynchrony can help to implement a lock-free micro service that achieves great performance and again simplicity in code, thanks to the elegant pplx library.

Tasks

The pplx::task class follows the approach of chaining tasks together, each task holds its own state and they progress to the next task via continuations. This chaining concept helps mantain in mind that a task maps into a single transaction allowing a cleaner implementation and due to the tasks hold its own state avoids excessive locking. Tasks implement the promise pattern which helps to avoid too much levels of chaining that can lead to poor code readability and reduces latency. Another option expecially for more complex cases could be the use of Fibers or Coroutines, but that is out of the scope of this article.

Modifying Modern C++ micro service

First of all we need to create some infrastructure so let’s say we want to create a user sign-up/sign-on service, so before we continue please git clonethe sample code found here and build it following the instructions in the README.mdfile.

Then please come back here and cd micro-service/source and add the files: user_manager.hppand user_manager.cpp using your favorite editor.

I am using Visual Studio Code (VSC for short) and you can follow here some instructions to use it for this article too, but you can use vim, emacs, Xcode, Visual Studio, etc.

please edit the file micro-service/CMakeLists.txt adding the user_manager.cpp to the list of sources as follows (add the line in bold):

add_executable(${PROJECT_NAME} ./source/main.cpp
./source/microsvc_controller.cpp
./source/user_manager.cpp
./source/foundation/network_utils.cpp
./source/foundation/basic_controller.cpp)

Now we are not going to use any full fledge database, but an in memory database so let’s go through the following code, feel free to copy and paste it into the file user_manager.hpp you just created:

#pragma once
#include <map>
#include <std_micro_service.hpp>
typedef struct {
std::string email;
std::string password;
std::string name;
std::string lastName;
} UserInformation;
class UserManagerException : public std::exception {
std::string _message;
public:
UserManagerException(const std::string & message) :
_message(message) { }
const char * what() const throw() {
return _message.c_str();
}
};
// alias declaration of our In Memory database...
using UserDatabase = std::map<std::string, UserInformation>;
class UserManager {
public:
   void signUp(const UserInformation & userInfo)  
throw(UserManagerException);
   bool signOn(const std::string email, 
const std::string password,
UserInformation & userInfo);
};

Our user manager micro service will manage the users registration and authentication, so as shown above we are declaring our UserManager class and at the top we declare the type UserInformation, that represents one record in our In Memory database, then we declare UserManagerException derived from std::exception that we’ll use to inform the controller (find declaration in source/microsvc_controller.hpp file) about any problem occurred on aUserManger object. In the middle we declare an alias of std::map<std::string, UserInformation> using the name UserDatabase that will represent the In Memory database, meaning it’s never persisted to a file, it goes away when the micro service shuts down, but it serves to our sample purposes, I leave it to you the code to incorporate MongoDB, Postgres or whatever is your favorite database.

Finally is the declaration of UserManager class, which basically has two methods:

  • signUp, to register users
  • signOn, to authenticate users

Now lets see the implementation which you can as well copy/paste to the file user_manager.cpp you have already created:

#include <mutex>
#include "user_manager.hpp"
UserDatabase usersDB;
std::mutex usersDBMutex;
void UserManager::signUp(const UserInformation & userInfo) throw(UserManagerException) {

std::unique_lock<std::mutex> lock { usersDBMutex };
   if (usersDB.find(userInfo.email) != usersDB.end()) {
throw UserManagerException("user already exists!");
}
   usersDB.insert(
std::pair<std::string, UserInformation>(userInfo.email,
userInfo));
}
bool UserManager::signOn(const std::string email, 
const std::string password,
UserInformation & userInfo) {
if (usersDB.find(email) != usersDB.end()) {
auto ui = usersDB[email];
if (ui.password == password) {
userInfo = ui;
return true;
}
}
return false;
}

First at the top you can see the declaration of a global variable of type UserDatabase named usersDB and a global variable of typestd::mutex named usersDBMutex which acts as a lock mechanism to avoid that multiple threads falling into a race condition mess up our users database under heavy concurrency.

Our lock mechanism is a very simple implementation of what a comercial database provide out of the box to access the data under heavy concurrency, we use it only for sample purposes and to handle the concurrency on our In Memory database.

The signUp method declares a variable of type std::unique_lock named lock that aquires a lock on our global mutex usersDBMutex. (if possible, otherwise it blocks), this will prevent other threads to corrupt our database. Then under the lock blanket signUp validates the user’s email doesn’t exists otherwise throws an UserManagerException indicating the user already exists. If everything goes fine it inserts the user on the database and leaves but before executes the lock’s destructor which releases the mutex whether there was an exception or not.

The signOn method simply validates the email and password provided by the user and returns true if found otherwise false which means the authentication failed.

Implementing REST API using a sync tasks

Let’s say we are going to implement the following two REST APIs:

  • users/signup, registers a user with our UserManager micro service and it accepts a JSON body with the following format:
{ 
"email":"ivan@email.com",
"password":"allmighty",
"name":"ivan",
"lastName":"mejia"
}
  • users/signon, authenticates a user using our core service implementationUserManager which following the specification of HTTP Basic Authentication will require the credentials passed encoded in base64 format, so the user and password are not passed on the URL but on the Authorization HTTP header and not in this case but can be protected using SSL, which BTW is supported by C++ REST SDK as well and I’ll cover it in a future article.

Ok now that we have our core service implementation under UserManager, is time to process the requests using the asynchronous power of pplx::task, let’s implement out REST APIs

Implementing “users/singup” API

Let’s say UserManager::signUp is a long running operation where we need to deffer the execution to a separate thread while letting the calling thread go back to the pool to serve more requests. so please open the file microsvc_controller.cpp and let’s implement the handlePost method which currently looks like this:

void MicroserviceController::handlePost(http_request message) {
    message.reply(status_codes::NotImplemented,    
responseNotImpl(methods::POST));
}

remove all the content and copy/paste inside the following code so MicroserviceController::hadlePost looks like this:

#include <std_micro_service.hpp>
#include "microsvc_controller.hpp"
#include "user_manager.hpp"
...
void MicroserviceController::handlePost(http_request message) {
auto path = requestPath(message);
if (!path.empty() &&
path[0] == "users" &&
path[1] == "signup") {
message.
extract_json().
then([=](json::value request) {
try {
UserInformation userInfo {
request.at("email").as_string(),
request.at("password").as_string(),
request.at("name").as_string(),
request.at("lastName").as_string()
};
                UserManager users;
users.signUp(userInfo);
                json::value response;
response["message"] = json::value::string(
"succesful registration!");
message.reply(status_codes::OK, response);
}
catch(UserManagerException & e) {
message.reply(status_codes::BadRequest, e.what());
}
catch(json::json_exception & e) {
message.reply(status_codes::BadRequest);
}
});
}
}

The code above implements the handler for our users/signup API so let’s go through the code, first we parse the request’s URL, you can find the implementation of requestPath on the file sources/foundation/basic_controller.cpp which returns an std::vector<std::string> containing each part of the path.

Then we validate the path variable is not empty and contains the required sections in the path that matches with the API that we are implementing.

Finally we use the variable message of type http_request passed as the argument of handlePost, and use the method extract_json that extracts the JSON payload from the request. http_request has this and other handy methods like extract_string, extract_vector, etc. you can check on the reference documentation. The method extract_json() returns an pplx::task<json::value> that executes asynchronously on a thread other than the thread where the request is being served, releasing the serving thread and returning it to the main thread pool so it can serve more requests while the json extraction tasks deals with the JSON body sent in the request, letting our micro service to handle a good deal of concurrency.

At completion of extract_json is called what is know as a value based continuation, a continuation that receives a json::value value argument.

message.
extract_json(). // return pplx::task<json::value>
then([=](json::value request) { // value based continuation
...
}

A continuation as you see acts as a callback, executed after task completion, then inside a continuation is possible to perform another task and so on, here is where you most be careful not to impement too much nesting of tasks otherwise you could end with code difficult to read and maintain.

The code above can be modified into the following form known as a task based continuation:

message.
extract_json().
then([=](pplx::task<json::value> requestTask) {
auto request = requestTask.get();
...
});

where the continuation parameter in this case is of type pplx::task<json::value>, where we can call theget() method that returns the result produced by the task if it already finished otherwise will block until the task has finished. More on this ahead.

Another way to modify the code above is:

message.
extract_json().
then([=](pplx::task<json::value> requestTask) {
requestTask.then([=](json::value request) {
...
       });       
});

on this second option you add an extra level of nesting but a new thread executes and on completion calls the continuation, so you will have three threads instead of two as in the first case when we used the get() method.

You might wonder why you want to use these two options? well, let’s suppose you have a really big JSON request that might require some time to receive and then parse, then you’ll probably want as well read the JSON asynchronously keeping the micro service responsive.

Implementing “users/signon” API

This API deals with reading the credentials from the Authorization HTTP header decoding them from base64 to plain text string. So in the request the Authorization header could look like this:

Authorization Basic dXNlckBlbWFpbGRvbWFpbi5jb206c2VjcmV0d29yZAo=

where credentials are passed in plain text, formatted in the following way:

<user email>:<password>

you can use the following command on your terminal to generate the base64 sequence as follows:

$ echo user@emaildomain.com:secretword | base64
dXNlckBlbWFpbGRvbWFpbi5jb206c2VjcmV0d29yZAo=

now, please open the file source/microsvc_controller.cpp and on the MicroserviceController::handleGet method which looks like this:

void MicroserviceController::handleGet(http_request message) {
auto path = requestPath(message);
if (!path.empty()) {
if (path[0] == "service" && path[1] == "test") {
auto response = json::value::object();
response["version"] = json::value::string("0.1.1");
response["status"] = json::value::string("ready!");
message.reply(status_codes::OK, response);
}
}
else {
message.reply(status_codes::NotFound);
}
}

please add the following code (you can copy/paste the code in bold) so Microservice::handleGet look like this now:

#include <tuple>
...
void MicroserviceController::handleGet(http_request message) {
auto path = requestPath(message);
if (!path.empty()) {
if (path[0] == "service" && path[1] == "test") {
...
}
else if (path[0] == "users" && path[1] == "signon") {
pplx::create_task([=]() -> std::tuple<bool, UserInformation> {
            auto headers = message.headers();
if (message.headers().find("Authorization") == headers.end())
throw std::exception();
            auto authHeader = headers["Authorization"];
auto credsPos = authHeader.find("Basic");
if (credsPos == std::string::npos)
throw std::exception();

auto base64 = authHeader.substr(credsPos +
std::string("Basic").length() + 1);
if (base64.empty())
throw std::exception();
            auto bytes = utility::conversions::from_base64(base64);
std::string creds(bytes.begin(), bytes.end());
auto colonPos = creds.find(":");
if (colonPos == std::string::npos)
throw std::exception();
            auto useremail = creds.substr(0, colonPos);
auto password = creds.substr(colonPos + 1, creds.size() - colonPos - 1);

UserManager users;
UserInformation userInfo;
if (users.signOn(useremail, password, userInfo)) {
return std::make_tuple(true, userInfo);
}
else {
return std::make_tuple(false, UserInformation {});
}
        })
.then([=](pplx::task<std::tuple<bool, UserInformation>> resultTsk) {
try {
auto result = resultTsk.get();
                if (std::get<0>(result) == true) {
json::value response;
response["success"] = json::value::string("welcome " + std::get<1>(result).name + "!");
message.reply(status_codes::OK, response);
}
else {
message.reply(status_codes::Unauthorized);
}
}
catch(std::exception) {
message.reply(status_codes::Unauthorized);
}
});
}
else {
message.reply(status_codes::NotFound);
}
}

In the new code you’ll notice right in next line to the path validation, a call to pplx::create_task this function creates a task which returns the type specified in the lambda; in this case it returns a tuple containing a bool and UserInformation. That’s why we declare at the top the #include <tuple> statement BTW.

We have created this task to perform the user’s authentication on a different thread than the one serving the request, so again releasing the serivng thread to the pool and serve more requests, while we perform our heavy operation on a separate thread.

The remaing code is very simple:

  1. Reads the HTTP headers provided by http_request on its headers() method, validates the Authorization header exists.
  2. Read the credentials base64 encoded into the string variable base64 and decode them into a bytes array using utility::conversions::from_base64(...) utility function.
  3. Extract the user and password and pass them to UserManager::signOn method to authenticate the user which on case of success returns a tuple with a boolean value to indicate authentication’s success or failure and the user information on the variable userInfo.

Finally in the continuation code we receive the response tuple where if the fist element is true indicates the authetication was successful and we use the UserInformation to send back the response:

{
"success": "welcome ivan!"
}

otherwise the user won’t be granted access and we’ll send back an Unauthorized error.

In the code above you will notice that in case of error an std::exception is thrown on task’s main lambda, but they are catched in the continuation. To be able to do that we need to implement a task based continuation and on the task passed resultTsk call the get() method in other to catch any exception.

Benchmark

In the our micro service repository you can find a Lua script that can be executed using the WRK2 benchmark tool that shows some pretty good results as shown on the README.md of the project. You can create the Lua scripts to test our REST APIs following the ones found under tests/wrk.

Conclusion

As you can see the asynchronous execution of each request on the server allows us to have a listener thread pool quite responsive due to the lengthily processing can be decoupled from the serving thread releasing it to serve more requests while the lengthily operation can inform of its results on the thread is running.

Additionally the simplicity of the pplx::task API allows us to create again code that challenges other productive tools with the benefit of astonishing performance, in next article I’ll talk about how to run this micro service inside a Docker container.