userver: gRPC client and service
Loading...
Searching...
No Matches
gRPC client and service

Before you start

Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.

Make sure that you understand the basic concepts of userver grpc driver.

Step by step guide

In this example, we will write a client side and a server side for a simple GreeterService from greeter.proto (see the schema below). Its single SayHello method accepts a name string and replies with a corresponding greeting string.

Installation

Find and link to userver gRPC:

find_package(userver COMPONENTS grpc REQUIRED)
add_library(${PROJECT_NAME}_objs OBJECT
# Note: it's nonsense to have the same client and service in the same executable.
# For test and demonstration purposes only.
src/greeter_client.cpp
src/greeter_service.cpp
)
target_link_libraries(${PROJECT_NAME}_objs PUBLIC userver::grpc)
target_include_directories(${PROJECT_NAME}_objs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)

Generate and link to a CMake library from our .proto schema:

userver_add_grpc_library(${PROJECT_NAME}-proto PROTOS samples/greeter.proto)
target_link_libraries(${PROJECT_NAME}_objs PUBLIC ${PROJECT_NAME}-proto)

By default, userver_add_grpc_library looks in ${CMAKE_CURRENT_SOURCE_DIR}/proto, you can override this using the SOURCE_PATH option.

Proto includes can be specified in INCLUDE_DIRECTORIES option (multiple directories can be specified).

The client side

Wrap the generated api::GreeterServiceClient in a component that exposes a simplified interface:

