Ping!

Source Code on GitHub #

In this section, we use a simple ping example to demonstrate how to realize server-side computation via message passing. The Ping program can be considered as the simplified version of many much more complicated distributed programs. Without considering the message handling logic, most full-fledged distributed program share the same communication patterns with the simple ping program.

Ping is probably the most well-known network utility, which is usually used to test the network connection. To test whether machine A can reach machine B, we send machine B a short message from machine A. For the purpose of demonstration, we are going to use 3 different types of messages to make the ping test.

Let us start with the simplest one: unidirectional ping. It looks like the ordinary ping, but is simplified. When machine B received the ping message from A, it only sends back a network acknowledgement to A without any meaningful response. After completing the ping test, machine A knows the network connection to B is OK, but it does not know whether machine B functions well.

Let us see how to implement this simple ping program. We need to specify three things:

  • What message is going to be sent?

  • What is the protocol via which the message is sent?

  • Which component of the system is responsible for handling the message?

These three things can be specified using the following Trinity specification script:

struct MyMessage
{
    int sn;
}

protocol SynPing
{
    Type: Syn;
    Request: MyMessage;
    Response: void;
}

server MyServer
{
    protocol SynPing;
}

The struct definition MyMessage specifies the struct of our message, which consists of a 32-bit sequential number sn.

Next, we specify a protocol named SynPing. The protocol definition consists of three parts:

  • Type of the protocol, which can be either Syn, Asyn, or HTTP.

  • Schema/Structure of the request message.

  • Schema/Structure of the response message.

In this example, we create SynPing as a synchronous protocol. The schema/structure of the message it uses is the previously defined MyMessage and it requires no response.

The last thing we need to do is to register the SynPing protocol to a GE system component. As we have elaborated earlier, three system components play different roles in a distributed Trinity system: server, proxy, and client. Among them, both server and proxy process messages, therefore we can register message passing protocols on them. In this example, we register SynPing to a server named MyServer.

We then create a Graph Engine Project Ping in visual studio. We cut and paste the above script to the generated MyCell.tsl file. Now we can create a Graph Engine Application Project called PingTest in visual studio.

Open the generated Program.cs, and put the following content in:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Trinity.Data;
using Trinity.Storage;
using Trinity;
using System.Threading;

namespace TestTrinity
{
    class MyServer : MyServerBase
    {
        public override void SynPingHandler(MyMessageReader request)
        {
            Console.WriteLine("Received SynPing, sn={0}", request.sn);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var my_server = new MyServer();
            my_server.Start(false);
            var synReq = new MyMessageWriter(1);
            Global.CloudStorage.SynPingToMyServer(0, synReq);
            Console.WriteLine();
            Console.ReadKey();
        }
    }
}

After compiling the Project Ping, an abstract class called MyServerBase is generated. Now we can implement our GE server very easily. All we need to do is to implement the abstract handlers provided by MyServerBase.

Here, we only have one synchronous handler SynPingHandler needs to be implemented. The name of the handler is generated based on the protocol name specified in the TSL script. For demonstration purpose, here we just output the received message to the console. When implementing SynPingHandler, the received message is wrapped in a Message Reader object called MyMessageReader. The previously specified MyMessage contains a sequential number sn. We can access this sn via the interfaces provided by MyMessageReader in the SynPingHandler.

Now the implementation of MyServer is done. Let us instantiate an instance of MyServer in the program's Main entry. After instantiating a MyServer instance, we can start the server by calling its Start() method.

To send a ping message, we first construct a message using MyMessageWriter(). Sending a message can be done by calling the SynPingToMyServer method of Global.CloudStorage. The method has two parameters. The first one is the server id to which the message is sent. The second parameter is the message writer object.

After compilation, we can test the ping application by starting the generated PingTest.exe. If everything goes well, you can see the output message "Received SynPing, sn=1" on the output console of PingTest.exe.

SynPing is a synchronous request without response. It is called synchronous because SynPing will be returned only if SynPingHandler is complete. As you can imagine, there should be an asynchronous counterpart. We can specify asynchronous requests very easily using TSL. Continuing with the ping example, we can define an asynchronous message passing protocol using the following lines in TSL:

protocol AsynPing
{
    Type: Asyn;
    Request: MyMessage;
    Response: void;
}

The only difference from protocol SynPing is their Type definition, one is Syn and the other is Asyn.

Correspondingly, GE will generate an AsynPingHandler abstract method in MyServerBase as well as an extension method called AsynPingToMyServer to CloudStorage. We add the following lines to MyServer class to implement the AsynPingHandler.

public override void AsynPingHandler(MyMessageReader request 
{
    Console.WriteLine("Received AsynPing, sn={0}", request.sn); 
}

After that, we can send an asynchronous message using the following two lines:

var asynReq = new MyMessageWriter(2);
Global.CloudStorage.AsynPingToMyServer(0, asynReq);

We call this message passing protocol asynchronous because AsynPing will return immediately after asynReq message is received by the other servers. That means when AsynPingToMyServer call returns, the corresponding AsynPingHandler may haven't been invoked.

Both SynPing and AsynPing are requests without response because no response message is specified. For synchronized protocols, the response could be set to a user-defined struct. Here is an example of synchronized protocol with response:

protocol SynEchoPing
{
    Type: Syn;
    Request: MyMessage;
    Response: MyMessage;
}

As specified by the definition, SynEchoPing is a synchronous message passing protocol. It sends request MyMessage out and will be responded with another MyMessage struct .

A complete specification script of the ping example is listed below:

struct MyMessage
{
    int sn;
}

protocol SynPing
{
    Type: Syn;
    Request: MyMessage;
    Response: void;
}

protocol SynEchoPing
{
    Type: Syn;
    Request: MyMessage;
    Response: MyMessage;
}

protocol AsynPing
{
    Type: Asyn;
    Request: MyMessage;
    Response: void;
}

server MyServer
{
    protocol SynPing;
    protocol SynEchoPing;
    protocol AsynPing;
}

The corresponding code of the server implementation and message sending is listed below.

using System;
using System.Collections.Generic;
using System.Text;
using Trinity.Data;
using Trinity.Storage;
using Trinity;
using System.Threading;

namespace PingTest
{
    class MyServer : MyServerBase
    {
        public override void SynPingHandler(MyMessageReader request)
        {
            Console.WriteLine("Received SynPing, sn={0}", request.sn);
        }

        public override void AsynPingHandler(MyMessageReader request)
        {
            Console.WriteLine("Received AsynPing, sn={0}", request.sn);
        }

        public override void SynEchoPingHandler(MyMessageReader request,
        MyMessageWriter response)
        {
            Console.WriteLine("Received SynEchoPing, sn={0}", request.sn);
            response.sn = request.sn;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var server = new MyServer();
            server.Start(false);

            var synReq = new MyMessageWriter(1);

            Global.CloudStorage.SynPingToMyServer(0, synReq);

            var asynReq = new MyMessageWriter(2);
            Global.CloudStorage.AsynPingToMyServer(0, asynReq);

            var synReqRsp = new MyMessageWriter(3);
            Console.WriteLine("response: {0}", 
                Global.CloudStorage.SynEchoPingToMyServer(0, synReqRsp).sn);

            while (true)
            {
                Thread.Sleep(3000);
            }

        }
    }
}

Running this example, you will see the following messages on the server console:

Received SynPing, sn=1
Received AsynPing, sn=2
Received SynEchoPing, sn=3
response: 3