The first several sections will be filled later.
We have four sets of RPC interfaces, JSON-RPC, websocket RPC, rippled command line RPC, and gRPC. They have similarities as well as differences, such as the wire format (JSON vs protobuf) and the streaming capacity (no-stream, client-stream, server-stream, and bi-directional). They may also face different client-side developers with different conventions. So we will try to have the same set of requirements and the same design, and acknowledge the differences when needed.
The API version number is a single 32-bit unsigned integer and starts from 1. We increase the version number if and only if we introduce breaking changes to the API.
We support a range of consecutive API versions in rippled. The lower and higher bounds of the range will be hard-coded in rippled code. Different rippled software versions may support different API version ranges. When a rippled server receives a client request targeting an API version that is out of the supported version range, it rejects the request. For JSON-RPC and websocket RPC, the returned error message will include the string “Unsupported API version”, the version number targeted by the request, and the lower and higher bounds of the supported range.
The table below lists the types of changes and their backwards-compatibility. The table is a work in progress.
Changes | JSON-RPC & websocket | gRPC |
Deleting, renaming, changing the type of, or changing the meaning of a field of a request or a response message. | breaking | breaking |
Adding a new field to the request or the response message. (Note the exceptions with position based parameters below.) | non-breaking | non-breaking |
JSON based: Change the order of position based parameters. Or insert a new field in front of existing parameters.
gRPC: Changing a proto field number. |
breaking | breaking |
Deleting or renaming an API function. | breaking | breaking |
Changing the behavior of an API function visible to existing clients | breaking | breaking |
Adding a new API function. | non-breaking | non-breaking |
The rows below are gRPC only breaking changes | ||
Deleting or renaming an enum, or an enum value. | breaking | |
Move fields into or out of a oneof, split or merge oneof | breaking | |
Changing the label of a message field, i.e. optional, repeated, required. | breaking | |
Changing the stream value of a method request or response. | breaking | |
Deleting or renaming a package or service | breaking |
Add an "api_version" field in the request to indicate which API version the request is requesting.
{
"method": "...",
"params": [
{
"api_version": 1,
...
}
]
}
This way of specifying the version number in the request message is self-contained and in line with the way JSON-RPC specifying its own version. Since putting it at the top level is not a good choice, it is placed in the one-item array of “params” as required.
The sequence diagram below shows a sunny day case of processing a JSON-RPC request. On the execution path, two classes are modified, ServerHandlerImp and HandlerTable. The modifications are in the yellow boxes, (1) the requests will have the “api_version” field, and (2) the request handlers is currently stored in a map (in the format of map<name, Handler>), they will be stored in an array of maps (in the format of array<map<name, Handler>, NumVersionSupported>, where NumVersionSupported is the number of API versions supported by this rippled server). If a handler does not change across different API versions, the same handler will be pointed by multiple entries of the array.
Add an "api_version" field in the request to indicate which API version the request is requesting.
{
"api_version": 1,
"id": 4,
"command": "...",
...
}
This is in line with our JSON-RPC request version field format. Following our websocket request format convention, it is placed on the top level. The rippled code changes required will be similar to JSON-RPC as shown in the section above.
The RPC command and its parameters passed to rippled command line are parsed first and a JSON-RPC request is created and sent to a rippled server. We will not support multiple versions of the command-parameter parsers. Hence all the JSON-RPC requests generated by a particular rippled software version will have one API version number. We will use the latest API version number. In the rippled code, the "api_version" field will be inserted into the JSON-RPC request before being sent to a rippled server.
The version number of gRPC requests are specified as part of the package name of gRPC’s service definition in .proto files. Some of the well known projects (such as uber and envoy) use this approach. We recommend to place the .proto files of different versions in version specific folders.
The example .proto file defines a version 1 service “Greeter” that has an RPC function “SayHello”.
syntax = "proto3";
package helloworld.v1;
import "helloworld_msg.proto";
service Greeter {
rpc SayHello (.helloworld_msg.HelloRequest) returns (.helloworld_msg.HelloReply) {}
}
In the generated cpp file, the package name is translated to namespaces. Among other code, it declares a server side “Greeter::AsyncService” class and a “RequestSayHello” method, and a client side “Greeter::Stub” class and a “SayHello” method. The gRPC clients and servers will use the “SayHello” method and the “RequestSayHello” method to communicate. The code below is reduced from the generated cpp file.
...
namespace helloworld {
namespace v1 {
class Greeter final {
public:
static constexpr char const* service_full_name() {
return "helloworld.v1.Greeter";
}
//server side
class WithAsyncMethod_SayHello : public BaseClass {
void RequestSayHello(::grpc::ServerContext* context, ::helloworld_msg::HelloRequest* request, ::grpc::ServerAsyncResponseWriter< ::helloworld_msg::HelloReply>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag);
};
typedef WithAsyncMethod_SayHello<Service > AsyncService;
//client side
class Stub final : public StubInterface {
::grpc::Status SayHello(::grpc::ClientContext* context, const ::helloworld_msg::HelloRequest& request, ::helloworld_msg::HelloReply* response) override;
};
static std::unique_ptr<Stub> NewStub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options = ::grpc::StubOptions());
}
At the gRPC server side, multiple versions of the API are created by constructing multiple services, e.g. helloworld::v1::Greeter::AsyncService. The services are located in different namespaces. They are simply different services from gRPC’s point of view. All of the services can be registered to a single gRPC server.
The gRPC client must know the gRPC server’s IP:port to connect. Once connected, the client can invoke RPCs from different API versions by constructing the stubs from different namespaces and then calling the stub methods. (In distributed computing, a stub means a piece of code that converts parameters passed between client and server. In the example above, helloworld::v1::Greeter::Stub is the client side class for invoking the RPC.) The matching of a client stub to the corresponding server service is done by the generated gRPC code behind the scenes.
One of the existing RPC functions is the Version function. When invoked, it currently replies:
"Version":{"first":"1.0.0","good":"1.0.0","last":"1.0.0"}
With the planned change, if request’s api_version is 1 or missing, it will reply:
"Version":{"first":"1.0.0","good":"2.0.0","last":"2.0.0"}
If request’s api_version is 2, it will reply:
"Version":{"api_version_high":2,"api_version_low":1},
According to the current design of the native support for gRPC in rippled, the code paths of processing a gRPC request and processing other types of requests are in parallel. The design of transcoding gRPC requests to JSON formatted requests which can be handled by the existing set of handlers was considered and discarded. In the future, it may be desirable to merge the code paths to provide a unified RPC approach with single source of truth. One approach is by transcoding JSON formatted requests to protobuf requests (via protobuf’s json_util, or other ways), and then invoking the protobuf requests’ handlers, the version number and command name should be parsed out and used to locate the handler. I.e., we may need something similar to the current rippled’s HandlerTable class.