#include <samples/greeter_client.usrv.pb.hpp>
// A user-defined wrapper around api::GreeterServiceClient that handles
// the metadata and deadline bureaucracy and provides a simplified interface.
//
// Alternatively, you can use ugrpc::client::SimpleClientComponent directly.
//
// Note that we have both service and client to that service in the same
// microservice. Ignore that, it's just for the sake of example.
class GreeterClient final {
public:
explicit GreeterClient(api::GreeterServiceClient&& raw_client);
std::string SayHello(std::string name) const;
private:
api::GreeterServiceClient raw_client_;
};
class GreeterClientComponent final : public components::ComponentBase {
public:
static constexpr std::string_view kName = "greeter-client";
GreeterClientComponent(const components::ComponentConfig& config,
const GreeterClient& GetClient() const;
static yaml_config::Schema GetStaticConfigSchema();
private:
ugrpc::client::ClientFactory& client_factory_;
GreeterClient client_;
};
GreeterClientComponent::GreeterClientComponent(
: ComponentBase(config, context),
// ClientFactory is used to create gRPC clients
client_factory_(
context.FindComponent<ugrpc::client::ClientFactoryComponent>()
.GetFactory()),
// The client needs a fixed endpoint
client_(client_factory_.MakeClient<api::GreeterServiceClient>(
"greeter", config["endpoint"].As<std::string>())) {}

We intentionally split GreeterClient from GreeterClientComponent to make the logic unit-testable. If you don't need gtest tests, you can put the logic into the component directly.

A single request-response RPC handling is simple: fill in request and context, initiate the RPC, receive the response.

std::string GreeterClient::SayHello(std::string name) const {
api::GreetingRequest request;
request.set_name(std::move(name));
// Deadline must be set manually for each RPC
auto context = std::make_unique<grpc::ClientContext>();
context->set_deadline(
engine::Deadline::FromDuration(std::chrono::seconds{20}));
// Initiate the RPC. No actual actions have been taken thus far besides
// preparing to send the request.
auto stream = raw_client_.SayHello(request, std::move(context));
// Complete the unary RPC by sending the request and receiving the response.
// The client should call `Finish` (in case of single response) or `Read`
// until `false` (in case of response stream), otherwise the RPC will be
// cancelled.
api::GreetingResponse response = stream.Finish();
return std::move(*response.mutable_greeting());
}

Fill in the static config entries for the client side:

# yaml
# Creates gRPC clients
grpc-client-factory:
# The TaskProcessor for blocking connection initiation
task-processor: grpc-blocking-task-processor
# Optional channel parameters for gRPC Core
# https://grpc.github.io/grpc/core/group__grpc__arg__keys.html
channel-args: {}
# Our wrapper around the generated client for GreeterService
greeter-client:
# The service endpoint (URI). We talk to our own service,
# which is kind of pointless, but works for an example
endpoint: '[::1]:8091'
# yaml
task_processors:
grpc-blocking-task-processor: # For blocking gRPC channel creation
worker_threads: 2
thread_name: grpc-worker

The server side

Implement the generated api::GreeterServiceBase. As a convenience, api::GreeterServiceBase::Component base class is provided that inherits from both api::GreeterServiceBase and ugrpc::server::ServiceComponentBase. However, for in this example we will also test our service using gtest, so we need to split the logic from the component.

#include <samples/greeter_service.usrv.pb.hpp>
class GreeterService final : public api::GreeterServiceBase {
public:
explicit GreeterService(std::string prefix);
void SayHello(SayHelloCall& call, api::GreetingRequest&& request) override;
private:
const std::string prefix_;
};
class GreeterServiceComponent final
public:
static constexpr std::string_view kName = "greeter-service";
GreeterServiceComponent(const components::ComponentConfig& config,
static yaml_config::Schema GetStaticConfigSchema();
private:
GreeterService service_;
};
GreeterServiceComponent::GreeterServiceComponent(
: ugrpc::server::ServiceComponentBase(config, context),
service_(config["greeting-prefix"].As<std::string>()) {
RegisterService(service_);
}

A single request-response RPC handling is simple: fill in the response and send it.

void GreeterService::SayHello(api::GreeterServiceBase::SayHelloCall& call,
api::GreetingRequest&& request) {
// Authentication checking could have gone here.
// Or even better, use a global gRPC authentication middleware.
api::GreetingResponse response;
response.set_greeting(fmt::format("{}, {}!", prefix_, request.name()));
// Complete the RPC by sending the response. The service should complete
// each request by calling `Finish` or `FinishWithError`, otherwise the
// client will receive an Internal Error (500) response.
call.Finish(response);
}

Fill in the static config entries for the server side:

# yaml
# Common configuration for gRPC server
grpc-server:
# The single listening port for incoming RPCs
port: $grpc_server_port
port#fallback: 8091
completion-queue-count: 3
# Our GreeterService implementation
greeter-service:
task-processor: main-task-processor
greeting-prefix: Hello
middlewares: []

int main()

Finally, we register our components and start the server.

int main(int argc, char* argv[]) {
const auto component_list =
// Default client factory. You can create multiple instances of this
// component using `.Append<T>("name")` if different gRPC clients
// require different credentials or different grpc-core options.
.Append<ugrpc::client::ClientFactoryComponent>()
// All gRPC services are registered in this component.
// Custom components:
.Append<samples::GreeterClientComponent>()
.Append<samples::GreeterServiceComponent>()
.Append<samples::CallGreeterClientTestHandler>();
return utils::DaemonMain(argc, argv, component_list);
}

Build and Run

To build the sample, execute the following build steps at the userver root directory:

mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-grpc_service

The sample could be started by running make start-userver-samples-grpc_service. The command would invoke testsuite start target that sets proper paths in the configuration files and starts the service.

To start the service manually run ./samples/grpc_service/userver-samples-grpc_service -c </path/to/static_config.yaml>.

The service is available locally at port 8091 (as per our static_config.yaml).

Functional testing for the sample gRPC service and client

To implement Functional tests for the service some preparational steps should be done.

Preparations

First of all, import the required modules and add the required pytest_userver.plugins.grpc pytest plugin:

import pytest
import samples.greeter_pb2_grpc as greeter_services # noqa: E402, E501
pytest_plugins = ['pytest_userver.plugins.grpc']

gRPC server mock

To mock the gRPC server provide a hook for the static config to change the endpoint:

USERVER_CONFIG_HOOKS = ['prepare_service_config']
@pytest.fixture(scope='session')
def prepare_service_config(grpc_mockserver_endpoint):
def patch_config(config, config_vars):
components = config['components_manager']['components']
components['greeter-client']['endpoint'] = grpc_mockserver_endpoint
return patch_config

Write the mocking fixtures using grpc_mockserver:

@pytest.fixture(name='mock_grpc_greeter_session', scope='session')
def _mock_grpc_greeter_session(grpc_mockserver, create_grpc_mock):
mock = create_grpc_mock(greeter_services.GreeterServiceServicer)
greeter_services.add_GreeterServiceServicer_to_server(
mock.servicer, grpc_mockserver,
)
return mock
@pytest.fixture
def mock_grpc_greeter(mock_grpc_greeter_session):
with mock_grpc_greeter_session.mock() as mock:
yield mock

After that everything is ready to check the service client requests:

async def test_grpc_client(service_client, mock_grpc_greeter):
@mock_grpc_greeter('SayHello')
async def _mock_say_hello(request, context):
return greeter_protos.GreetingResponse(
greeting=f'Hello, {request.name} from mockserver!',
)
response = await service_client.post(
'/hello', data='tests', headers={'Content-type': 'text/plain'},
)
assert response.status == 200
assert response.content == b'Hello, tests from mockserver!'
assert _mock_say_hello.times_called == 1

gRPC client

To do the gRPC requests write a client fixture using grpc_channel:

@pytest.fixture
def grpc_client(grpc_channel, service_client):
return greeter_services.GreeterServiceStub(grpc_channel)

Use it to do gRPC requests to the service:

async def test_say_hello(grpc_client):
request = greeter_protos.GreetingRequest(name='Python')
response = await grpc_client.SayHello(request)
assert response.greeting == 'Hello, Python!'

Unit testing for the sample gRPC service and client (gtest)

First, link the unit tests to userver::grpc-utest:

add_executable(${PROJECT_NAME}-unittest unittests/greeter_service_test.cpp)
target_link_libraries(${PROJECT_NAME}-unittest
${PROJECT_NAME}_objs
userver::utest
userver::grpc-utest
)
add_google_tests(${PROJECT_NAME}-unittest)

Create a fixture that sets up the gRPC service in unit tests:

class GreeterServiceTest : public ugrpc::tests::ServiceFixtureBase {
protected:
GreeterServiceTest() : service_(prefix_) {
RegisterService(service_);
}
~GreeterServiceTest() override { StopServer(); }
private:
const std::string prefix_{"Hello"};
// We've made sure to separate the logic into samples::GreeterService that is
// detached from the component system and only depends on things obtainable
// in gtest tests.
samples::GreeterService service_;
};

Finally, we can create gRPC service and client in unit tests:

UTEST_F(GreeterServiceTest, DirectCall) {
const auto client = MakeClient<samples::api::GreeterServiceClient>();
samples::api::GreetingRequest request;
request.set_name("gtest");
const auto response = client.SayHello(request).Finish();
EXPECT_EQ(response.greeting(), "Hello, gtest!");
}
UTEST_F(GreeterServiceTest, CustomClient) {
// We've made sure to separate some logic into samples::GreeterClient that is
// detached from the component system, it only needs the gRPC client, which we
// can create in gtest tests.
const samples::GreeterClient client{
MakeClient<samples::api::GreeterServiceClient>()};
const auto response = client.SayHello("gtest");
EXPECT_EQ(response, "Hello, gtest!");
}

We can also use toy test-only gRPC services for unit tests:

namespace {
class GreeterMock final : public samples::api::GreeterServiceBase {
public:
void SayHello(SayHelloCall& call,
samples::api::GreetingRequest&& /*request*/) override {
samples::api::GreetingResponse response;
response.set_greeting("Mocked response");
call.Finish(response);
}
};
// Default-constructs GreeterMock.
} // namespace
UTEST_F(GreeterClientTest, MockedServiceCustomClient) {
const samples::GreeterClient client{
MakeClient<samples::api::GreeterServiceClient>()};
const auto response = client.SayHello("gtest");
EXPECT_EQ(response, "Mocked response");
}

Full sources

See the full example at: