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). The service has 4 methods. Its methods accept a name and reply with a greeting: samples/grpc_service/proto/samples/greeter.proto

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:

// 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;
std::vector<std::string> SayHelloResponseStream(std::string name) const;
std::string SayHelloRequestStream(const std::vector<std::string_view>& names) const;
std::vector<std::string> SayHelloStreams(const std::vector<std::string_view>& names) const;
private:
static std::unique_ptr<grpc::ClientContext> MakeClientContext();
api::GreeterServiceClient raw_client_;
};
class GreeterClientComponent final : public ugrpc::client::SimpleClientComponent<Client> {
public:
static constexpr std::string_view kName = "greeter-client";
GreeterClientComponent(const ::components::ComponentConfig& config, const ::components::ComponentContext& context)
: Base(config, context) {}
using Base::GetClient;
};

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.

Single request - single 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));
// Initiate the RPC. No actual actions have been taken thus far besides
// preparing to send the request.
auto stream = raw_client_.SayHello(request, MakeClientContext());
// 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());
}
std::unique_ptr<grpc::ClientContext> GreeterClient::MakeClientContext() {
// Deadline must be set manually for each RPC
// Note that here in all tests the deadline equals 20 sec which works for an
// example. However, generally speaking the 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}));
return context;
}

Single request - stream response RPC handling:

std::vector<std::string> GreeterClient::SayHelloResponseStream(std::string name) const {
api::GreetingRequest request;
request.set_name(std::move(name));
auto stream = raw_client_.SayHelloResponseStream(request, MakeClientContext());
api::GreetingResponse response;
std::vector<std::string> result;
constexpr auto kCountSend = 5;
for (int i = 0; i < kCountSend; i++) {
if (!stream.Read(response)) {
throw ugrpc::client::RpcError(stream.GetCallName(), "Missing responses");
}
result.push_back(std::move(*response.mutable_greeting()));
}
if (stream.Read(response)) {
throw ugrpc::client::RpcError(stream.GetCallName(), "Extra responses");
}
return result;
}

Stream request - single response RPC handling:

std::string GreeterClient::SayHelloRequestStream(const std::vector<std::string_view>& names) const {
auto stream = raw_client_.SayHelloRequestStream(MakeClientContext());
for (const auto& name : names) {
api::GreetingRequest request;
request.set_name(grpc::string(name));
stream.WriteAndCheck(request);
}
auto response = stream.Finish();
return std::move(*response.mutable_greeting());
}

Stream request - stream response RPC handling:

std::vector<std::string> GreeterClient::SayHelloStreams(const std::vector<std::string_view>& names) const {
auto stream = raw_client_.SayHelloStreams(MakeClientContext());
std::vector<std::string> result;
api::GreetingResponse response;
for (const auto& name : names) {
api::GreetingRequest request;
request.set_name(grpc::string(name));
stream.WriteAndCheck(request);
if (!stream.Read(response)) {
throw ugrpc::client::RpcError(stream.GetCallName(), "Missing responses before WritesDone");
}
result.push_back(std::move(*response.mutable_greeting()));
}
const bool is_success = stream.WritesDone();
LOG_DEBUG() << "Write task finish: " << is_success;
if (stream.Read(response)) {
throw ugrpc::client::RpcError(stream.GetCallName(), "Extra responses after WritesDone");
}
return result;
}

Fill in the static config entries for the client side:

# yaml
# Contains machinery common to all gRPC clients
grpc-client-common:
# The TaskProcessor for blocking connection initiation
blocking-task-processor: grpc-blocking-task-processor
# Creates gRPC clients
grpc-client-factory:
# The list of gRPC client middleware components to use
middlewares: []
# 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'
client-name: greeter
dedicated-channel-counts:
SayHello: 3
SayHelloRequestStream: 2
factory-component: grpc-client-factory
# 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);
SayHelloResult SayHello(CallContext& context, api::GreetingRequest&& request) override;
SayHelloResponseStreamResult SayHelloResponseStream(
CallContext& context,
api::GreetingRequest&& request,
SayHelloResponseStreamWriter& writer
) override;
SayHelloRequestStreamResult SayHelloRequestStream(CallContext& context, SayHelloRequestStreamReader& reader)
override;
SayHelloStreamsResult SayHelloStreams(CallContext& context, SayHelloStreamsReaderWriter& stream) override;
private:
const std::string prefix_;
};
class GreeterServiceComponent final : public ugrpc::server::ServiceComponentBase {
public:
static constexpr std::string_view kName = "greeter-service";
GreeterServiceComponent(const components::ComponentConfig& config, const components::ComponentContext& context);
static yaml_config::Schema GetStaticConfigSchema();
private:
GreeterService service_;
};
GreeterServiceComponent::GreeterServiceComponent(
)
: ugrpc::server::ServiceComponentBase(config, context), service_(config["greeting-prefix"].As<std::string>()) {
RegisterService(service_);
}

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

GreeterService::SayHelloResult GreeterService::SayHello(CallContext& /*context*/, 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 returning the response.
return response;
}

