• Stars
    star
    127
  • Rank 282,790 (Top 6 %)
  • Language
    Elixir
  • License
    Apache License 2.0
  • Created about 5 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Elixir implementation of the twirp RPC framework

Twirp

Twirp provides an Elixir implementation of the Twirp RPC framework developed by Twitch. The protocol defines semantics for routing and serialization of RPCs based on protobufs.

https://hexdocs.pm/twirp.

Installation

Add Twirp to your list of dependencies:

def deps do
  [
    {:twirp, "~> 0.8"}
  ]
end

If you want to be able to generate services and clients based on proto files you'll need the protoc compiler (brew install protobuf on MacOS) and both the twirp and elixir protobuf plugins:

$ mix escript.install hex protobuf
$ mix escript.install hex twirp

Both of these escripts will need to be in your $PATH in order for protoc to find them.

Example

The canonical Twirp example is a Haberdasher service. Here's the protobuf description for the service.

syntax = "proto3";

package example;

// Haberdasher service makes hats for clients.
service Haberdasher {
  // MakeHat produces a hat of mysterious, randomly-selected color!
  rpc MakeHat(Size) returns (Hat);
}

// Size of a Hat, in inches.
message Size {
  int32 inches = 1; // must be > 0
}

// A Hat is a piece of headwear made by a Haberdasher.
message Hat {
  int32 inches = 1;
  string color = 2; // anything but "invisible"
  string name = 3; // i.e. "bowler"
}

We'll assume for now that this proto file lives in priv/protos/service.proto

Code generation

We can now use protoc to generate the files we need. You can run this command from the root directory of your project.

$ protoc --proto_path=./priv/protos --elixir_out=./lib/example --twirp_elixir_out=./lib/example ./priv/protos/service.proto

After running this command there should be 2 files located in lib/example.

The message definitions:

defmodule Example.Size do
  @moduledoc false
  use Protobuf, syntax: :proto3

  @type t :: %__MODULE__{
          inches: integer
        }

  defstruct [:inches]

  field :inches, 1, type: :int32
end

defmodule Example.Hat do
  @moduledoc false
  use Protobuf, syntax: :proto3

  @type t :: %__MODULE__{
          inches: integer,
          color: String.t(),
          name: String.t()
        }
  defstruct [:inches, :color, :name]

  field :inches, 1, type: :int32
  field :color, 2, type: :string
  field :name, 3, type: :string
end

The service and client definition:

defmodule Example.HaberdasherService do
  @moduledoc false
  use Twirp.Service

  package "example"
  service "Haberdasher"

  rpc :MakeHat, Example.Size, Example.Hat, :make_hat
end

defmodule Example.HaberdasherClient do
  @moduledoc false
  # client implementation...
end

Implementing the server

Now that we've generated the service definition we can implement a "handler" module that will implement each "method".

defmodule Example.HaberdasherHandler do
  @colors ~w|white black brown red blue|
  @names ["bowler", "baseball cap", "top hat", "derby"]

  def make_hat(_ctx, size) do
    if size.inches <= 0 do
      Twirp.Error.invalid_argument("I can't make a hat that small!")
    else
      %Example.Hat{
        inches: size.inches,
        color: Enum.random(@colors),
        name: Enum.random(@names)
      }
    end
  end
end

Separating the service and handler like this may seem a little odd but there are good reasons to do this. The most important is that it allows the service to be autogenerated again in the future. The second reason is that it allows us to easily mock service implementations for testing.

Running the server

To serve traffic Twirp provides a Plug. We use this plug to attach our service definition with our handler.

defmodule Example.Router do
  use Plug.Router

  plug Twirp.Plug, service: Example.HaberdasherService, handler: Example.HaberdasherHandler
