Remote Call Framework 3.4
Versioning

RCF provides robust versioning support, allowing you to upgrade distributed components without breaking runtime compatibility with previously deployed components.

If you write components which only communicate with peer components of the same version, you won't need to worry about versioning. For instance, if all your components are built from the same codebase and deployed together, versioning won't be an issue.

However, if for example you have clients which need to communicate with servers that were built from an older version of the same codebase, and have already been deployed, then you will need to be aware of how RCF deals with versioning.

Unlike other RPC systems you may have encountered, RCF is designed to be versioning-friendly. The data contract between a client and a server is essentially defined by the following:

  • The methods on the RCF interface.
  • The order and type of the parameters of those methods.
  • The serialization functions of the parameters.

RCF allows you considerable scope to evolve all these aspects of your data contract, without invalidating your existing data contract.

Interface Versioning

Adding or Removing Methods

Each method in an RCF interface has a method ID associated with it, which is used by clients to identify that particular method. The first method on an interface has a method ID of 0, the next one a method ID of 1, and so on.

Inserting a method at the beginning, or in the middle, of an RCF interface, changes the existing method ID's and hence breaks compatibility with existing clients and servers. To preserve compatibility, new methods need to be added at the end of the RCF interface:

// Version 1
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R2(double, add, double, double)
RCF_METHOD_R2(double, subtract, double, double)
RCF_END(I_Calculator)
// Version 2.
// * Clients compiled against this interface will be able to call add() and
// subtract() on servers compiled against the old interface.
//
// * Servers compiled against this interface will be able to take add() and
// subtract() calls from clients compiled against the old interface.
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R2(double, add, double, double)
RCF_METHOD_R2(double, subtract, double, double)
RCF_METHOD_R2(double, multiply, double, double)
RCF_END(I_Calculator)

Removing methods can be done as well, as long as a place holder is left in the interface, in order to preserve the method ID's of the remaining methods in the interface.

// Version 1
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R2(double, add, double, double)
RCF_METHOD_R2(double, subtract, double, double)
RCF_END(I_Calculator)
// Version 2.
// * Clients compiled against this interface will be able to call subtract()
// on servers compiled against the old interface.
//
// * Servers compiled against this interface will be able to take subtract()
// calls from clients compiled against the old interface (but not add() calls).
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_PLACEHOLDER()
RCF_METHOD_R2(double, subtract, double, double)
RCF_END(I_Calculator)

Adding or Removing Parameters

Parameters can be added to a method, or removed from a method, without breaking compatibility. RCF servers and clients ignore any extra (redundant) parameters that are passed in a remote call, and if an expected parameter is not supplied, it is default initialized.

// Version 1
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_Calculator)
// Version 2
// * Clients compiled against this interface will be able to call add()
// on servers compiled against the old interface (the server will ignore
// the third parameter).
//
// * Servers compiled against this interface will be able to take add()
// calls from clients compiled against the old interface (the third parameter
// will be default initialized to zero).
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R3(double, add, double, double, double)
RCF_END(I_Calculator)

Likewise, parameters can be removed:

// Version 1
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_Calculator)
// Version 2
// * Clients compiled against this interface will be able to call add()
// on servers compiled against the old interface (the server will assume the
// second parameter is zero).
//
// * Servers compiled against this interface will be able to take add()
// calls from clients compiled against the old interface (the second parameter
// from the client will be ignored).
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R1(double, add, double)
RCF_END(I_Calculator)

Note that RCF marshals in-parameters and out-parameters in the order that they appear in the RCF_METHOD_XX() declaration. Any added (or removed) parameters must be the last to be marshalled, otherwise compatibility will be broken.

Renaming Interfaces

RCF interfaces are identified by their runtime name, as specified in the second parameter of the RCF_BEGIN() macro. As long as this name is preserved, the compile time name of the interface can be changed, without breaking compatibility.

// Version 1
RCF_BEGIN(I_Calculator, "I_Calculator")
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_Calculator)
// Version 2
// * Clients compiled against this interface will be able to call add()
// on servers compiled against the old interface.
//
// * Servers compiled against this interface will be able to take add()
// calls from clients compiled against the old interface.
RCF_BEGIN(I_CalculatorService, "I_Calculator")
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_CalculatorService)

Archive Versioning

The application-specific data types passed in a remote call, are likely to change over time. As those data types change, their serialization functions will change as well. To assist applications in maintaining backwards compatibility for serialization code, RCF provides an archive version number concept.

The archive version number is passed from client to server on every remote call, and is zero by default. To implement versioning, your application is expected to manage the archive version, essentially updating it whenever a breaking change is made to serialization code.

To set the archive version number, call RCF::setArchiveVersion() before creating any servers or clients.

Alternatively, you can set the archive version number for individual clients and servers by calling RCF::ClientStub::setArchiveVersion() and RCF::RcfServer::setArchiveVersion().

When a client connects to a server, an automatic version negotiation step takes place, to ensure that the connection uses the greatest archive version number that both components support.

The archive version number is intended to be used by serialization code. From within a serialization function you can retrieve the archive version number in use, by calling SF::Archive::getArchiveVersion():

