Giter Site home page Giter Site logo

boxy's Introduction

Boxy: A Elixir gRPC project using grpcbox

Introduction

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.

Requirements

  • Erlang
  • Elixir

Some prior knowledge on how Elixir maps to Erlang is required.

Project Layout

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 --sup

This 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 --sup

And

$ rebar3 new app boxy_erlang

Erlang project

In 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.erl

This might complain about not being able to write to a _build directory in the boxy_erlang directory.

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.

Elixir project

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}
    ]
  end
  • grpcbox contains the code necessary to start a gRPC server.
  • chatterbox is the HTTP/2 library used by grpcbox.
  • boxy_erlang is 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
end

We'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
end

Starting the server

We can start the gRPC server by running the application as you normally would:

$ iex -S mix

Provided that you have a logger interceptor defined, making gRPC requests to this endpoint should result in a response.

boxy's People

Contributors

nathancyam avatar

Stargazers

 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.