This repository is an example of a Elixir gRPC service that uses grpcbox, a Erlang gRPC library. There aren't many examples demonstrating how this is done, so this example is a very naive attempt to utilise grpcbox as the main gRPC plumbing, while we can write our business/controller logic in Elixir.
- Erlang
- Elixir
Some prior knowledge on how Elixir maps to Erlang is required.
This project uses the umbrella project pattern, which allows us to house the .erl as well as the .ex files in one location. grpcbox has a sibling project called grpcbox_plugin, utilised to generate the Erlang gRPC stub files/modules. We need to refer to these modules in our Elixir configuration, typically found in config/config.exs.
To get started we need to start an umbrella project:
$ mix new boxy --umbrella --supThis will create the apps directory, in which we need to create two applications:
- The Elixir application that will actually run the gRPC server via
grpcbox. - The Erlang application that is primarily used to build the gRPC stub files.
In our apps directory, we need to run the following:
$ mix new boxy_elixir --supAnd
$ rebar3 new app boxy_erlangIn our boxy_erlang project, we need to define a number of dependencies in rebar.config:
{erl_opts, [debug_info]}.
{deps, [grpcbox]}.
{grpc, [{protos, "protos"},
{gpb_opts, [{module_name_suffix, "_pb"}]}]}.
{plugins, [grpcbox_plugin]}.
{shell, [
% {config, "config/sys.config"},
{apps, [boxy_erlang]}
]}.
The grpcbox library will be used to generate our Erlang protobuf stubs via grpcbox_plugin which adds a rebar3 grpc gen command.
Let's get started by generating our Erlang stubs. Provided that we have a .proto file like this in boxy/apps/boxy_erlang/proto/:
syntax = "proto3";
package example;
service HelloService {
rpc Hello(HelloRequest) returns (HelloResponse);
rpc Greet(GreetRequest) returns (stream GreetResponse);
}
message GreetRequest {
string name = 1;
}
message GreetResponse {
string response = 1;
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string response = 1;
}Running rebar3 grpc gen should generate our Erlang stubs as follows:
$ cd apps/boxy_erlang
$ rebar3 grpc gen
===> Writing /.../boxy_erlang/src/hello_world_pb.erlThis might complain about not being able to write to a
_builddirectory in theboxy_erlangdirectory.
Now that we have our protobuf Erlang modules, we can access it in Elixir as :hello_world_pb. We can also utilise alias ..., as: ... to make it more idiomatic.
In boxy_elixir, we need to add a couple of dependencies,
# mix.exs
defp deps do
[
{:grpcbox, "~> 0.15.0"},
{:chatterbox,
git: "https://github.com/tsloughter/chatterbox.git", tag: "v0.12.0", override: true},
{:boxy_erlang, in_umbrella: true, manager: :rebar3}
]
endgrpcboxcontains the code necessary to start a gRPC server.chatterboxis the HTTP/2 library used bygrpcbox.boxy_erlangis a sibling application, used to house the Erlang stub files.
Run mix deps.get to get our dependencies. Next, we generate our configuration in the umbrella configuration that maps to the grpcbox sys.config configuration:
# config/config.exs
config :boxy_elixir,
client: %{
channels: [
default_channel: [
{:http, "localhost", 8080, []},
%{}
]
]
},
servers: [
%{
grpc_opts: %{
service_protos: [:hello_world_pb],
unary_interceptors: [&BoxyElixir.LoggingMiddleware.log/4],
services: %{
:"grpc.health.v1.Health" => :grpcbox_health_service,
:"example.HelloService" => BoxyElixir.HelloController
}
},
transport_opts: %{ssl: false},
listen_opts: %{
port: 8080,
ip: {0, 0, 0, 0}
},
pool_opts: %{size: 50},
server_opts: %{
header_table_size: 4096,
enable_push: 1,
max_concurrent_streams: :unlimited,
initial_window_size: 65535,
max_frame_size: 16384,
max_header_list_size: :unlimited
}
}
]
We can map the protobuf service definitions to our Elixir controller: BoxyElixir.HelloController. This allows us to utilise Elixir code at the edge of our business logic. A controller can look something like this:
defmodule BoxyElixir.HelloController do
def hello(ctx, request) do
{:ok, %{response: "Welcome #{request.name}"}, ctx}
end
def greet(_message, stream) do
Enum.each(1..10, fn count ->
IO.inspect("sending #{count}")
:grpcbox_stream.send(%{response: "Hello #{count}"}, stream)
Process.sleep(5_000)
end)
:ok
end
endWe're still bound by the callbacks that are specified in the :grpcbox implementation, but we can return maps and lists that represent our response types. Last by not least, we need to start the :grpcbox supervisor in our application supervision tree:
defmodule BoxyElixir.Application do
use Application
@impl true
def start(_type, _args) do
# An application can host multiple servers, so we need to generate a child spec
# for each entry
children =
for s <- servers(),
do:
grpc_child_spec(
s.server_opts,
s.grpc_opts,
s.listen_opts,
s.pool_opts,
s.transport_opts
)
opts = [strategy: :one_for_one, name: BoxyElixir.Supervisor]
Supervisor.start_link(children, opts)
end
defp servers, do: Application.get_env(:boxy_elixir, :servers)
# A simple wrapper on top of the `:grpcbox` module to define our server child specs.
defp grpc_child_spec(server_opts, grpc_opts, listen_opts, pool_opts, transport_opts) do
:grpcbox.server_child_spec(
server_opts || %{},
grpc_opts(grpc_opts || %{}),
listen_opts || %{},
pool_opts || %{},
transport_opts || %{}
)
end
def grpc_opts(opts) do
interceptors = opts.unary_interceptors || []
Map.put(opts, :unary_interceptor, :grpcbox_chain_interceptor.unary(interceptors))
end
endWe can start the gRPC server by running the application as you normally would:
$ iex -S mixProvided that you have a logger interceptor defined, making gRPC requests to this endpoint should result in a response.