Single request - stream response RPC handling:

GreeterService::SayHelloResponseStreamResult GreeterService::SayHelloResponseStream(
CallContext& /*context*/,
api::GreetingRequest&& request,
SayHelloResponseStreamWriter& writer
) {
std::string message = fmt::format("{}, {}", prefix_, request.name());
api::GreetingResponse response;
constexpr auto kCountSend = 5;
for (auto i = 0; i < kCountSend; ++i) {
message.push_back('!');
response.set_greeting(grpc::string(message));
writer.Write(response);
}
return grpc::Status::OK;
}

Stream request - single response RPC handling:

GreeterService::SayHelloRequestStreamResult
GreeterService::SayHelloRequestStream(CallContext& /*context*/, SayHelloRequestStreamReader& reader) {
std::string message{};
api::GreetingRequest request;
while (reader.Read(request)) {
message.append(request.name());
}
api::GreetingResponse response;
response.set_greeting(fmt::format("{}, {}", prefix_, message));
return response;
}

Stream request - stream response RPC handling:

GreeterService::SayHelloStreamsResult
GreeterService::SayHelloStreams(CallContext& /*context*/, SayHelloStreamsReaderWriter& stream) {
std::string message;
api::GreetingRequest request;
api::GreetingResponse response;
while (stream.Read(request)) {
message.append(request.name());
response.set_greeting(fmt::format("{}, {}", prefix_, message));
stream.Write(response);
}
return grpc::Status::OK;
}

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 = components::MinimalServerComponentList()
// Contains machinery common to all gRPC clients
.Append<ugrpc::client::CommonComponent>()
// 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.
// All gRPC services are registered in this component.
.Append<ugrpc::server::ServerComponent>()
// 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,
stream_method_names=['SayHelloResponseStream', 'SayHelloStreams'],
)
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 single request - single response service client requests:

async def test_grpc_client_mock_say_hello(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?case=say_hello', data='tests')
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert response.text == 'Hello, tests from mockserver!'
assert _mock_say_hello.times_called == 1

To check single request - stream response service client requests:

async def test_grpc_client_mock_say_hello_response_stream(
service_client, mock_grpc_greeter,
):
@mock_grpc_greeter('SayHelloResponseStream')
async def _mock_say_hello_response_stream(request, context):
message = f'Hello, {request.name}'
for i in range(5):
message += '!'
yield greeter_protos.GreetingResponse(greeting=message)
response = await service_client.post(
'/hello?case=say_hello_response_stream', data='Python',
)
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert (
response.text
== """Hello, Python!
Hello, Python!!
Hello, Python!!!
Hello, Python!!!!
Hello, Python!!!!!
"""
)
assert _mock_say_hello_response_stream.times_called == 1

To check stream request - single response service client requests:

async def test_grpc_client_mock_say_hello_request_stream(
service_client, mock_grpc_greeter,
):
@mock_grpc_greeter('SayHelloRequestStream')
async def _mock_say_hello_request_stream(request_iterator, context):
message = 'Hello, '
async for request in request_iterator:
message += f'{request.name}'
return greeter_protos.GreetingResponse(greeting=message)
response = await service_client.post(
'/hello?case=say_hello_request_stream', data='Python\n!\n!\n!',
)
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert response.text == 'Hello, Python!!!'
assert _mock_say_hello_request_stream.times_called == 1

To check stream request - stream response service client requests:

async def test_grpc_client_mock_say_hello_streams(
service_client, mock_grpc_greeter,
):
@mock_grpc_greeter('SayHelloStreams')
async def _mock_say_hello_streams(request_iterator, context):
message = 'Hello, '
async for request in request_iterator:
message += f'{request.name}'
yield greeter_protos.GreetingResponse(greeting=message)
response = await service_client.post(
'/hello?case=say_hello_streams', data='Python\n!\n!\n!',
)
assert response.status == 200
assert 'text/plain' in response.headers['Content-Type']
assert (
response.text
== """Hello, Python
Hello, Python!
Hello, Python!!
Hello, Python!!!
"""
)
assert _mock_say_hello_streams.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 single request - single response 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!'

To do single request - stream response gRPC requests to the service:

async def test_say_hello_response_stream(grpc_client):
request = greeter_protos.GreetingRequest(name='Python')
message = 'Hello, Python'
async for response in grpc_client.SayHelloResponseStream(request):
message += '!'
assert response.greeting == message

To do stream request - single response gRPC requests to the service:

async def test_say_hello_request_stream(grpc_client):
request = (
greeter_protos.GreetingRequest(name=name)
for name in ['Python', '!', '!', '!']
)
response = await grpc_client.SayHelloRequestStream(request)
assert response.greeting == 'Hello, Python!!!'

To do stream request - stream response gRPC requests to the service:

async def test_say_hello_streams(grpc_client):
requests = (
greeter_protos.GreetingRequest(name=name)
for name in ['Python', '!', '!', '!']
)
message = 'Hello, Python'
async for response in grpc_client.SayHelloStreams(requests):
assert response.greeting == message
message += '!'

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

To implement unit testing for the sample gRPC service and client some preparational steps should be done.

Preparations

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_;
};

