gRPC API versioning

37 Views Asked by At

There is a situation where I need to implement 2 versions of the grpc API on my server. How can I do this?

syntax = "proto3";

package greet.v1;

will help me to add versioning to my pb2 autogenerated files, but I completely don't understand how to combine both pb2(v1 and v2) on the same server. Maybe anyone has an example of grpc versioning in Python? Or maybe it's a bad practice to use 2 versions in one server and I should use another pod for a new grpc version..

1

There are 1 best solutions below

0
DazWilkin On

Apologies for the weak example.

It's possible to run a single server with multiple (versions of) services and there's no intrinsic reason why you wouldn't want to do this.

Just as you can share Python classes|methods (e.g. using packages), you can share the implementation details of classes for your servicers.

It's good practice to keep implementations of stubs in distinct packages (partly because of the way gRPC|protobuf use packages in Python) but as a way to (more accurately) reflect the structure and versioning of your code.

For example, ideally, you'd not want to change the server implementation (simplistically reflected here by main.py) when you have minor version changes in v1 or v2; you'd prefer to simply bump the requirements.txt packages.

I'm assuming that you've breaking changes between v1 and v2 and have similar in the following:

protos/greet/v1/greet.proto:

syntax = "proto3";

// Reflect package in folder path i.e. protos/greet/v1
package greet.v1;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

protos/greet/v2/greet.proto:

syntax = "proto3";

// Reflect package in folder path i.e. protos/greet/v1
package greet.v2;

// Reuse (!) 'greet.v1.HelloReply'
import "greet/v1/greet.proto";

service Greeter {
  rpc SayHello (HelloRequest) returns (greet.v1.HelloReply) {}
}

message HelloRequest {
  fixed32  id = 1;
}

And:

requirements.txt:

grpcio==1.62.0
grpcio-reflection==1.62.0
grpcio-tools==1.62.0
protobuf==4.25.3
python3 -m venv venv
source venv/bin/activate

python3 -m pip install --requirement requirements.txt

python3 \
-m grpc_tools.protoc \
--proto_path=${PWD}/protos \
--python_out=${PWD} \
--pyi_out=${PWD} \
--grpc_python_out=${PWD} \
${PWD}/protos/greet/v1/greet.proto \
${PWD}/protos/greet/v2/greet.proto

And: main.py:

# Python gRPC service reflection
from grpc_reflection.v1alpha import reflection

from concurrent import futures
import grpc

# v1
import greet.v1.greet_pb2 as greet_v1_pb2
import greet.v1.greet_pb2_grpc as greet_v1_pb2_grpc


class GreeterV1(greet_v1_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return greet_v1_pb2.HelloReply(message=f"Hello {request.name}")

# v2
import greet.v2.greet_pb2 as greet_v2_pb2
import greet.v2.greet_pb2_grpc as greet_v2_pb2_grpc


class GreeterV2(greet_v2_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return greet_v1_pb2.HelloReply(message=f"Hello {request.id}")


def serve():
    # One service
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

    # Adds v1 and v2 services
    greet_v1_pb2_grpc.add_GreeterServicer_to_server(GreeterV1(), server)
    greet_v2_pb2_grpc.add_GreeterServicer_to_server(GreeterV2(), server)

    SERVICE_NAMES = (
        greet_v1_pb2.DESCRIPTOR.services_by_name['Greeter'].full_name,
        greet_v2_pb2.DESCRIPTOR.services_by_name['Greeter'].full_name,
    )
    # Adds third service (!)
    reflection.enable_server_reflection(SERVICE_NAMES, server)

    server.add_insecure_port("[::]:50051")
    server.start()
    server.wait_for_termination()

if __name__ == "__main__":
    serve()

And:

Enumerate v1 Method

grpcurl \
-plaintext \
localhost:50051 \
list greet.v1.Greeter
greet.v1.Greeter.SayHello

Invoke v1 Method

grpcurl \
-plaintext \
-d '{"name":"Freddie"}' \
localhost:50051 \
greet.v1.Greeter.SayHello
{
  "message": "Hello Freddie"
}

Enumerate v2 Method

grpcurl \
-plaintext \
localhost:50051 \
list greet.v2.Greeter
greet.v2.Greeter.SayHello

Invoke v2 Method

grpcurl \
-plaintext \
-d '{"id":314159265}' \
localhost:50051 \
greet.v2.Greeter.SayHello
{
  "message": "Hello 314159265"
}

NOTE Python's gRPC reflection service appears to not work with describe {method}.