Converse Client-Server (CCS) - Stearing the 2D Jacobi Program

CCS

CCS is a protocol that enables any Charm++ program to act as a server, and respond to requests sent by a client. The server will export the actions available, and a client (written in C/C++ or Java) can request the server to perform any action available.

Using CCS

In using CCS there are two steps:
  1. Modify a Charm++ program to add the methods that will perform the required operations, and register them to the system;
  2. Create a client program with the desired user interface to use the features exported by the server.
The server, when launched, will need to be instructed to listen for incoming requests. This is performed with the option ++server. The option ++server-port=<number> can be used to force the server to use a specific port. The server, while starting, will print the information necessary for the client to connect: IP and port number. Following is an example:
ccs: Server IP = 192.168.3.23, Server port = 15763

Modifying the 2D Jacobi Program

The 2D Jacobi program was presented here. We will start with this program as a basis, and extend it to receive request to modify values in the grid. When a request is received, the program will modify the value and proceed to evolve the system until a new equilibrium is reached. In Charm++ CCS requests can be mapped to callbacks (see here for more info on callbacks). The main function to register callbacks to CCS is:
void CcsRegisterHandler(const char *handlerName, const CkCallback &cb);
where handlerName is the string identifier of the CCS request which will be sent by the client, and cb is the associated callback that will be invoked upon receipt of the request. This registration needs to be performed only once in the entire system, therefore an intuitive location do do it is inside the mainchare "main" method (the one receiving CkArgMsg as parameter). The maximum length of the handler name is 32 characters.

We can see in the example code, in main.C line 85 and 86, how this is performed by registering a callback to the entire jacobi array when there is an incoming request named "changeValue", and a callback to the mainchare when there is an incoming request named "exit".
// Register the callbacks for CCS
CcsRegisterHandler("changeValue", CkCallback(CkIndex_Jacobi::ccsRequest(0), jacobiArray));
CcsRegisterHandler("exit", CkCallback(CkIndex_Main::terminate(0), mainProxy));
struct CkCcsRequestMsg {
  CcsDelayedReply reply; //Object to send reply to.
  int length; //Number of bytes of request data.
  char *data; //Actual data sent along with request.
};

The method to respond to the callback is registered as a normal entry method for the corresponding object. This method receives as input a CkCcsRequestMsg which contains the information received from the client, as well as the information to reply to such client. The structure of CkCcsRequestMsg is shown on the right.

Being "changeValue" triggered on the entire chare array, every element will execute the code. Each element will therefore pull the information out of the request, and update the values that belong to it. The format of this information must be agreed between client as server. This agreement, in our example is in the data structures ChangeRequest and SingleValue inside the header file jacobi-CS.h (which is included by both the client and the server).

Once the modification has happened, the array contributes back to the mainchare in stepCheckin, and a new iteration will be triggered if the modification exceeds the value of the threshold. The computation will then continue until a new balance is reached. To notice that this example follows the same structure used by the plain Jacobi example, therefore the upper left corner of the matrix is maintained fixed at one, and the border of the entire matrix is maintained fixed at zero.

Another modification we made to Jacobi is eliminating the call to CkExit in Main::stepCheckin when the equilibrium is reached. This gives the possibility to a client to connect and send requests. In this case, to terminate the application, the client has to send a request to the CCS handler "exit", which will provide to call CkExit.

In the case of "exit" we have no desire to reply to the client, so we can simply ignore it. Nevertheless, in the case of "changeValue" we would like to let the client know if the modification has succeded. In order to do this, we use the same structure of the request to send back a list of value change request that could not be applied by the server (clearly an empty list means that everything went ok).

The only caviat to be careful when replying to the client, is that only one single reply is allowed per request. This implies that not every element of the chare array can respond to the client, but only one element can. In our example, the chare element with index (0, 0) will reply to the client.

Creating the client

Together with the modification of the parallel application to accept requests through CCS, we need to create a specific application to generate such requests. This application will run on the user's desktop. For this example we choose to build the client in C++. The source code and be found here together with the server code.

This client reads two parameters from the command line (the name of the host where the server is running and its port, as printed by the server at startup), connects to the server, and enters an infinite loop asking the user for input. This input, which specifies a position in the matrix to modify and its new value, is then converted to a request in the agreed format and sent to the server. It then waits for the reply from the server before asking the user for the next input. An empty input from the user will trigger the other request to the server, the one to terminate.

To notice that the server does not implement security measures to prevent requests coming from a client to corrupt the data. This can happen if a request arrives to the server while it is still updating the matrix, before it has reached the equilibrium. Here we will consider only the case that the user sends requests at the right moment, namely when the server is idle. It is left as an exercise to the user to implement a synchronization mechanism to allow the server to accept requests at any time, even if the matrix has not yet reached an equilibrium.

Other than the usual system libraries, the client requires to header files. One is jacobi-CS.h which specifies the format of request and replies; the other is ccs-client.h which contains the classes and functions necessary to use CCS. When compiling, we will still need to use the Charm++ compiler charmc with a target language of C++ and the extra flag -seq to specify that the code is sequential and not parallel. Moreover, the dynamic library called ccs-client must be linked in. The command line used to compile the client (present in the makefile) is shown here below.

charmc -o client.o client.C
charmc -language c++ -seq -o client client.o -lreadline -lccs-client