void serialize(SF::Archive & ar, MyClass & m)
{
std::uint32_t archiveVersion = ar.getArchiveVersion();
// ...
}

The serialization code can then use the value of the archive version number to determine which members to serialize.

For example, assume that the first version of your application contains this code:

class X
{
public:
int mA = 0;
void serialize(SF::Archive & ar)
{
ar & mA;
}
};
RCF_BEGIN(I_Echo, "I_Echo")
RCF_METHOD_R1(X, Echo, const X &)
RCF_END(I_Echo)
class EchoImpl
{
public:
X Echo(const X & x)
{
return x;
}
};
//--------------------------------------------------------------------------
// Accepting calls from other processes...
EchoImpl echo;
RCF::RcfServer server( RCF::TcpEndpoint(50001) );
server.bind<I_Echo>(echo);
server.start();
//--------------------------------------------------------------------------
// ... or making calls to other processes.
RcfClient<I_Echo> client( RCF::TcpEndpoint(50002) );
X x1;
X x2;
x2 = client.Echo(x1);

Once this version has been released, a new version is prepared, with a new member added to the X class:

class X
{
public:
int mA = 0;
int mB = 0;
void serialize(SF::Archive & ar)
{
// Retrieve archive version, to determine which members to serialize.
std::uint32_t version = ar.getArchiveVersion();
ar & mA;
if (version >= 1)
{
ar & mB;
}
}
};
class EchoImpl
{
public:
X Echo(const X & x) { return x; }
};

Notice that the serialization code of X now uses the archive version number to determine whether it should serialize the new mB member.

With these changes, new servers are able to process calls from both old and new clients, and new clients are able to call either old or new servers:

// The specified archive version should be the latest archive version this process supports.
//--------------------------------------------------------------------------
// Accepting calls from other processes...
// This server can take calls from either new or old clients. Archive version
// will be 0 when old clients call in, and 1 when new clients call in.
EchoImpl echo;
RCF::RcfServer server( RCF::TcpEndpoint(50001) );
server.bind<I_Echo>(echo);
server.start();
//--------------------------------------------------------------------------
// ... or making calls to other processes.
// This client can call either new or old servers.
RcfClient<I_Echo> client( RCF::TcpEndpoint(50002) );
X x1;
X x2;
// If the server on port 50002 is old, this call will have archive version set to 0.
// If the server on port 50002 is new, this call will have archive version set to 1.
x2 = client.Echo(x1);

Runtime Versioning

RCF maintains runtime compatibility with itself, for previous RCF releases dating back to and including RCF 2.0. Runtime compatibilty with RCF releases older than 2.0 is not guaranteed.

To implement runtime compatibility between RCF releases, RCF maintains a runtime version number, which is incremented for each RCF release. The runtime version number is passed in the request header for each remote call, and allows older and newer RCF releases to interoperate.

RCF's automatic client-server version negotiation handles runtime versioning, as well as archive versioning. In most circumstances, you won't need to know about runtime version numbers. You can mix and match RCF releases, and at runtime, an appropriate runtime version is negotiated for each client-server connection.

Custom Version Negotiation

In some situations, the client may already know the runtime version and archive version of the server it is about to call, in which case it can disable automatic versioning and set the version numbers explicitly:

RcfClient<I_Echo> client( RCF::TcpEndpoint(50001) );
// Turn off automatic version negotiation.
client.getClientStub().setAutoVersioning(false);
// Client setting version numbers explicitly to match RCF 2.0, with archive version 2.
client.getClientStub().setRuntimeVersion(10); // RCF 2.0
client.getClientStub().setArchiveVersion(2);
// If the server doesn't support the requested version numbers, an exception will be thrown.
X x1;
X x2 = client.Echo(x1);

Automatic version negotiation is not supported for one-way calls. In particular, the one-way calls made by a publisher to its subscribers, are not automatically versioned. If you have subscribers with varying runtime and archive version numbers, the publisher will need to explicitly set version numbers on the publishing RcfClient<> object, to match that of the oldest anticipated subscriber:

RCF::RcfServer server( RCF::TcpEndpoint(50001) );
server.start();
typedef RCF::Publisher<I_PrintService> PrintServicePublisher;
typedef std::shared_ptr< PrintServicePublisher > PrintServicePublisherPtr;
PrintServicePublisherPtr pubPtr = server.createPublisher<I_PrintService>();
// Client setting version numbers explicitly to support older subscribers.
pubPtr->publish().getClientStub().setRuntimeVersion(10); // RCF runtime version 10 (RCF 2.0).
pubPtr->publish().getClientStub().setArchiveVersion(5); // Application archive version 5.

For reference, here is a table of runtime version numbers for each RCF release.

RCF release Runtime version number
2.0 10
2.1 11
2.2 12
3.0 13

Protocol Buffers

For applications with backwards compatibility requirements and short or continuous release cycles, archive versioning can become difficult to manage. Each increment of the archive version number involves adding new execution paths to serialization functions, and may over time lead to complicated serialization code.

RCF also supports Protocol Buffers, which provides an alternative approach to versioning. Rather than manually writing serialization code for C++ objects, Protocol Buffers can be used to generate C++ classes with built-in serialization code, which deals automatically with versioning differences (see Protocol Buffers).