userver: gRPC client and service
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
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.

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

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

include(GrpcTargets)
add_grpc_library(${PROJECT_NAME}-proto PROTOS samples/greeter.proto)
target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}-proto)

Register the necessary ugrpc components:

.Append<ugrpc::client::ClientFactoryComponent>()

The client side

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

// A user-defined wrapper around api::GreeterServiceClient that provides
// a simplified interface.
class GreeterClient final : public components::LoggableComponentBase {
public:
static constexpr std::string_view kName = "greeter-client";
GreeterClient(const components::ComponentConfig& config,
: LoggableComponentBase(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>())) {}
std::string SayHello(std::string name);
static yaml_config::Schema GetStaticConfigSchema();
private:
ugrpc::client::ClientFactory& client_factory_;
api::GreeterServiceClient client_;
};

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) {
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 = 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
thread_name: grpc-worker
worker_threads: 2

The server side

Implement the generated api::GreeterServiceBase. As a convenience, a derived api::GreeterServiceBase::Component class is provided for easy integration with the component system.

class GreeterServiceComponent final
: public api::GreeterServiceBase::Component {
public:
static constexpr std::string_view kName = "greeter-service";
GreeterServiceComponent(const components::ComponentConfig& config,
: api::GreeterServiceBase::Component(config, context),
prefix_(config["greeting-prefix"].As<std::string>()) {}
void SayHello(SayHelloCall& call, api::GreetingRequest&& request) override;
static yaml_config::Schema GetStaticConfigSchema();
private:
const std::string prefix_;
};

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

void GreeterServiceComponent::SayHello(
api::GreeterServiceBase::SayHelloCall& call,
api::GreetingRequest&& request) {
// Authentication checking could have gone here. For this example, we trust
// the world.
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: 8091
# 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 =
.Append<samples::http_cache::HttpCachedTranslations>()
.Append<samples::http_cache::GreetUser>()
.Append<server::handlers::TestsControl>()
.Append<components::HttpClient>();
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> (do not forget to prepare the configuration files!).

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

Functional testing

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(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!'

Full sources

See the full example at: