PrevUpHomeNext

Versioning

Interface versioning
Archive versioning
Runtime versioning
Custom version negotiation
Protocol Buffers

Versioning, in this context, is the subject of upgrading distributed components (clients or servers) without breaking runtime compatibility with previously deployed components (clients or servers).

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 you, for example, 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.

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

Inserting a method at the beginning, or in the middle, of an RCF interface, changes the existing dispatch ID's and hence breaks compatibility with existing clients and servers. To preserve compatibility, 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 dispatch 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)

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.

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)

The application-specific data types passed in a remote call, are likely to change over time. To assist applications in maintaining backwards compatibility, RCF provides an archive version number, which is automatically passed from the client to the server on each call. The archive version number is a 32 bit unsigned integer, and is set to zero by default. The archive version number can be retrieved from any serialization function, by calling SF::Archive::getArchiveVersion():

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

The archive version number in use on a client-server connection is determined by an automatic version negotiation step that takes place when the client connects. The version negotiation step ensures that the connection uses the greatest archive version number that both components support.

When breaking changes are made to serialization functions, the archive version number should be updated to reflect the change. Typically you would increment the archive version number once for each release of the application. The serialization functions 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:
    X() : mN(0)
    {}

    int mN;

    void serialize(SF::Archive & ar)
    {
        ar & mN;
    }
};

RCF_BEGIN(I_Echo, "I_Echo")
    RCF_METHOD_R1(X, echo, const X &)
RCF_END(I_Echo)

class Echo
{
public:
    X echo(const X & x) { return x; }
};

//--------------------------------------------------------------------------
// Accepting calls from other processes...
Echo 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:
    X() : mN(0)
    {}

    int mN;
    std::string mS;

    void serialize(SF::Archive & ar)
    {
        // Retrieve archive version, to determine which members to serialize.
        boost::uint32_t version = ar.getArchiveVersion();

        if (version == 0)
        {
            ar & mN;
        }
        else if (version == 1)
        {
            ar & mN;
            ar & mS;
        }
        else
        {
            // Unsupported version
            throw std::runtime_error("Unsupported version.");
        }
    }
};

class Echo
{
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 mS 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 default archive version should be the latest archive version this process supports.
RCF::setDefaultArchiveVersion(1);

//--------------------------------------------------------------------------
// 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.
Echo 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);

RCF maintains runtime compatibility with itself, for all releases dating back to and including RCF 0.9c, released in July 2007.

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 old and new 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.

RCF's automatic version negotiation does require an extra round trip between the client and the server, in the case of a version mismatch. 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);

// Assume that the server is running RCF 1.1, with archive version 2.
client.getClientStub().setRuntimeVersion(5); // RCF 1.1
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 oneway calls. In particular, the oneway 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 subscriber:

RCF::RcfServer server( RCF::TcpEndpoint(50001) );
server.start();

typedef RCF::Publisher<I_HelloWorld> HelloWorldPublisher;
typedef boost::shared_ptr< HelloWorldPublisher > HelloWorldPublisherPtr;
HelloWorldPublisherPtr pubPtr = server.createPublisher<I_HelloWorld>();

// Explicitly set version numbers to support older subscribers.
pubPtr->publish().getClientStub().setRuntimeVersion(3); // RCF runtime version 3 (RCF 0.9d).
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

0.9c

2

0.9d

3

1.0

4

1.1

5

1.2

6

1.3

8

1.3.1

9

2.0

10

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 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).


PrevUpHomeNext