end
defmodule Example.Application do
  use Application

  def start(_type, _args) do
    children = [
      Plug.Cowboy.child_spec(scheme: :http, plug: Example.Router, options: [port: 4040]),
    ]

    opts = [strategy: :one_for_one, name: Example.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

If you start your application your plug will now be available on port 4040.

Using the client

Client definitions are generated alongside the service definition. This allows you to generate clients for your services in other applications. You can make RPC calls like so:

defmodule AnotherService.GetHats do
  alias Example.HaberdasherClient, as: Client
  alias Example.{Size, Hat}

  def make_a_hat(inches) do
    case Client.make_hat(Size.new(inches: inches)) do
      {:ok, %Hat{}=hat} ->
        hat

      {:error, %Twirp.Error{msg: msg}} ->
        Logger.error(msg)
    end
  end
end

Running with Phoenix

The plug can also be attached within a Phoenix Router. Example below would be accessible at /rpc/hat

URL: /rpc/hat/twirp/example.Haberdasher/MakeHat or /{prefix?}/twirp/{package}.{service}/{method}

defmodule ExampleWeb.Router do
  use ExampleWeb, :router

  scope "/rpc" do
    forward "/hat", Twirp.Plug,
      service: Example.HaberdasherService, handler: Example.HaberdasherHandler
  end
end

Client adapters

Twirp supports either Finch or Hackney as the underlying http client. Finch is the default. If you want to configure the adapter you can pass the adapter option when you call start_link.

Client.start_link(
  url: "https://some.url",
  adapter: {Twirp.Client.Hackney, [pool_opts: [timeout: 30_000, max_connections: 100]]}
)

Should I use this?

This implementation is still early but I believe that it should be ready for use. One of the benefits of twirp is that the implementation is quite easy to understand. At the moment there's only about ~600 LOC in the entire lib directory so if something goes wrong it shouldn't be hard to look through it and understand the issue.

More Repositories

1

pointfree.io

A web site for converting haskell code into pointfree haskell code
HTML
155
star
2

distsys_training

Distributed Systems Training with Elixir
Elixir
132
star
3

webpack-react-skeleton

Quickly build a React app with Webpack
JavaScript
52
star
4

oath

Design by contract in elixir
Elixir
45
star
5

fawkes

Chatbot framework
Elixir
43
star
6

breaking_mnesia

Elixir
17
star
7

ecto_isolation

Elixir
15
star
8

dotfiles

I ❤️ dotfiles
Shell
14
star
9

drax

CRDTs for elixir.
Elixir
13
star
10

phoenix_webpack

A webpack generator for phoenix
Elixir
13
star
11

jenga

Example elixir application for building systems that handle failure.
Elixir
11
star
12

norm2

Elixir
10
star
13

url-shortener

Elixir
9
star
14

chatops_rpc

Elixir implementation of chatops-rpc protocol
Elixir
9
star
15

orwell

Kafka lag monitoring and alerting
Elixir
9
star
16

weirding

Random Text Generator
Elixir
7
star
17

conflicted

Elixir
6
star
18

soft_delete

Elixir
5
star
19

aoc2020

Elixir
5
star
20

nyan-cat

NyanCat all of the things
Ruby
4
star
21

sync_dispatch

Elixir
4
star
22

aoc_2018

Elixir
3
star
23

butler_cage

A butler plugin for Nick Cage photos
Elixir
3
star
24

dist_sys_training

Elixir, Distributed Systems Training
3
star
25

6.824

Homework and Labs for MIT 6.824
Go
2
star
26

butler_tableflip

Flipping tables with butler
Elixir
2
star
27

aoc2021

Nim
2
star
28

chattdevs

the chattanooga directory listing
Ruby
2
star
29

sprawl

Testing out phoenix
Elixir
1
star
30

telling-stories-with-d3

A talk I gave that demonstrates how to tell stories using data visuals and d3
JavaScript
1
star
31

pool_test

Elixir
1
star
32

component_design

A talk on designing webapps
HTML
1
star
33

conference-talks

JavaScript
1
star
34

sahara

Elixir
1
star
35

lunch-stats

Stats on lunch meetups
JavaScript
1
star
36

codestock_speakers

Codestock speakers by year
1
star
37

advent_of_code_2019

Elixir
1
star
38

cis194

Homework for cis194
Haskell
1
star
39

form_test

potential phx-live-view bug
Elixir
1
star
40

2014-chadev-info

d3 visualization for chadev lunches
JavaScript
1
star
41

eproxy

A proxy server written in elixir
Elixir
1
star
42

songocracy

A way to democratize music in the workplace
Ruby
1
star