Core concepts

View Source

This page covers the concepts you need to work with livery_grpc: the service definition, the four call types, and how they map to Erlang.

Service definition

You start with a .proto file. It declares a service, its methods, and the messages they exchange.

syntax = "proto3";
package helloworld;

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

message HelloRequest { string name = 1; }
message HelloReply   { string message = 1; }

At build time, rebar3_gpb_plugin compiles this to an Erlang module using gpb in maps mode. Messages are plain maps: #{name => <<"ada">>}. livery_grpc reads the service's methods, message types, and call kinds from that module at runtime, so there is no separate generated dispatch layer to keep in sync.

How the module and names are set

The generated module name is the proto file name plus the module_name_suffix from gpb_opts (_pb by convention): helloworld.proto compiles to helloworld_pb, and route_guide.proto to route_guide_pb. The file name sets the module; the package line does not.

For the helloworld.proto above:

In the protoIn Erlang
file helloworld.protomodule helloworld_pb
service Greeterservice atom 'Greeter'
rpc SayHello(...)method atom 'SayHello'
message HelloRequesta map #{name => _}

So you name a method with the generated module and the proto's service and method atoms:

{ok, Method} = livery_grpc_client:method(helloworld_pb, 'Greeter', 'SayHello').

The wire path is built from the package and these names: /helloworld.Greeter/SayHello. For the rebar3 and gpb_opts setup that produces the module, see the Erlang integration guide.

Two modules: generated messages and your handler

There are two modules per service, and they are different things:

  • The generated *_pb module (helloworld_pb, from helloworld.proto) holds the messages and their encode/decode. It is generated; you never edit it.
  • Your handler module holds the RPC functions (say_hello/2, ...). You write it, and you choose its name; it does not have to match the proto. The RouteGuide example names its handler route_guide, alongside the generated route_guide_pb.

You connect the two when you start the server: proto is the generated module, handler is yours.

livery_grpc:start_server(#{
    services => [#{proto   => route_guide_pb,   %% generated messages
                   service => 'RouteGuide',
                   handler => route_guide}]     %% your functions
}).

The four call types

gRPC has four call types. livery_grpc supports all of them.

  • Unary: one request, one reply. The most common shape.
  • Server-streaming: one request, a stream of replies.
  • Client-streaming: a stream of requests, one reply.
  • Bidirectional: both sides stream, independently.

On the server, each method is one Erlang function whose name is the RPC name in snake_case. The shape of the function follows the call type:

%% unary: request and context in, reply out
say_hello(#{name := Name}, _Ctx) ->
    {ok, #{message => <<"hello ", Name/binary>>}}.

%% server-streaming: a Send function pushes replies
say_hello_stream(#{name := Name}, Send, _Ctx) ->
    Send(#{message => <<"hi ", Name/binary>>}),
    ok.

%% client-streaming and bidirectional: a stream handle to recv (and send)
say_hello_collect(Stream, _Ctx) ->
    {ok, Requests, _} = livery_grpc_stream:recv_all(Stream),
    {ok, #{message => summarize(Requests)}}.

See the streaming guide for each shape in full.

The context

Every callback receives a context map, Ctx. It carries the call metadata, the method descriptor, the deadline, and the underlying request:

#{metadata := [{binary(), binary()}],
  method   := map(),
  deadline := timeout(),
  req      := livery_req:req()}

Metadata

Metadata is key-value pairs sent alongside a call, as HTTP/2 headers. A client attaches it per call; a handler reads it from Ctx. Use it for auth tokens, request ids, and tracing. See the metadata guide.

Deadlines

A client sets a deadline; it travels on the wire as grpc-timeout. The server exposes it in Ctx and aborts a unary handler that overruns. See the deadlines guide.

Status

Every call ends with a status. Success is ok; an error is a status code (such as not_found or invalid_argument) with an optional message and details. A handler returns {error, Status} or {error, {Status, Msg}}; a client sees {error, {Status, Msg}}. See the error handling and status codes guides.

Multiple services

A server takes a list of services, so you run many on one listener. Each entry binds a generated module, a service name, and a handler:

livery_grpc:start_server(#{
    port     => 50051,
    services => [
        #{proto => greeter_pb,     service => 'Greeter',    handler => my_greeter},
        #{proto => route_guide_pb, service => 'RouteGuide', handler => route_guide},
        livery_grpc_health:service()
    ],
    reflection => true
}).

The dispatcher routes each call by its path (/package.Service/Method), so the services coexist with no conflict. The built-in health and reflection services are just entries in the same list. A single .proto can also declare several service blocks; register one entry per service (same module, different service atom).

Start a second server only when you need a separate port, TLS config, or isolation. The usual pattern is one server with many services.

Connections

A client opens a connection with livery_grpc_client:connect/2,3 and makes many calls on it. A server is a dedicated listener started with livery_grpc:start_server/1. Both speak gRPC over HTTP/2.