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.

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

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: $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 =
.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>.

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

Full sources

See the full example at: