Pluggable RPC Architecture

This article will introduce the pluggable remote service invocation architecture. Large applications today are usually distributed across different servers, which are either invoking the services via a network, exposing such a network service themselves or both. It is important to build such server in a manner, which will introduce maximum flexibility to avoid vendor lock in and provide easily testable framework.

The layers structure of suggested architecture can be seen in the following figure:-

Layer 1 – Abstract Remote Services Layer – ARS

This layer implements a most convenient way of accessing the remote services across the application. Abstract remote services, is fully adopted to service invocation over network. The adaptation should be made to any local API because of the following reasons:-

  1. Invoker of the ARS API must be prepared for the long delay, even for methods that contain seemingly trivial application logic. The delay might occur due to high network latency or server side load. This means that some way should be provided to limit the function invocation in time, after which the response is no longer relevant, and after which all resources that were associated with the call should be released. This also implies that network API should preferably be asynchronous on both sides of the network service to simplify high performance application which usually does not tolerate long blocking calls. 
  2. Parameters, method names, and return values must be serializable. We can go one step further and simplify the logic by demanding that any parameter can be represented with plain binary structure without references or pointers (local or remote references) between the parameter. Some frameworks provide the feature of remote references, which seem to introduce complicated API without much added value.
  3. Invoker of the ARS API should be prepared for immediate call failure due to network outage. As in case of timeout, some method should be provided to inform the invoker of failure.

    Pay attention that ARS API should not reveal any vendor or implementation specific data structure to the caller, to simplify the implementation replacement and avoid vendor lock in. However, it may expose a rich set of generic error codes which will help the API user to cope correctly with the error conditions. :-

    Take, for example, a local interface of Calculator object that adds two numbers and returns the result

    class ICalculator
    {
    public:
      int Add (int a, int b) = 0;
    }

    This interface clearly not adopted for over-the-network usage. The example of network adopted interface would look something like that:-

    enum Result
    {
      Success,
      Timeout,
      GenericFailure
    };
    
    calss IRemoteCalculator {
    public:
      Result Add(int a, int b , int &sum) = 0;
    }

    or in asynchronous variant:-

    enum Result
    {
      Success,
      Timeout,
      GenericFailure
    };
    
    class IResultHandler // result callback interface
    {
    public:
       ResultCb(int sum, Result res) = 0;
    };
    
    class RemoteCalculator {
    public:
      Result StartAdd(int a, int b , ResultHandler);  // results are handled in callback 
    };

    This layer consists of two parts, the interface itself and its implementation. The implementation of the layer should be fully replaceable in a transparent manner during either link time, or runtime.

    Making application interface only with abstract service interface will be a step forward to build an application with flexible service provider implementation. The implementation may call directly the generated API provided by different vendors CORBA, DCOM, GRPC etc. However, it will create the dependency on networking layer and usually the application data serialization mechanism. In other words, we would like sometimes serialize data in binary format and sometimes as XML or JSON, and also we would like to have different options as networking transport solutions. For example, depending on configuration, we would like to use either HTTP or ZeroMQ as the layer that provides generic request response service. That’s why we would like to introduce an additional level of flexibility into the system. Specifically, we would like to abstract away application data serialization mechanism and network service provider.

    Layer 2 – Canonical Services Provider Layer – CSP

    Three classes of generic networking services can be identified. Each pattern has two roles depending on which side of the network communication channel it resides. The actual number of network components that implement the pattern may be bigger than two. For example, any communication pattern, can be implemented with broker or in brokerless manner. These implementation details are not relevant for conceptual communication pattern classification. 

    Networking classes are summarized in the following table. The number in braces reflects the ratio between number or application request issued and responses received for each side of certain network service class:-

    ClassProducer Consumer
    P2PSender (N:0)Receiver (0:N)
    Request/ResponseClient (1:1)Server (1:1)
    Publish/SubscriberPublisher (1:0)Subscriber (1:N)

    These classes correspond to the low level of network services API, which we call canonical services API. This API reflects the communication patterns above and deal mostly with binary blobs that must be delivered over the network. It does not know anything about types of arguments methods or return types. An example of such pluggable API can be found in project bricks on GitHub. Having the canonical services abstracted away makes it possible to replace the actual implementation behind the network transport service. For example, replace Kafka broker with RabbitMQ.

    The interfaces for all canonical patterns should be provided. And, as in all other cases, the implementation should be easily replaceable. This is the example of sync interface for request response pattern on client side. Pay attention in bricks to make the interface more flexible, some methods are added for initialization of the object and another option object is always passed along the basic method signature.

    class IReqRep
    {
    public:
         Result IssueRequest(char *req_buf, int req_len, char **rsp_buf, int &rsp_len) = 0;
    };
    
    

    Generic Serialization Considerations

    To convert arguments and method passed in ARS into the blob that can be used as a parameter to canonical services layer, we suggest creating an abstract interface which reflects the methods that correspond to the abstract network interface. For each ARS method, two functions should be provided, one for serialization of the arguments and one for deserealization of the blob into a result object. This is an alternative of providing the hierarchy of self serializable parameters. Making serialization interface accept regular application objects without imposing them to be derivative of ISerialiazable class seams less difficult to implement and provides more flexibility to serialization layer. For example, for the calculator interface above :-

    class ICalculatorSerilalzier
    {
    public:
       Result Serialize_Add(int a, int b, char **buf, int &size) = 0; 
       Result DeSerialize_Add(char *buf, int size, int &sum) = 0; 
    }

    Client side implementation of remote server invocation would then look like

    class RemoteCalculatorImpl : public IRemoteCalculator 
    {
    public:
      RemoteCalculatorImpl (ICalculatorSerilalzier *s, IReqRep r) : _s(s),_r(r){}
    
      Result Add(int a, int b , int &sum)
      {
    
           char *req;
           int req_len;
           auto err = _s->Serialize_Add(a, b, &req, req_len);
           if (err != Success) return err;
    
           char rsp[MAX_RSP_LENGTH];
            _r->IssueRequest(req,req_len,rsp, MAX_RSP_LENGTH)
             if (err != Success) return err;
    
            _s->Deserialize_Add(rsp,MAX_RSP_LENGTH,sum);
            return err;
      }
    }

    Conclusion

    In order to provide pluggable stack for remote service invocation framework. The architecture should provide three abstract, independent layers:

    • Abstract layer with interfaces representing actual service logic, adopted for network invocation.
    • Serializing layer with function signatures corresponding to abstract layer functions
    • Canonical network services layer interfaces.

    Leave a Comment