Unit testing for the gRPC service

Finally, we can create gRPC service and client in unit tests. To do single request - single response requests:

UTEST_F(GreeterServiceTest, SayHelloDirectCall) {
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, SayHelloCustomClient) {
// 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!");
}

To do single request - stream response service client requests:

UTEST_F(GreeterServiceTest, SayHelloResponseStreamCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const auto responses = client.SayHelloResponseStream("gtest");
EXPECT_THAT(
responses,
testing::ElementsAre(
"Hello, gtest!", "Hello, gtest!!", "Hello, gtest!!!", "Hello, gtest!!!!", "Hello, gtest!!!!!"
)
);
}

To do stream request - single response service client requests:

UTEST_F(GreeterServiceTest, SayHelloRequestStreamCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
const auto response = client.SayHelloRequestStream(names);
EXPECT_EQ(response, "Hello, gtest!!!");
}

To do stream request - stream response service client requests:

UTEST_F(GreeterServiceTest, SayHelloStreamsCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
const auto responses = client.SayHelloStreams(names);
EXPECT_THAT(responses, testing::ElementsAre("Hello, gtest", "Hello, gtest!", "Hello, gtest!!", "Hello, gtest!!!"));
}

Unit testing for the gRPC client

We can use toy test-only services to test the service client as a unit.

Testing single request - single response client RPC-handling:

namespace {
class GreeterMock final : public samples::api::GreeterServiceBase {
public:
SayHelloResult SayHello(CallContext& /*context*/, ::samples::api::GreetingRequest&& /*request*/) override {
samples::api::GreetingResponse response;
response.set_greeting("Mocked response");
return response;
}
SayHelloResponseStreamResult SayHelloResponseStream(
CallContext& context,
::samples::api::GreetingRequest&& request,
SayHelloResponseStreamWriter& writer
) override;
SayHelloRequestStreamResult SayHelloRequestStream(CallContext& context, SayHelloRequestStreamReader& reader)
override;
SayHelloStreamsResult SayHelloStreams(CallContext& context, SayHelloStreamsReaderWriter& stream) override;
};
// Default-constructs GreeterMock.
} // namespace
UTEST_F(GreeterClientTest, SayHelloMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const auto response = client.SayHello("gtest");
EXPECT_EQ(response, "Mocked response");
}

Testing single request - stream response client RPC-handling:

GreeterMock::SayHelloResponseStreamResult GreeterMock::SayHelloResponseStream(
CallContext& /*context*/,
::samples::api::GreetingRequest&& /*request*/,
SayHelloResponseStreamWriter& writer
) {
samples::api::GreetingResponse response;
std::string message = "Mocked response";
int kCountSend = 5;
for (auto i = 0; i < kCountSend; ++i) {
message.push_back('!');
response.set_greeting(grpc::string(message));
writer.Write(response);
}
return grpc::Status::OK;
}
UTEST_F(GreeterClientTest, SayHelloResponseStreamMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const auto responses = client.SayHelloResponseStream("gtest");
EXPECT_THAT(
responses,
testing::ElementsAre(
"Mocked response!", "Mocked response!!", "Mocked response!!!", "Mocked response!!!!", "Mocked response!!!!!"
)
);
}

Testing stream request - single response client RPC-handling:

GreeterMock::SayHelloRequestStreamResult
GreeterMock::SayHelloRequestStream(CallContext& /*context*/, SayHelloRequestStreamReader& reader) {
samples::api::GreetingResponse response;
samples::api::GreetingRequest request;
while (reader.Read(request)) {
}
response.set_greeting("Mocked response!!!");
return response;
}
UTEST_F(GreeterClientTest, SayHelloRequestStreamMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
auto response = client.SayHelloRequestStream(names);
EXPECT_EQ(response, "Mocked response!!!");
}

Testing stream request - stream response client RPC-handling:

GreeterMock::SayHelloStreamsResult
GreeterMock::SayHelloStreams(CallContext& /*context*/, SayHelloStreamsReaderWriter& stream) {
samples::api::GreetingResponse response;
std::string message = "Mocked response";
samples::api::GreetingRequest request;
while (stream.Read(request)) {
response.set_greeting(grpc::string(message));
stream.Write(response);
message.push_back('!');
}
return grpc::Status::OK;
}
UTEST_F(GreeterClientTest, SayHelloStreamsMockedServiceCustomClient) {
const samples::GreeterClient client{MakeClient<samples::api::GreeterServiceClient>()};
const std::vector<std::string_view> names = {"gtest", "!", "!", "!"};
const auto responses = client.SayHelloStreams(names);
EXPECT_THAT(
responses,
testing::ElementsAre("Mocked response", "Mocked response!", "Mocked response!!", "Mocked response!!!")
);
}

Full sources

See the full example at: