diff --git a/.ci/build-plt-cache.sh b/.ci/build-plt-cache.sh deleted file mode 100755 index 53842593..00000000 --- a/.ci/build-plt-cache.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# This script builds/restores PLT cache files to/from a cache at ~/.pltcache -set -ex - -# "Mix will default to the :dev environment" -MIX_ENV="${MIX_ENV:-dev}" - -mix compile - -# Create the PLT cache directory, if it doesn't already exist -mkdir -p "$HOME"/.pltcache -# Copy the PLT files into the _build directory, if they exist -cp "$HOME"/.pltcache/*-"$MIX_ENV".plt _build/"$MIX_ENV"/ || true - -# Build the PLT cache (uses the existing one if present) -mix dialyzer --plt - -# Copy the PLT files into the cache so they can be used next time -cp _build/"$MIX_ENV"/*-"$MIX_ENV".plt "$HOME"/.pltcache/ diff --git a/.formatter.exs b/.formatter.exs index 26b1f145..6ee67280 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,9 @@ - [ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "examples/*/{config,lib,priv}/*.ex"], + inputs: [ + "{mix,.formatter}.exs", + "{config,lib,test}/**/*.{ex,exs}", + "examples/*/{config,lib,priv}/*.ex" + ], import_deps: [:protobuf], locals_without_parens: [rpc: 3, intercept: 1, intercept: 2, run: 1, run: 2], export: [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 968b7166..0760d9db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: branches: - - '**' + - "**" push: branches: - master @@ -12,15 +12,20 @@ jobs: check_format: runs-on: ubuntu-latest name: Check format - container: - image: elixir:1.9-slim steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: 24 + elixir-version: 1.13.1 + - name: Retrieve dependencies cache + uses: actions/cache@v3 + id: mix-cache # id to use in retrieve action + with: + path: deps + key: v1-${{ matrix.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get + run: mix deps.get 1>/dev/null - name: Check format run: mix format --check-formatted @@ -29,57 +34,137 @@ jobs: name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: - otp: [20.x, 21.x, 22.x] - elixir: [1.7.x, 1.8.x, 1.9.x, 1.10.x] + otp: [22.x, 23.x, 24.x, 25.x] + elixir: [1.11.x, 1.12.x, 1.13.x] exclude: - - otp: 20.x - elixir: 1.10.x + - otp: 25.x + elixir: 1.11.x + - otp: 25.x + elixir: 1.12.x needs: check_format steps: - - uses: actions/checkout@v1 - - uses: actions/setup-elixir@v1.2.0 - with: - otp-version: ${{matrix.otp}} - elixir-version: ${{matrix.elixir}} - - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get - - name: Run Tests - run: mix test + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + - name: Retrieve dependencies cache + uses: actions/cache@v3 + id: mix-cache # id to use in retrieve action + with: + path: deps + key: v1-${{ matrix.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + - name: Install Dependencies + run: mix deps.get 1>/dev/null + - name: Run Tests + run: mix test interop-tests: runs-on: ubuntu-latest name: Interop tests - container: - image: elixir:1.9-slim needs: check_format + if: ${{ github.ref != 'refs/heads/master' }} + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: 25.x + elixir-version: 1.13.x + - name: Retrieve dependencies cache + uses: actions/cache@v3 + id: mix-cache # id to use in retrieve action + with: + path: deps + key: v1-${{ matrix.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + - name: Install Dependencies + run: mix deps.get 1>/dev/null + working-directory: ./interop + - name: Run interop tests + run: mix run script/run.exs --rounds 64 + working-directory: ./interop + + interop-tests-all: + runs-on: ubuntu-latest + name: Interop tests OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + needs: check_format + if: ${{ github.ref == 'refs/heads/master' }} + strategy: + matrix: + otp: [22.x, 23.x, 24.x, 25.x] + elixir: [1.11.x, 1.12.x, 1.13.x] + exclude: + - otp: 25.x + elixir: 1.11.x + - otp: 25.x + elixir: 1.12.x steps: - - uses: actions/checkout@v1 - - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get - working-directory: ./interop - - name: Run interop tests - run: mix run script/run.exs - working-directory: ./interop + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: 25.x + elixir-version: 1.13.x + - name: Retrieve dependencies cache + uses: actions/cache@v3 + id: mix-cache # id to use in retrieve action + with: + path: deps + key: v1-${{ matrix.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + - name: Install Dependencies + run: mix deps.get 1>/dev/null + working-directory: ./interop + - name: Run interop tests + run: mix run script/run.exs --rounds 64 + working-directory: ./interop + + dialyzer: + name: Dialyzer + runs-on: ubuntu-latest + strategy: + matrix: + otp: [24.x, 25.x] + elixir: [1.13.x] + env: + MIX_ENV: test + steps: + - uses: actions/checkout@v3 + - id: set_vars + run: | + mix_hash="${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}" + echo "::set-output name=mix_hash::$mix_hash" + - id: cache-plt + uses: actions/cache@v3 + with: + path: | + _build/test/plts/dialyzer.plt + _build/test/plts/dialyzer.plt.hash + key: plt-cache-${{ matrix.otp }}-${{ matrix.elixir }}-${{ steps.set_vars.outputs.mix_hash }} + restore-keys: | + plt-cache-${{ matrix.otp }}-${{ matrix.elixir }}- + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + - run: mix deps.get 1>/dev/null + - run: mix dialyzer --format short check_release: runs-on: ubuntu-latest name: Check release needs: check_format - container: - image: elixir:1.9-slim steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: 24 + elixir-version: 1.13.1 + - name: Retrieve dependencies cache + uses: actions/cache@v3 + id: mix-cache # id to use in retrieve action + with: + path: deps + key: v1-${{ matrix.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get + run: mix deps.get 1>/dev/null - name: Build hex run: mix hex.build - name: Generate docs diff --git a/.github/workflows/cron_ci.yml b/.github/workflows/cron_ci.yml deleted file mode 100644 index beebc9a4..00000000 --- a/.github/workflows/cron_ci.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Cron CI - -on: - schedule: - - cron: '0 0 * * 0' - -jobs: - interop-tests: - runs-on: ubuntu-latest - name: Interop tests - container: - image: elixir:1.9 - steps: - - uses: actions/checkout@v1 - - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get - working-directory: ./interop - - name: Run Cron CI - run: make ci-cron diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..d905c6c8 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.13.3-otp-25 +erlang 25.0.3 diff --git a/Makefile b/Makefile index efc78c46..33479e25 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,4 @@ test-all: mix test cd interop && mix run script/run.exs -# This is heavy -ci-cron: - cd interop && mix deps.get && mix run script/run.exs --rounds 1000 --concurrency 30 && cd - - mix deps.get && bash .ci/build-plt-cache.sh && mix dialyzer - - .PHONY: test release test-prepare test-all ci-cron diff --git a/README.md b/README.md index 8f1754b6..f01146fb 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ # gRPC Elixir [![Hex.pm](https://img.shields.io/hexpm/v/grpc.svg)](https://hex.pm/packages/grpc) -[![Travis Status](https://travis-ci.org/elixir-grpc/grpc.svg?branch=master)](https://travis-ci.org/elixir-grpc/grpc) +[![Travis Status](https://app.travis-ci.com/elixir-grpc/grpc.svg?branch=master)](https://app.travis-ci.com/elixir-grpc/grpc) [![GitHub actions Status](https://github.com/elixir-grpc/grpc/workflows/CI/badge.svg)](https://github.com/elixir-grpc/grpc/actions) -[![Inline docs](http://inch-ci.org/github/elixir-grpc/grpc.svg?branch=master)](http://inch-ci.org/github/elixir-grpc/grpc) +[![License](https://img.shields.io/hexpm/l/grpc.svg)](https://github.com/elixir-grpc/grpc/blob/master/LICENSE.md) +[![Last Updated](https://img.shields.io/github/last-commit/elixir-grpc/grpc.svg)](https://github.com/elixir-grpc/grpc/commits/master) +[![Total Download](https://img.shields.io/hexpm/dt/grpc.svg)](https://hex.pm/packages/elixir-grpc/grpc) An Elixir implementation of [gRPC](http://www.grpc.io/). -**WARNING: Be careful to use it in production! Test and benchmark in advance.** +## Table of contents -**NOTICE: Erlang/OTP needs >= 20.3.2** +- [Notice](#notice) +- [Installation](#installation) +- [Usage](#usage) +- [Features](#features) +- [Benchmark](#benchmark) +- [Contributing](#contributing) -**NOTICE: grpc_gun** - -Now `{:gun, "~> 2.0.0", hex: :grpc_gun}` is used in mix.exs because grpc depnds on Gun 2.0, -but its stable version is not released. So I published a [2.0 version on hex](https://hex.pm/packages/grpc_gun) -with a different name. So if you have other dependencies who depends on Gun, you need to use -override: `{:gun, "~> 2.0.0", hex: :grpc_gun, override: true}`. Let's wait for this issue -https://github.com/ninenines/gun/issues/229. +## Notice +> __Note__ +> The [Gun](https://github.com/ninenines/gun) library doesn't have a full 2.0 release yet, so we depend on `:grcp_gun 2.0.1` for now. +This is the same as `:gun 2.0.0-rc.2`, but [Hex](https://hex.pm/) doesn't let us depend on RC versions for releases. ## Installation @@ -26,9 +30,11 @@ The package can be installed as: ```elixir def deps do [ - {:grpc, github: "elixir-grpc/grpc"}, - # 2.9.0 fixes some important bugs, so it's better to use ~> 2.9.0 - {:cowlib, "~> 2.9.0", override: true} + {:grpc, "~> 0.5.0"}, + # We don't force protobuf as a dependency for more + # flexibility on which protobuf library is used, + # but you probably want to use it as well + {:protobuf, "~> 0.10"} ] end ``` @@ -36,7 +42,9 @@ The package can be installed as: ## Usage 1. Generate Elixir code from proto file as [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) shows(especially the `gRPC Support` section). + 2. Implement the server side code like below and remember to return the expected message types. + ```elixir defmodule Helloworld.Greeter.Server do use GRPC.Server, service: Helloworld.Greeter.Service @@ -67,7 +75,7 @@ defmodule HelloworldApp do def start(_type, _args) do children = [ # ... - supervisor(GRPC.Server.Supervisor, [{Helloworld.Endpoint, 50051}]) + {GRPC.Server.Supervisor, endpoint: Helloworld.Endpoint, port: 50051} ] opts = [strategy: :one_for_one, name: YourApp] @@ -76,25 +84,8 @@ defmodule HelloworldApp do end ``` -Then start it when starting your application: - -```elixir -# config.exs -config :grpc, start_server: true - -# test.exs -config :grpc, start_server: false - -$ iex -S mix -``` - -or run grpc.server using a mix task - -``` -$ mix grpc.server -``` - 4. Call rpc: + ```elixir iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051") iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir") @@ -107,24 +98,18 @@ iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.L Check [examples](examples) and [interop](interop)(Interoperability Test) for some examples. -## TODO - -- [x] Unary RPC -- [x] Server streaming RPC -- [x] Client streaming RPC -- [x] Bidirectional streaming RPC -- [x] Helloworld and RouteGuide examples -- [x] Doc and more tests -- [x] Authentication with TLS -- [x] Improve code generation from protos ([protobuf-elixir](https://github.com/tony612/protobuf-elixir) [#8](https://github.com/elixir-grpc/grpc/issues/8)) -- [x] Timeout for unary calls -- [x] Errors handling -- [x] Benchmarking -- [x] Logging -- [x] Interceptors(See `GRPC.Endpoint`) -- [x] [Connection Backoff](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md) -- [x] Data compression -- [x] Support other encoding(other than protobuf) +## Features + +- Various kinds of RPC: + - [Unary](https://grpc.io/docs/what-is-grpc/core-concepts/#unary-rpc) + - [Server-streaming](https://grpc.io/docs/what-is-grpc/core-concepts/#server-streaming-rpc) + - [Client-streaming](https://grpc.io/docs/what-is-grpc/core-concepts/#client-streaming-rpc) + - [Bidirectional-streaming](https://grpc.io/docs/what-is-grpc/core-concepts/#bidirectional-streaming-rpc) +- [TLS Authentication](https://grpc.io/docs/guides/auth/#supported-auth-mechanisms) +- [Error handling](https://grpc.io/docs/guides/error/) +- Interceptors(See [`GRPC.Endpoint`](https://github.com/elixir-grpc/grpc/blob/master/lib/grpc/endpoint.ex)) +- [Connection Backoff](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md) +- Data compression ## Benchmark @@ -132,15 +117,9 @@ Check [examples](examples) and [interop](interop)(Interoperability Test) for som 2. [Benchmark](benchmark) followed by official spec -## Sponsors - -This project is being sponsored by [Tubi](https://tubitv.com/). Thank you! - - - ## Contributing -You contributions are welcome! +Your contributions are welcome! Please open issues if you have questions, problems and ideas. You can create pull requests directly if you want to fix little bugs, add small features and so on. diff --git a/benchmark/config/config.exs b/benchmark/config/config.exs index db5eb576..1c012859 100644 --- a/benchmark/config/config.exs +++ b/benchmark/config/config.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :logger, level: :info diff --git a/benchmark/config/dev.exs b/benchmark/config/dev.exs index d2d855e6..becde769 100644 --- a/benchmark/config/dev.exs +++ b/benchmark/config/dev.exs @@ -1 +1 @@ -use Mix.Config +import Config diff --git a/benchmark/config/prod.exs b/benchmark/config/prod.exs index 28c45b13..3da7d10f 100644 --- a/benchmark/config/prod.exs +++ b/benchmark/config/prod.exs @@ -1,3 +1,3 @@ -use Mix.Config +import Config config :logger, level: :warn diff --git a/benchmark/lib/grpc/core/stats.pb.ex b/benchmark/lib/grpc/core/stats.pb.ex index fcb7f196..0b106138 100644 --- a/benchmark/lib/grpc/core/stats.pb.ex +++ b/benchmark/lib/grpc/core/stats.pb.ex @@ -3,8 +3,8 @@ defmodule Grpc.Core.Bucket do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - start: float, - count: non_neg_integer + start: float(), + count: non_neg_integer() } defstruct [:start, :count] @@ -29,7 +29,7 @@ defmodule Grpc.Core.Metric do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - value: {atom, any}, + value: {atom(), any()}, name: String.t() } defstruct [:value, :name] diff --git a/benchmark/lib/grpc/testing/control.pb.ex b/benchmark/lib/grpc/testing/control.pb.ex index 6b73ab91..58121ce9 100644 --- a/benchmark/lib/grpc/testing/control.pb.ex +++ b/benchmark/lib/grpc/testing/control.pb.ex @@ -3,7 +3,7 @@ defmodule Grpc.Testing.PoissonParams do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - offered_load: float + offered_load: float() } defstruct [:offered_load] @@ -23,7 +23,7 @@ defmodule Grpc.Testing.LoadParams do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - load: {atom, any} + load: {atom(), any()} } defstruct [:load] @@ -37,7 +37,7 @@ defmodule Grpc.Testing.SecurityParams do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - use_test_ca: boolean, + use_test_ca: boolean(), server_host_override: String.t(), cred_type: String.t() } @@ -53,7 +53,7 @@ defmodule Grpc.Testing.ChannelArg do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - value: {atom, any}, + value: {atom(), any()}, name: String.t() } defstruct [:value, :name] @@ -70,22 +70,22 @@ defmodule Grpc.Testing.ClientConfig do @type t :: %__MODULE__{ server_targets: [String.t()], - client_type: atom | integer, + client_type: atom() | integer(), security_params: Grpc.Testing.SecurityParams.t() | nil, - outstanding_rpcs_per_channel: integer, - client_channels: integer, - async_client_threads: integer, - rpc_type: atom | integer, + outstanding_rpcs_per_channel: integer(), + client_channels: integer(), + async_client_threads: integer(), + rpc_type: atom() | integer(), load_params: Grpc.Testing.LoadParams.t() | nil, payload_config: Grpc.Testing.PayloadConfig.t() | nil, histogram_params: Grpc.Testing.HistogramParams.t() | nil, - core_list: [integer], - core_limit: integer, + core_list: [integer()], + core_limit: integer(), other_client_api: String.t(), channel_args: [Grpc.Testing.ChannelArg.t()], - threads_per_cq: integer, - messages_per_stream: integer, - use_coalesce_api: boolean + threads_per_cq: integer(), + messages_per_stream: integer(), + use_coalesce_api: boolean() } defstruct [ :server_targets, @@ -143,7 +143,7 @@ defmodule Grpc.Testing.Mark do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - reset: boolean + reset: boolean() } defstruct [:reset] @@ -155,7 +155,7 @@ defmodule Grpc.Testing.ClientArgs do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - argtype: {atom, any} + argtype: {atom(), any()} } defstruct [:argtype] @@ -169,16 +169,16 @@ defmodule Grpc.Testing.ServerConfig do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - server_type: atom | integer, + server_type: atom() | integer(), security_params: Grpc.Testing.SecurityParams.t() | nil, - port: integer, - async_server_threads: integer, - core_limit: integer, + port: integer(), + async_server_threads: integer(), + core_limit: integer(), payload_config: Grpc.Testing.PayloadConfig.t() | nil, - core_list: [integer], + core_list: [integer()], other_server_api: String.t(), - threads_per_cq: integer, - resource_quota_size: integer, + threads_per_cq: integer(), + resource_quota_size: integer(), channel_args: [Grpc.Testing.ChannelArg.t()] } defstruct [ @@ -213,7 +213,7 @@ defmodule Grpc.Testing.ServerArgs do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - argtype: {atom, any} + argtype: {atom(), any()} } defstruct [:argtype] @@ -228,8 +228,8 @@ defmodule Grpc.Testing.ServerStatus do @type t :: %__MODULE__{ stats: Grpc.Testing.ServerStats.t() | nil, - port: integer, - cores: integer + port: integer(), + cores: integer() } defstruct [:stats, :port, :cores] @@ -251,7 +251,7 @@ defmodule Grpc.Testing.CoreResponse do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - cores: integer + cores: integer() } defstruct [:cores] @@ -273,12 +273,12 @@ defmodule Grpc.Testing.Scenario do @type t :: %__MODULE__{ name: String.t(), client_config: Grpc.Testing.ClientConfig.t() | nil, - num_clients: integer, + num_clients: integer(), server_config: Grpc.Testing.ServerConfig.t() | nil, - num_servers: integer, - warmup_seconds: integer, - benchmark_seconds: integer, - spawn_local_worker_count: integer + num_servers: integer(), + warmup_seconds: integer(), + benchmark_seconds: integer(), + spawn_local_worker_count: integer() } defstruct [ :name, @@ -318,11 +318,11 @@ defmodule Grpc.Testing.ScenarioResultSummary do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - qps: float, - qps_per_server_core: float, - server_system_time: float, - server_user_time: float, - client_system_time: float, + qps: float(), + qps_per_server_core: float(), + server_system_time: float(), + server_user_time: float(), + client_system_time: float(), client_user_time: float, latency_50: float, latency_90: float, diff --git a/benchmark/lib/grpc/testing/messages.pb.ex b/benchmark/lib/grpc/testing/messages.pb.ex index 8c5021b3..fab6e82e 100644 --- a/benchmark/lib/grpc/testing/messages.pb.ex +++ b/benchmark/lib/grpc/testing/messages.pb.ex @@ -3,7 +3,7 @@ defmodule Grpc.Testing.BoolValue do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - value: boolean + value: boolean() } defstruct [:value] @@ -15,8 +15,8 @@ defmodule Grpc.Testing.Payload do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - type: atom | integer, - body: binary + type: atom() | integer(), + body: binary() } defstruct [:type, :body] @@ -29,7 +29,7 @@ defmodule Grpc.Testing.EchoStatus do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - code: integer, + code: integer(), message: String.t() } defstruct [:code, :message] @@ -43,11 +43,11 @@ defmodule Grpc.Testing.SimpleRequest do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - response_type: atom | integer, - response_size: integer, + response_type: atom() | integer(), + response_size: integer(), payload: Grpc.Testing.Payload.t() | nil, - fill_username: boolean, - fill_oauth_scope: boolean, + fill_username: boolean(), + fill_oauth_scope: boolean(), response_compressed: Grpc.Testing.BoolValue.t() | nil, response_status: Grpc.Testing.EchoStatus.t() | nil, expect_compressed: Grpc.Testing.BoolValue.t() | nil @@ -108,7 +108,7 @@ defmodule Grpc.Testing.StreamingInputCallResponse do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - aggregated_payload_size: integer + aggregated_payload_size: integer() } defstruct [:aggregated_payload_size] @@ -120,8 +120,8 @@ defmodule Grpc.Testing.ResponseParameters do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - size: integer, - interval_us: integer, + size: integer(), + interval_us: integer(), compressed: Grpc.Testing.BoolValue.t() | nil } defstruct [:size, :interval_us, :compressed] @@ -136,7 +136,7 @@ defmodule Grpc.Testing.StreamingOutputCallRequest do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - response_type: atom | integer, + response_type: atom() | integer(), response_parameters: [Grpc.Testing.ResponseParameters.t()], payload: Grpc.Testing.Payload.t() | nil, response_status: Grpc.Testing.EchoStatus.t() | nil @@ -166,7 +166,7 @@ defmodule Grpc.Testing.ReconnectParams do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - max_reconnect_backoff_ms: integer + max_reconnect_backoff_ms: integer() } defstruct [:max_reconnect_backoff_ms] @@ -178,8 +178,8 @@ defmodule Grpc.Testing.ReconnectInfo do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - passed: boolean, - backoff_ms: [integer] + passed: boolean(), + backoff_ms: [integer()] } defstruct [:passed, :backoff_ms] diff --git a/benchmark/lib/grpc/testing/payloads.pb.ex b/benchmark/lib/grpc/testing/payloads.pb.ex index 18a1a63e..713a88e0 100644 --- a/benchmark/lib/grpc/testing/payloads.pb.ex +++ b/benchmark/lib/grpc/testing/payloads.pb.ex @@ -3,8 +3,8 @@ defmodule Grpc.Testing.ByteBufferParams do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - req_size: integer, - resp_size: integer + req_size: integer(), + resp_size: integer() } defstruct [:req_size, :resp_size] @@ -17,8 +17,8 @@ defmodule Grpc.Testing.SimpleProtoParams do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - req_size: integer, - resp_size: integer + req_size: integer(), + resp_size: integer() } defstruct [:req_size, :resp_size] @@ -39,7 +39,7 @@ defmodule Grpc.Testing.PayloadConfig do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - payload: {atom, any} + payload: {atom(), any()} } defstruct [:payload] diff --git a/benchmark/lib/grpc/testing/stats.pb.ex b/benchmark/lib/grpc/testing/stats.pb.ex index 23418c6e..a8414038 100644 --- a/benchmark/lib/grpc/testing/stats.pb.ex +++ b/benchmark/lib/grpc/testing/stats.pb.ex @@ -3,12 +3,12 @@ defmodule Grpc.Testing.ServerStats do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - time_elapsed: float, - time_user: float, - time_system: float, - total_cpu_time: non_neg_integer, - idle_cpu_time: non_neg_integer, - cq_poll_count: non_neg_integer, + time_elapsed: float(), + time_user: float(), + time_system: float(), + total_cpu_time: non_neg_integer(), + idle_cpu_time: non_neg_integer(), + cq_poll_count: non_neg_integer(), core_stats: Grpc.Core.Stats.t() | nil } defstruct [ @@ -35,8 +35,8 @@ defmodule Grpc.Testing.HistogramParams do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - resolution: float, - max_possible: float + resolution: float(), + max_possible: float() } defstruct [:resolution, :max_possible] @@ -49,12 +49,12 @@ defmodule Grpc.Testing.HistogramData do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - bucket: [non_neg_integer], - min_seen: float, - max_seen: float, - sum: float, - sum_of_squares: float, - count: float + bucket: [non_neg_integer()], + min_seen: float(), + max_seen: float(), + sum: float(), + sum_of_squares: float(), + count: float() } defstruct [:bucket, :min_seen, :max_seen, :sum, :sum_of_squares, :count] @@ -71,8 +71,8 @@ defmodule Grpc.Testing.RequestResultCount do use Protobuf, syntax: :proto3 @type t :: %__MODULE__{ - status_code: integer, - count: integer + status_code: integer(), + count: integer() } defstruct [:status_code, :count] @@ -86,11 +86,11 @@ defmodule Grpc.Testing.ClientStats do @type t :: %__MODULE__{ latencies: Grpc.Testing.HistogramData.t() | nil, - time_elapsed: float, - time_user: float, - time_system: float, + time_elapsed: float(), + time_user: float(), + time_system: float(), request_results: [Grpc.Testing.RequestResultCount.t()], - cq_poll_count: non_neg_integer, + cq_poll_count: non_neg_integer(), core_stats: Grpc.Core.Stats.t() | nil } defstruct [ diff --git a/benchmark/mix.exs b/benchmark/mix.exs index 9ed822e3..ba3e4223 100644 --- a/benchmark/mix.exs +++ b/benchmark/mix.exs @@ -22,7 +22,7 @@ defmodule Benchmark.MixProject do defp deps do [ {:grpc, path: ".."}, - {:protobuf, github: "tony612/protobuf-elixir", override: true} + {:protobuf, "~> 0.10"} ] end end diff --git a/benchmark/mix.lock b/benchmark/mix.lock index 32da1efe..23a514a7 100644 --- a/benchmark/mix.lock +++ b/benchmark/mix.lock @@ -1,7 +1,7 @@ %{ - "cowboy": {:git, "https://github.com/elixir-grpc/cowboy.git", "db1b09fb06038415e5c643282554c0b9f8e6a976", [tag: "grpc-2.6.3"]}, - "cowlib": {:git, "https://github.com/elixir-grpc/cowlib.git", "1cc32e27d917bfe615da6957006fd9f8d6e604bd", [tag: "grpc-2.7.3"]}, - "gun": {:git, "https://github.com/elixir-grpc/gun.git", "975177bd5c179c800c8685cf64376689642ac972", [tag: "grpc-1.3.2"]}, - "protobuf": {:git, "https://github.com/tony612/protobuf-elixir.git", "384f97ca03aa4f874e527e9f28f5ebbae2f142f1", []}, - "ranch": {:git, "https://github.com/ninenines/ranch", "3190aef88aea04d6dce8545fe9b4574288903f44", [ref: "1.7.1"]}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "gun": {:hex, :gun, "2.0.0-rc.2", "7c489a32dedccb77b6e82d1f3c5a7dadfbfa004ec14e322cdb5e579c438632d2", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "6b9d1eae146410d727140dbf8b404b9631302ecc2066d1d12f22097ad7d254fc"}, + "protobuf": {:hex, :protobuf, "0.10.0", "4e8e3cf64c5be203b329f88bb8b916cb8d00fb3a12b2ac1f545463ae963c869f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4ae21a386142357aa3d31ccf5f7d290f03f3fa6f209755f6e87fc2c58c147893"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, } diff --git a/config/config.exs b/config/config.exs index 01274c30..773875cb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,3 +1,7 @@ -use Mix.Config +import Config -config :grpc, http2_client_adapter: GRPC.Adapter.Gun +config_file = Path.expand("#{config_env()}.exs", __DIR__) + +if File.exists?(config_file) do + import_config config_file +end diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 00000000..477c9079 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,3 @@ +import Config + +config :logger, level: :info diff --git a/examples/helloworld/.gitignore b/examples/helloworld/.gitignore index 62deea56..06dbcb6f 100644 --- a/examples/helloworld/.gitignore +++ b/examples/helloworld/.gitignore @@ -20,4 +20,4 @@ erl_crash.dump /src/grpc_c /tmp -/log +/log \ No newline at end of file diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md index 8eaa2c26..924681fd 100644 --- a/examples/helloworld/README.md +++ b/examples/helloworld/README.md @@ -9,7 +9,7 @@ $ mix do deps.get, compile 2. Run the server ```shell -$ mix grpc.server +$ mix run --no-halt ``` 3. Run the client script @@ -34,11 +34,7 @@ Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for ## How to start server when starting your application? -Change the config to: - -```elixir -config :grpc, start_server: true -``` +Pass `start_server: true` as an option for the `GRPC.Server.Supervisor` in your supervision tree. ## Benchmark diff --git a/examples/helloworld/config/config.exs b/examples/helloworld/config/config.exs index c2f683e9..9def7c2c 100644 --- a/examples/helloworld/config/config.exs +++ b/examples/helloworld/config/config.exs @@ -1,6 +1,3 @@ -use Mix.Config - -# Start server in OTP -# config :grpc, start_server: true +import Config import_config "#{Mix.env}.exs" diff --git a/examples/helloworld/config/dev.exs b/examples/helloworld/config/dev.exs index d2d855e6..becde769 100644 --- a/examples/helloworld/config/dev.exs +++ b/examples/helloworld/config/dev.exs @@ -1 +1 @@ -use Mix.Config +import Config diff --git a/examples/helloworld/config/prod.exs b/examples/helloworld/config/prod.exs index 9d6d504a..2dd33c31 100644 --- a/examples/helloworld/config/prod.exs +++ b/examples/helloworld/config/prod.exs @@ -1,6 +1,4 @@ -use Mix.Config - -config :grpc, start_server: true +import Config config :logger, level: :warn diff --git a/examples/helloworld/config/test.exs b/examples/helloworld/config/test.exs new file mode 100644 index 00000000..becde769 --- /dev/null +++ b/examples/helloworld/config/test.exs @@ -0,0 +1 @@ +import Config diff --git a/examples/helloworld/lib/helloworld.pb.ex b/examples/helloworld/lib/helloworld.pb.ex index 7c65811d..a8ff6dfa 100644 --- a/examples/helloworld/lib/helloworld.pb.ex +++ b/examples/helloworld/lib/helloworld.pb.ex @@ -1,31 +1,26 @@ defmodule Helloworld.HelloRequest do - use Protobuf + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - name: String.t() - } - defstruct [:name] - - field :name, 1, optional: true, type: :string + field :name, 1, type: :string end defmodule Helloworld.HelloReply do - use Protobuf - - @type t :: %__MODULE__{ - message: String.t() - } - defstruct [:message] + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :message, 1, optional: true, type: :string + field :message, 1, type: :string + field :today, 2, type: Google.Protobuf.Timestamp end defmodule Helloworld.Greeter.Service do - use GRPC.Service, name: "helloworld.Greeter" + @moduledoc false + use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.10.0" rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply end defmodule Helloworld.Greeter.Stub do + @moduledoc false use GRPC.Stub, service: Helloworld.Greeter.Service end diff --git a/examples/helloworld/lib/helloworld_app.ex b/examples/helloworld/lib/helloworld_app.ex index 93500609..d84d62a5 100644 --- a/examples/helloworld/lib/helloworld_app.ex +++ b/examples/helloworld/lib/helloworld_app.ex @@ -2,10 +2,8 @@ defmodule HelloworldApp do use Application def start(_type, _args) do - import Supervisor.Spec - children = [ - supervisor(GRPC.Server.Supervisor, [{Helloworld.Endpoint, 50051}]) + {GRPC.Server.Supervisor, endpoint: Helloworld.Endpoint, port: 50051, start_server: true} ] opts = [strategy: :one_for_one, name: HelloworldApp] diff --git a/examples/helloworld/lib/server.ex b/examples/helloworld/lib/server.ex index a82535d6..b85241f8 100644 --- a/examples/helloworld/lib/server.ex +++ b/examples/helloworld/lib/server.ex @@ -4,6 +4,13 @@ defmodule Helloworld.Greeter.Server do @spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) :: Helloworld.HelloReply.t() def say_hello(request, _stream) do - Helloworld.HelloReply.new(message: "Hello #{request.name}") + nanos_epoch = System.system_time() |> System.convert_time_unit(:native, :nanosecond) + seconds = div(nanos_epoch, 1_000_000_000) + nanos = nanos_epoch - seconds * 1_000_000_000 + + Helloworld.HelloReply.new( + message: "Hello #{request.name}", + today: %Google.Protobuf.Timestamp{seconds: seconds, nanos: nanos} + ) end end diff --git a/examples/helloworld/mix.exs b/examples/helloworld/mix.exs index ff2c94c8..9bf4cc3e 100644 --- a/examples/helloworld/mix.exs +++ b/examples/helloworld/mix.exs @@ -2,25 +2,26 @@ defmodule Helloworld.Mixfile do use Mix.Project def project do - [app: :helloworld, - version: "0.1.0", - elixir: "~> 1.4", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps()] + [ + app: :helloworld, + version: "0.1.0", + elixir: "~> 1.4", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps() + ] end def application do - [mod: {HelloworldApp, []}, - applications: [:logger, :grpc]] + [mod: {HelloworldApp, []}, applications: [:logger, :grpc]] end defp deps do [ {:grpc, path: "../../"}, - {:protobuf, github: "tony612/protobuf-elixir", override: true}, - {:cowlib, "~> 2.8.0", hex: :grpc_cowlib, override: true}, - {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, + {:protobuf, "~> 0.10"}, + {:google_protos, "~> 0.3.0"}, + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false} ] end end diff --git a/examples/helloworld/mix.lock b/examples/helloworld/mix.lock index 010aed56..f96a70d1 100644 --- a/examples/helloworld/mix.lock +++ b/examples/helloworld/mix.lock @@ -1,8 +1,10 @@ %{ - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, - "cowlib": {:hex, :grpc_cowlib, "2.8.1", "ddaf77f3b89bd8e6c76df67b28a4b069688eef91c0c497a246cf9bfcdf87f7d3", [:rebar3], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, - "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm"}, - "protobuf": {:git, "https://github.com/tony612/protobuf-elixir.git", "384f97ca03aa4f874e527e9f28f5ebbae2f142f1", []}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"}, + "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, + "protobuf": {:hex, :protobuf, "0.10.0", "4e8e3cf64c5be203b329f88bb8b916cb8d00fb3a12b2ac1f545463ae963c869f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4ae21a386142357aa3d31ccf5f7d290f03f3fa6f209755f6e87fc2c58c147893"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, } diff --git a/examples/helloworld/priv/client.exs b/examples/helloworld/priv/client.exs index cfb8560e..dc6bea5d 100644 --- a/examples/helloworld/priv/client.exs +++ b/examples/helloworld/priv/client.exs @@ -1,6 +1,9 @@ {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) {:ok, reply} = - channel |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir")) + channel + |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir")) + +# pass tuple `timeout: :infinity` as a second arg to stay in IEx debugging IO.inspect(reply) diff --git a/examples/helloworld/priv/protos/helloworld.proto b/examples/helloworld/priv/protos/helloworld.proto index 688974b2..12849981 100644 --- a/examples/helloworld/priv/protos/helloworld.proto +++ b/examples/helloworld/priv/protos/helloworld.proto @@ -5,6 +5,8 @@ option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; option objc_class_prefix = "HLW"; +import "google/protobuf/timestamp.proto"; + package helloworld; // The greeting service definition. @@ -21,4 +23,5 @@ message HelloRequest { // The response message containing the greetings message HelloReply { string message = 1; + google.protobuf.Timestamp today = 2; } diff --git a/examples/helloworld/test/hello_world_test.exs b/examples/helloworld/test/hello_world_test.exs new file mode 100644 index 00000000..962d07ac --- /dev/null +++ b/examples/helloworld/test/hello_world_test.exs @@ -0,0 +1,16 @@ +defmodule HelloworldTest do + @moduledoc false + + use ExUnit.Case + + setup_all do + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) + [channel: channel] + end + + test "helloworld should be successful", %{channel: channel} do + req = Helloworld.HelloRequest.new(name: "grpc-elixir") + assert {:ok, %{message: msg, today: _}} = Helloworld.Greeter.Stub.say_hello(channel, req) + assert msg == "Hello grpc-elixir" + end +end diff --git a/examples/helloworld/test/test_helper.exs b/examples/helloworld/test/test_helper.exs new file mode 100644 index 00000000..869559e7 --- /dev/null +++ b/examples/helloworld/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/examples/route_guide/README.md b/examples/route_guide/README.md index 7ab2d1a7..5e9fa0df 100644 --- a/examples/route_guide/README.md +++ b/examples/route_guide/README.md @@ -9,7 +9,7 @@ $ mix do deps.get, compile 2. Run the server ``` -$ mix grpc.server +$ mix run --no-halt ``` 2. Run the client @@ -35,7 +35,7 @@ Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for ## Authentication ``` -$ TLS=true mix grpc.server +$ TLS=true mix run --no-halt $ TLS=true mix run priv/client.exs ``` @@ -44,10 +44,3 @@ $ TLS=true mix run priv/client.exs * How to change log level? Check out `config/config.exs`, default to warn * Use local grpc-elixir? Uncomment `{:grpc, path: "../../"}` in `mix.exs` * Why is output format of `Feature` & `Point` different from normal map? Check out `lib/inspect.ex` -* How to start server when starting your application? - - Change the config to: - - ```elixir - config :grpc, start_server: true - ``` diff --git a/examples/route_guide/config/config.exs b/examples/route_guide/config/config.exs deleted file mode 100644 index f346b5d2..00000000 --- a/examples/route_guide/config/config.exs +++ /dev/null @@ -1,3 +0,0 @@ -use Mix.Config - -# config :grpc, start_server: true diff --git a/examples/route_guide/lib/app.ex b/examples/route_guide/lib/app.ex index 5410d767..8cebbf31 100644 --- a/examples/route_guide/lib/app.ex +++ b/examples/route_guide/lib/app.ex @@ -5,11 +5,9 @@ defmodule Routeguide.App do @key_path Path.expand("./tls/server1.key", :code.priv_dir(:route_guide)) def start(_type, _args) do - import Supervisor.Spec - children = [ - supervisor(RouteGuide.Data, []), - supervisor(GRPC.Server.Supervisor, [start_args()]) + RouteGuide.Data, + {GRPC.Server.Supervisor, start_args()} ] opts = [strategy: :one_for_one, name: Routeguide] @@ -17,12 +15,13 @@ defmodule Routeguide.App do end defp start_args do + opts = [endpoint: Routeguide.Endpoint, port: 10000, start_server: true] + if System.get_env("TLS") do cred = GRPC.Credential.new(ssl: [certfile: @cert_path, keyfile: @key_path]) - IO.inspect(cred) - {Routeguide.Endpoint, 10000, cred: cred} + Keyword.put(opts, :cred, cred) else - {Routeguide.Endpoint, 10000} + opts end end end diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex index 150a751b..5d33f4ba 100644 --- a/examples/route_guide/lib/client.ex +++ b/examples/route_guide/lib/client.ex @@ -89,9 +89,7 @@ defmodule RouteGuide.Client do Enum.each(result_enum, fn {:ok, note} -> IO.puts( - "Got message #{note.message} at point(#{note.location.latitude}, #{ - note.location.longitude - })" + "Got message #{note.message} at point(#{note.location.latitude}, #{note.location.longitude})" ) end) end diff --git a/examples/route_guide/lib/data.ex b/examples/route_guide/lib/data.ex index 11cf0343..7d1fe528 100644 --- a/examples/route_guide/lib/data.ex +++ b/examples/route_guide/lib/data.ex @@ -1,7 +1,9 @@ defmodule RouteGuide.Data do + use Agent + @json_path Path.expand("../priv/route_guide_db.json", __DIR__) - def start_link do + def start_link(_) do features = load_features() Agent.start_link(fn -> %{features: features, notes: %{}} end, name: __MODULE__) end @@ -20,13 +22,13 @@ defmodule RouteGuide.Data do defp load_features(path \\ @json_path) do data = File.read!(path) - items = Poison.Parser.parse!(data) + items = Jason.decode!(data) - Enum.map(items, fn %{"location" => location, "name" => name} -> + for %{"location" => location, "name" => name} <- items do point = Routeguide.Point.new(latitude: location["latitude"], longitude: location["longitude"]) Routeguide.Feature.new(name: name, location: point) - end) + end end end diff --git a/examples/route_guide/lib/route_guide.pb.ex b/examples/route_guide/lib/route_guide.pb.ex index f4ce6742..66a64c91 100644 --- a/examples/route_guide/lib/route_guide.pb.ex +++ b/examples/route_guide/lib/route_guide.pb.ex @@ -1,81 +1,59 @@ defmodule Routeguide.Point do - use Protobuf + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - latitude: integer, - longitude: integer - } - defstruct [:latitude, :longitude] - - field :latitude, 1, optional: true, type: :int32 - field :longitude, 2, optional: true, type: :int32 + field :latitude, 1, type: :int32 + field :longitude, 2, type: :int32 end defmodule Routeguide.Rectangle do - use Protobuf - - @type t :: %__MODULE__{ - lo: Routeguide.Point.t(), - hi: Routeguide.Point.t() - } - defstruct [:lo, :hi] + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :lo, 1, optional: true, type: Routeguide.Point - field :hi, 2, optional: true, type: Routeguide.Point + field :lo, 1, type: Routeguide.Point + field :hi, 2, type: Routeguide.Point end defmodule Routeguide.Feature do - use Protobuf + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - name: String.t(), - location: Routeguide.Point.t() - } - defstruct [:name, :location] - - field :name, 1, optional: true, type: :string - field :location, 2, optional: true, type: Routeguide.Point + field :name, 1, type: :string + field :location, 2, type: Routeguide.Point end defmodule Routeguide.RouteNote do - use Protobuf - - @type t :: %__MODULE__{ - location: Routeguide.Point.t(), - message: String.t() - } - defstruct [:location, :message] + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :location, 1, optional: true, type: Routeguide.Point - field :message, 2, optional: true, type: :string + field :location, 1, type: Routeguide.Point + field :message, 2, type: :string end defmodule Routeguide.RouteSummary do - use Protobuf + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - point_count: integer, - feature_count: integer, - distance: integer, - elapsed_time: integer - } - defstruct [:point_count, :feature_count, :distance, :elapsed_time] - - field :point_count, 1, optional: true, type: :int32 - field :feature_count, 2, optional: true, type: :int32 - field :distance, 3, optional: true, type: :int32 - field :elapsed_time, 4, optional: true, type: :int32 + field :point_count, 1, type: :int32, json_name: "pointCount" + field :feature_count, 2, type: :int32, json_name: "featureCount" + field :distance, 3, type: :int32 + field :elapsed_time, 4, type: :int32, json_name: "elapsedTime" end defmodule Routeguide.RouteGuide.Service do - use GRPC.Service, name: "routeguide.RouteGuide" + @moduledoc false + use GRPC.Service, name: "routeguide.RouteGuide", protoc_gen_elixir_version: "0.10.0" rpc :GetFeature, Routeguide.Point, Routeguide.Feature + rpc :ListFeatures, Routeguide.Rectangle, stream(Routeguide.Feature) + rpc :RecordRoute, stream(Routeguide.Point), Routeguide.RouteSummary + rpc :RouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote) end defmodule Routeguide.RouteGuide.Stub do + @moduledoc false use GRPC.Stub, service: Routeguide.RouteGuide.Service end diff --git a/examples/route_guide/lib/server.ex b/examples/route_guide/lib/server.ex index 3cf7e70f..3e90b8bc 100644 --- a/examples/route_guide/lib/server.ex +++ b/examples/route_guide/lib/server.ex @@ -13,7 +13,7 @@ defmodule Routeguide.RouteGuide.Server do end) end - @spec list_features(Routeguide.Rectangle.t(), GRPC.Server.Stream.t()) :: any + @spec list_features(Routeguide.Rectangle.t(), GRPC.Server.Stream.t()) :: any() def list_features(rect, stream) do features = Data.fetch_features() @@ -45,7 +45,7 @@ defmodule Routeguide.RouteGuide.Server do ) end - @spec record_route(Enumerable.t(), GRPC.Server.Stream.t()) :: any + @spec record_route(Enumerable.t(), GRPC.Server.Stream.t()) :: any() def route_chat(req_enum, stream) do notes = Enum.reduce(req_enum, Data.fetch_notes(), fn note, notes -> diff --git a/examples/route_guide/mix.exs b/examples/route_guide/mix.exs index 812a09d8..045792a6 100644 --- a/examples/route_guide/mix.exs +++ b/examples/route_guide/mix.exs @@ -4,7 +4,7 @@ defmodule RouteGuide.Mixfile do def project do [app: :route_guide, version: "0.1.0", - elixir: "~> 1.3", + elixir: "~> 1.11", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, deps: deps()] @@ -15,7 +15,7 @@ defmodule RouteGuide.Mixfile do # Type "mix help compile.app" for more information def application do [mod: {Routeguide.App, []}, - applications: [:logger, :grpc, :poison]] + applications: [:logger, :grpc, :protobuf, :jason]] end # Dependencies can be Hex packages: @@ -30,9 +30,9 @@ defmodule RouteGuide.Mixfile do defp deps do [ {:grpc, path: "../../"}, - {:poison, "~> 3.0"}, - {:cowlib, "~> 2.8.0", hex: :grpc_cowlib, override: true}, - {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, + {:protobuf, "~> 0.10"}, + {:jason, "~> 1.2"}, + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, ] end end diff --git a/examples/route_guide/mix.lock b/examples/route_guide/mix.lock index 5205d247..3d14fba0 100644 --- a/examples/route_guide/mix.lock +++ b/examples/route_guide/mix.lock @@ -1,9 +1,10 @@ %{ - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, - "cowlib": {:hex, :grpc_cowlib, "2.8.1", "ddaf77f3b89bd8e6c76df67b28a4b069688eef91c0c497a246cf9bfcdf87f7d3", [:rebar3], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], []}, - "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, - "protobuf": {:hex, :protobuf, "0.5.0", "ec9857903f8c49cf01ad5f1340956e19ebe0ef05e225b15b442f33b13031ce08", [:mix], []}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "protobuf": {:hex, :protobuf, "0.10.0", "4e8e3cf64c5be203b329f88bb8b916cb8d00fb3a12b2ac1f545463ae963c869f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4ae21a386142357aa3d31ccf5f7d290f03f3fa6f209755f6e87fc2c58c147893"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, } diff --git a/examples/route_guide/priv/client.exs b/examples/route_guide/priv/client.exs index d3c3178e..79a33f0f 100644 --- a/examples/route_guide/priv/client.exs +++ b/examples/route_guide/priv/client.exs @@ -1,9 +1,10 @@ opts = [interceptors: [GRPC.Logger.Client]] + opts = if System.get_env("TLS") do ca_path = Path.expand("./tls/ca.pem", :code.priv_dir(:route_guide)) cred = GRPC.Credential.new(ssl: [cacertfile: ca_path]) - [{:cred, cred}|opts] + [{:cred, cred} | opts] else opts end diff --git a/examples/route_guide/priv/route_guide.proto b/examples/route_guide/priv/protos/route_guide.proto similarity index 100% rename from examples/route_guide/priv/route_guide.proto rename to examples/route_guide/priv/protos/route_guide.proto diff --git a/interop/config/config.exs b/interop/config/config.exs index 3fe942a9..5d2660ce 100644 --- a/interop/config/config.exs +++ b/interop/config/config.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :prometheus, GRPCPrometheus.ServerInterceptor, latency: :histogram @@ -6,7 +6,4 @@ config :prometheus, GRPCPrometheus.ServerInterceptor, config :prometheus, GRPCPrometheus.ClientInterceptor, latency: :histogram -# config :grpc, start_server: true - -# config :logger, level: :debug config :logger, level: :warn diff --git a/interop/lib/grpc_testing/empty.pb.ex b/interop/lib/grpc_testing/empty.pb.ex index 0208e266..77ee57fd 100644 --- a/interop/lib/grpc_testing/empty.pb.ex +++ b/interop/lib/grpc_testing/empty.pb.ex @@ -1,7 +1,4 @@ defmodule Grpc.Testing.Empty do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{} - defstruct [] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 end diff --git a/interop/lib/grpc_testing/messages.pb.ex b/interop/lib/grpc_testing/messages.pb.ex index aec33378..6a9d03e8 100644 --- a/interop/lib/grpc_testing/messages.pb.ex +++ b/interop/lib/grpc_testing/messages.pb.ex @@ -1,202 +1,283 @@ defmodule Grpc.Testing.PayloadType do @moduledoc false - use Protobuf, enum: true, syntax: :proto3 - - @type t :: integer | :COMPRESSABLE + use Protobuf, enum: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :COMPRESSABLE, 0 end +defmodule Grpc.Testing.GrpclbRouteType do + @moduledoc false + use Protobuf, enum: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 -defmodule Grpc.Testing.BoolValue do + field :GRPCLB_ROUTE_TYPE_UNKNOWN, 0 + field :GRPCLB_ROUTE_TYPE_FALLBACK, 1 + field :GRPCLB_ROUTE_TYPE_BACKEND, 2 +end +defmodule Grpc.Testing.ClientConfigureRequest.RpcType do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, enum: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - value: boolean - } - defstruct [:value] + field :EMPTY_CALL, 0 + field :UNARY_CALL, 1 +end +defmodule Grpc.Testing.BoolValue do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :value, 1, type: :bool end - defmodule Grpc.Testing.Payload do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - type: Grpc.Testing.PayloadType.t(), - body: binary - } - defstruct [:type, :body] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :type, 1, type: Grpc.Testing.PayloadType, enum: true field :body, 2, type: :bytes end - defmodule Grpc.Testing.EchoStatus do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - code: integer, - message: String.t() - } - defstruct [:code, :message] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :code, 1, type: :int32 field :message, 2, type: :string end - defmodule Grpc.Testing.SimpleRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - response_type: Grpc.Testing.PayloadType.t(), - response_size: integer, - payload: Grpc.Testing.Payload.t() | nil, - fill_username: boolean, - fill_oauth_scope: boolean, - response_compressed: Grpc.Testing.BoolValue.t() | nil, - response_status: Grpc.Testing.EchoStatus.t() | nil, - expect_compressed: Grpc.Testing.BoolValue.t() | nil, - fill_server_id: boolean - } - defstruct [ - :response_type, - :response_size, - :payload, - :fill_username, - :fill_oauth_scope, - :response_compressed, - :response_status, - :expect_compressed, - :fill_server_id - ] - - field :response_type, 1, type: Grpc.Testing.PayloadType, enum: true - field :response_size, 2, type: :int32 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :response_type, 1, type: Grpc.Testing.PayloadType, json_name: "responseType", enum: true + field :response_size, 2, type: :int32, json_name: "responseSize" field :payload, 3, type: Grpc.Testing.Payload - field :fill_username, 4, type: :bool - field :fill_oauth_scope, 5, type: :bool - field :response_compressed, 6, type: Grpc.Testing.BoolValue - field :response_status, 7, type: Grpc.Testing.EchoStatus - field :expect_compressed, 8, type: Grpc.Testing.BoolValue - field :fill_server_id, 9, type: :bool + field :fill_username, 4, type: :bool, json_name: "fillUsername" + field :fill_oauth_scope, 5, type: :bool, json_name: "fillOauthScope" + field :response_compressed, 6, type: Grpc.Testing.BoolValue, json_name: "responseCompressed" + field :response_status, 7, type: Grpc.Testing.EchoStatus, json_name: "responseStatus" + field :expect_compressed, 8, type: Grpc.Testing.BoolValue, json_name: "expectCompressed" + field :fill_server_id, 9, type: :bool, json_name: "fillServerId" + field :fill_grpclb_route_type, 10, type: :bool, json_name: "fillGrpclbRouteType" end - defmodule Grpc.Testing.SimpleResponse do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil, - username: String.t(), - oauth_scope: String.t(), - server_id: String.t() - } - defstruct [:payload, :username, :oauth_scope, :server_id] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload field :username, 2, type: :string - field :oauth_scope, 3, type: :string - field :server_id, 4, type: :string -end + field :oauth_scope, 3, type: :string, json_name: "oauthScope" + field :server_id, 4, type: :string, json_name: "serverId" + + field :grpclb_route_type, 5, + type: Grpc.Testing.GrpclbRouteType, + json_name: "grpclbRouteType", + enum: true + field :hostname, 6, type: :string +end defmodule Grpc.Testing.StreamingInputCallRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil, - expect_compressed: Grpc.Testing.BoolValue.t() | nil - } - defstruct [:payload, :expect_compressed] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload - field :expect_compressed, 2, type: Grpc.Testing.BoolValue + field :expect_compressed, 2, type: Grpc.Testing.BoolValue, json_name: "expectCompressed" end - defmodule Grpc.Testing.StreamingInputCallResponse do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - aggregated_payload_size: integer - } - defstruct [:aggregated_payload_size] - - field :aggregated_payload_size, 1, type: :int32 + field :aggregated_payload_size, 1, type: :int32, json_name: "aggregatedPayloadSize" end - defmodule Grpc.Testing.ResponseParameters do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - size: integer, - interval_us: integer, - compressed: Grpc.Testing.BoolValue.t() | nil - } - defstruct [:size, :interval_us, :compressed] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :size, 1, type: :int32 - field :interval_us, 2, type: :int32 + field :interval_us, 2, type: :int32, json_name: "intervalUs" field :compressed, 3, type: Grpc.Testing.BoolValue end - defmodule Grpc.Testing.StreamingOutputCallRequest do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :response_type, 1, type: Grpc.Testing.PayloadType, json_name: "responseType", enum: true - @type t :: %__MODULE__{ - response_type: Grpc.Testing.PayloadType.t(), - response_parameters: [Grpc.Testing.ResponseParameters.t()], - payload: Grpc.Testing.Payload.t() | nil, - response_status: Grpc.Testing.EchoStatus.t() | nil - } - defstruct [:response_type, :response_parameters, :payload, :response_status] + field :response_parameters, 2, + repeated: true, + type: Grpc.Testing.ResponseParameters, + json_name: "responseParameters" - field :response_type, 1, type: Grpc.Testing.PayloadType, enum: true - field :response_parameters, 2, repeated: true, type: Grpc.Testing.ResponseParameters field :payload, 3, type: Grpc.Testing.Payload - field :response_status, 7, type: Grpc.Testing.EchoStatus + field :response_status, 7, type: Grpc.Testing.EchoStatus, json_name: "responseStatus" end - defmodule Grpc.Testing.StreamingOutputCallResponse do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil - } - defstruct [:payload] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload end - defmodule Grpc.Testing.ReconnectParams do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - max_reconnect_backoff_ms: integer - } - defstruct [:max_reconnect_backoff_ms] + field :max_reconnect_backoff_ms, 1, type: :int32, json_name: "maxReconnectBackoffMs" +end +defmodule Grpc.Testing.ReconnectInfo do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :max_reconnect_backoff_ms, 1, type: :int32 + field :passed, 1, type: :bool + field :backoff_ms, 2, repeated: true, type: :int32, json_name: "backoffMs" end +defmodule Grpc.Testing.LoadBalancerStatsRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 -defmodule Grpc.Testing.ReconnectInfo do + field :num_rpcs, 1, type: :int32, json_name: "numRpcs" + field :timeout_sec, 2, type: :int32, json_name: "timeoutSec" +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer.RpcsByPeerEntry do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - passed: boolean, - backoff_ms: [integer] - } - defstruct [:passed, :backoff_ms] + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :passed, 1, type: :bool - field :backoff_ms, 2, repeated: true, type: :int32 + field :rpcs_by_peer, 1, + repeated: true, + type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer.RpcsByPeerEntry, + json_name: "rpcsByPeer", + map: true +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeerEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer +end +defmodule Grpc.Testing.LoadBalancerStatsResponse do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :rpcs_by_peer, 1, + repeated: true, + type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeerEntry, + json_name: "rpcsByPeer", + map: true + + field :num_failures, 2, type: :int32, json_name: "numFailures" + + field :rpcs_by_method, 3, + repeated: true, + type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByMethodEntry, + json_name: "rpcsByMethod", + map: true +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsStartedByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsSucceededByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsFailedByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats.ResultEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :int32 + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :rpcs_started, 1, type: :int32, json_name: "rpcsStarted" + + field :result, 2, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats.ResultEntry, + map: true +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.StatsPerMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :num_rpcs_started_by_method, 1, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsStartedByMethodEntry, + json_name: "numRpcsStartedByMethod", + map: true, + deprecated: true + + field :num_rpcs_succeeded_by_method, 2, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsSucceededByMethodEntry, + json_name: "numRpcsSucceededByMethod", + map: true, + deprecated: true + + field :num_rpcs_failed_by_method, 3, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsFailedByMethodEntry, + json_name: "numRpcsFailedByMethod", + map: true, + deprecated: true + + field :stats_per_method, 4, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.StatsPerMethodEntry, + json_name: "statsPerMethod", + map: true +end +defmodule Grpc.Testing.ClientConfigureRequest.Metadata do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :type, 1, type: Grpc.Testing.ClientConfigureRequest.RpcType, enum: true + field :key, 2, type: :string + field :value, 3, type: :string +end +defmodule Grpc.Testing.ClientConfigureRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :types, 1, repeated: true, type: Grpc.Testing.ClientConfigureRequest.RpcType, enum: true + field :metadata, 2, repeated: true, type: Grpc.Testing.ClientConfigureRequest.Metadata + field :timeout_sec, 3, type: :int32, json_name: "timeoutSec" +end +defmodule Grpc.Testing.ClientConfigureResponse do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 end diff --git a/interop/lib/grpc_testing/test.pb.ex b/interop/lib/grpc_testing/test.pb.ex index 963a45c5..a1a231cd 100644 --- a/interop/lib/grpc_testing/test.pb.ex +++ b/interop/lib/grpc_testing/test.pb.ex @@ -1,9 +1,11 @@ defmodule Grpc.Testing.TestService.Service do @moduledoc false - use GRPC.Service, name: "grpc.testing.TestService" + use GRPC.Service, name: "grpc.testing.TestService", protoc_gen_elixir_version: "0.10.0" rpc :EmptyCall, Grpc.Testing.Empty, Grpc.Testing.Empty + rpc :UnaryCall, Grpc.Testing.SimpleRequest, Grpc.Testing.SimpleResponse + rpc :CacheableUnaryCall, Grpc.Testing.SimpleRequest, Grpc.Testing.SimpleResponse rpc :StreamingOutputCall, @@ -29,10 +31,9 @@ defmodule Grpc.Testing.TestService.Stub do @moduledoc false use GRPC.Stub, service: Grpc.Testing.TestService.Service end - defmodule Grpc.Testing.UnimplementedService.Service do @moduledoc false - use GRPC.Service, name: "grpc.testing.UnimplementedService" + use GRPC.Service, name: "grpc.testing.UnimplementedService", protoc_gen_elixir_version: "0.10.0" rpc :UnimplementedCall, Grpc.Testing.Empty, Grpc.Testing.Empty end @@ -41,12 +42,12 @@ defmodule Grpc.Testing.UnimplementedService.Stub do @moduledoc false use GRPC.Stub, service: Grpc.Testing.UnimplementedService.Service end - defmodule Grpc.Testing.ReconnectService.Service do @moduledoc false - use GRPC.Service, name: "grpc.testing.ReconnectService" + use GRPC.Service, name: "grpc.testing.ReconnectService", protoc_gen_elixir_version: "0.10.0" rpc :Start, Grpc.Testing.ReconnectParams, Grpc.Testing.Empty + rpc :Stop, Grpc.Testing.Empty, Grpc.Testing.ReconnectInfo end @@ -54,3 +55,50 @@ defmodule Grpc.Testing.ReconnectService.Stub do @moduledoc false use GRPC.Stub, service: Grpc.Testing.ReconnectService.Service end +defmodule Grpc.Testing.LoadBalancerStatsService.Service do + @moduledoc false + use GRPC.Service, + name: "grpc.testing.LoadBalancerStatsService", + protoc_gen_elixir_version: "0.10.0" + + rpc :GetClientStats, + Grpc.Testing.LoadBalancerStatsRequest, + Grpc.Testing.LoadBalancerStatsResponse + + rpc :GetClientAccumulatedStats, + Grpc.Testing.LoadBalancerAccumulatedStatsRequest, + Grpc.Testing.LoadBalancerAccumulatedStatsResponse +end + +defmodule Grpc.Testing.LoadBalancerStatsService.Stub do + @moduledoc false + use GRPC.Stub, service: Grpc.Testing.LoadBalancerStatsService.Service +end +defmodule Grpc.Testing.XdsUpdateHealthService.Service do + @moduledoc false + use GRPC.Service, + name: "grpc.testing.XdsUpdateHealthService", + protoc_gen_elixir_version: "0.10.0" + + rpc :SetServing, Grpc.Testing.Empty, Grpc.Testing.Empty + + rpc :SetNotServing, Grpc.Testing.Empty, Grpc.Testing.Empty +end + +defmodule Grpc.Testing.XdsUpdateHealthService.Stub do + @moduledoc false + use GRPC.Stub, service: Grpc.Testing.XdsUpdateHealthService.Service +end +defmodule Grpc.Testing.XdsUpdateClientConfigureService.Service do + @moduledoc false + use GRPC.Service, + name: "grpc.testing.XdsUpdateClientConfigureService", + protoc_gen_elixir_version: "0.10.0" + + rpc :Configure, Grpc.Testing.ClientConfigureRequest, Grpc.Testing.ClientConfigureResponse +end + +defmodule Grpc.Testing.XdsUpdateClientConfigureService.Stub do + @moduledoc false + use GRPC.Stub, service: Grpc.Testing.XdsUpdateClientConfigureService.Service +end diff --git a/interop/lib/interop/app.ex b/interop/lib/interop/app.ex index 22857fde..1d3d2d81 100644 --- a/interop/lib/interop/app.ex +++ b/interop/lib/interop/app.ex @@ -2,15 +2,10 @@ defmodule Interop.App do use Application def start(_type, _args) do - import Supervisor.Spec - - children = [ - supervisor(GRPC.Server.Supervisor, [{Interop.Endpoint, 10000}]) - ] + children = [{GRPC.Server.Supervisor, endpoint: Interop.Endpoint, port: 10000}] GRPCPrometheus.ServerInterceptor.setup() GRPCPrometheus.ClientInterceptor.setup() - :prometheus_httpd.start() Interop.ServerInterceptor.Statix.connect() opts = [strategy: :one_for_one, name: __MODULE__] diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index dfa9e041..d8661ea9 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -1,13 +1,15 @@ defmodule Interop.Client do import ExUnit.Assertions, only: [refute: 1] + require Logger + def connect(host, port, opts \\ []) do {:ok, ch} = GRPC.Stub.connect(host, port, opts) ch end def empty_unary!(ch) do - IO.puts("Run empty_unary!") + Logger.info("Run empty_unary!") empty = Grpc.Testing.Empty.new() {:ok, ^empty} = Grpc.Testing.TestService.Stub.empty_call(ch, empty) end @@ -17,21 +19,21 @@ defmodule Interop.Client do end def large_unary!(ch) do - IO.puts("Run large_unary!") + Logger.info("Run large_unary!") req = Grpc.Testing.SimpleRequest.new(response_size: 314_159, payload: payload(271_828)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(314_159)) {:ok, ^reply} = Grpc.Testing.TestService.Stub.unary_call(ch, req) end def large_unary2!(ch) do - IO.puts("Run large_unary2!") + Logger.info("Run large_unary2!") req = Grpc.Testing.SimpleRequest.new(response_size: 1024*1024*8, payload: payload(1024*1024*8)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(1024*1024*8)) {:ok, ^reply} = Grpc.Testing.TestService.Stub.unary_call(ch, req) end def client_compressed_unary!(ch) do - IO.puts("Run client_compressed_unary!") + Logger.info("Run client_compressed_unary!") # "Client calls UnaryCall with the feature probe, an uncompressed message" is not supported req = Grpc.Testing.SimpleRequest.new(expect_compressed: %{value: true}, response_size: 314_159, payload: payload(271_828)) @@ -44,7 +46,7 @@ defmodule Interop.Client do end def server_compressed_unary!(ch) do - IO.puts("Run server_compressed_unary!") + Logger.info("Run server_compressed_unary!") req = Grpc.Testing.SimpleRequest.new(response_compressed: %{value: true}, response_size: 314_159, payload: payload(271_828)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(314_159)) @@ -57,7 +59,7 @@ defmodule Interop.Client do end def client_streaming!(ch) do - IO.puts("Run client_streaming!") + Logger.info("Run client_streaming!") stream = ch @@ -79,7 +81,7 @@ defmodule Interop.Client do end def client_compressed_streaming!(ch) do - IO.puts("Run client_compressed_streaming!") + Logger.info("Run client_compressed_streaming!") # INVALID_ARGUMENT testing is not supported @@ -97,7 +99,7 @@ defmodule Interop.Client do end def server_streaming!(ch) do - IO.puts("Run server_streaming!") + Logger.info("Run server_streaming!") params = Enum.map([31415, 9, 2653, 58979], &res_param(&1)) req = Grpc.Testing.StreamingOutputCallRequest.new(response_parameters: params) {:ok, res_enum} = ch |> Grpc.Testing.TestService.Stub.streaming_output_call(req) @@ -110,7 +112,7 @@ defmodule Interop.Client do end def server_compressed_streaming!(ch) do - IO.puts("Run server_compressed_streaming!") + Logger.info("Run server_compressed_streaming!") req = Grpc.Testing.StreamingOutputCallRequest.new(response_parameters: [ %{compressed: %{value: true}, size: 31415}, @@ -127,7 +129,7 @@ defmodule Interop.Client do end def ping_pong!(ch) do - IO.puts("Run ping_pong!") + Logger.info("Run ping_pong!") stream = Grpc.Testing.TestService.Stub.full_duplex_call(ch) req = fn size1, size2 -> @@ -156,7 +158,7 @@ defmodule Interop.Client do end def empty_stream!(ch) do - IO.puts("Run empty_stream!") + Logger.info("Run empty_stream!") {:ok, res_enum} = ch @@ -168,7 +170,7 @@ defmodule Interop.Client do end def custom_metadata!(ch) do - IO.puts("Run custom_metadata!") + Logger.info("Run custom_metadata!") # UnaryCall req = Grpc.Testing.SimpleRequest.new(response_size: 314_159, payload: payload(271_828)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(314_159)) @@ -205,7 +207,7 @@ defmodule Interop.Client do end def status_code_and_message!(ch) do - IO.puts("Run status_code_and_message!") + Logger.info("Run status_code_and_message!") code = 2 msg = "test status message" @@ -227,7 +229,7 @@ defmodule Interop.Client do end def unimplemented_service!(ch) do - IO.puts("Run unimplemented_service!") + Logger.info("Run unimplemented_service!") req = Grpc.Testing.Empty.new() {:error, %GRPC.RPCError{status: 12}} = @@ -235,7 +237,7 @@ defmodule Interop.Client do end def cancel_after_begin!(ch) do - IO.puts("Run cancel_after_begin!") + Logger.info("Run cancel_after_begin!") stream = Grpc.Testing.TestService.Stub.streaming_input_call(ch) stream = GRPC.Stub.cancel(stream) error = GRPC.RPCError.exception(1, "The operation was cancelled") @@ -243,7 +245,7 @@ defmodule Interop.Client do end def cancel_after_first_response!(ch) do - IO.puts("Run cancel_after_first_response!") + Logger.info("Run cancel_after_first_response!") req = Grpc.Testing.StreamingOutputCallRequest.new( @@ -264,7 +266,7 @@ defmodule Interop.Client do end def timeout_on_sleeping_server!(ch) do - IO.puts("Run timeout_on_sleeping_server!") + Logger.info("Run timeout_on_sleeping_server!") req = Grpc.Testing.StreamingOutputCallRequest.new( diff --git a/interop/mix.exs b/interop/mix.exs index dd3dff6a..2ce8d69c 100644 --- a/interop/mix.exs +++ b/interop/mix.exs @@ -23,13 +23,12 @@ defmodule Interop.MixProject do defp deps do [ {:grpc, path: "..", override: true}, - {:cowlib, "~> 2.9.0", override: true}, + {:protobuf, "~> 0.10"}, {:grpc_prometheus, ">= 0.1.0"}, {:grpc_statsd, "~> 0.1.0"}, {:statix, ">= 1.2.1"}, {:extrace, "~> 0.2"}, - {:prometheus, "~> 4.0", override: true}, - {:prometheus_httpd, "~> 2.0"} + {:prometheus, "~> 4.0", override: true} ] end end diff --git a/interop/mix.lock b/interop/mix.lock index b04bcec6..11f40443 100644 --- a/interop/mix.lock +++ b/interop/mix.lock @@ -1,17 +1,17 @@ %{ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, - "cowlib": {:hex, :cowlib, "2.9.0", "8365736a2ada74d5e8640c9b03efff15aceffcf2c7cba2e5ffd0c549f54bf0da", [:rebar3], [], "hexpm", "e8a93bbdf5c4f3d63fbb0cae422de2b58227955bebad5fed978f7a83b0ca4c89"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, "extrace": {:hex, :extrace, "0.2.1", "e234f1f64df8c989771b7b5d047a3412f10512c0e3d414fc7eb0e8fc633779f8", [:mix], [{:recon, "~> 2.5", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "1b2d9fc4bacc208d5aaa97f61e7c47b66ff4dfc155e9b95647e68ca316ab3981"}, "grpc": {:git, "https://github.com/elixir-grpc/grpc.git", "21422839798e49bf6d29327fab0a7add51becedd", []}, "grpc_prometheus": {:hex, :grpc_prometheus, "0.1.0", "a2f45ca83018c4ae59e4c293b7455634ac09e38c36cba7cc1fb8affdf462a6d5", [:mix], [{:grpc, ">= 0.0.0", [hex: :grpc, repo: "hexpm", optional: true]}, {:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8b9ab3098657e7daec0b3edc78e1d02418bc0871618d8ca89b51b74a8086bb71"}, "grpc_statsd": {:hex, :grpc_statsd, "0.1.0", "a95ae388188486043f92a3c5091c143f5a646d6af80c9da5ee616546c4d8f5ff", [:mix], [{:grpc, ">= 0.0.0", [hex: :grpc, repo: "hexpm", optional: true]}, {:statix, ">= 0.0.0", [hex: :statix, repo: "hexpm", optional: true]}], "hexpm", "de0c05db313c7b3ffeff345855d173fd82fec3de16591a126b673f7f698d9e74"}, - "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "03dbbca1a9c604a0267a40ea1d69986225091acb822de0b2dbea21d5815e410b"}, + "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm", "b479a33d4aa4ba7909186e29bb6c1240254e0047a8e2a9f88463f50c0089370e"}, "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"}, "prometheus_httpd": {:hex, :prometheus_httpd, "2.1.11", "f616ed9b85b536b195d94104063025a91f904a4cfc20255363f49a197d96c896", [:rebar3], [{:accept, "~> 0.3", [hex: :accept, repo: "hexpm", optional: false]}, {:prometheus, "~> 4.2", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "0bbe831452cfdf9588538eb2f570b26f30c348adae5e95a7d87f35a5910bcf92"}, - "protobuf": {:hex, :protobuf, "0.7.1", "7d1b9f7d9ecb32eccd96b0c58572de4d1c09e9e3bc414e4cb15c2dce7013f195", [:mix], [], "hexpm", "6eff7a5287963719521c82e5d5b4583fd1d7cdd89ad129f0ea7d503a50a4d13f"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, + "protobuf": {:hex, :protobuf, "0.10.0", "4e8e3cf64c5be203b329f88bb8b916cb8d00fb3a12b2ac1f545463ae963c869f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4ae21a386142357aa3d31ccf5f7d290f03f3fa6f209755f6e87fc2c58c147893"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"}, "statix": {:hex, :statix, "1.2.1", "4f23c8cc2477ea0de89fed5e34f08c54b0d28b838f7b8f26613155f2221bb31e", [:mix], [], "hexpm", "7f988988fddcce19ae376bb8e47aa5ea5dabf8d4ba78d34d1ae61eb537daf72e"}, } diff --git a/interop/script/run.exs b/interop/script/run.exs index 0728d6e7..81cb5d53 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -1,18 +1,26 @@ {options, _, _} = OptionParser.parse(System.argv(), strict: [rounds: :integer, concurrency: :integer, port: :integer]) -rounds = Keyword.get(options, :rounds, 100) -concurrency = Keyword.get(options, :concurrency, 10) -port = Keyword.get(options, :port, 0) +rounds = Keyword.get(options, :rounds) || 20 +max_concurrency = System.schedulers_online() +concurrency = Keyword.get(options, :concurrency) || max_concurrency +port = Keyword.get(options, :port) || 0 +level = Keyword.get(options, :log_level) || "warn" +level = String.to_existing_atom(level) -IO.puts "Rounds: #{rounds}; concurrency: #{concurrency}; port: #{port}" -IO.puts "" +require Logger + +Logger.configure(level: level) + +Logger.info("Rounds: #{rounds}; concurrency: #{concurrency}; port: #{port}") alias Interop.Client + {:ok, _pid, port} = GRPC.Server.start_endpoint(Interop.Endpoint, port) -stream = Task.async_stream(1..concurrency, fn cli -> +1..concurrency +|> Task.async_stream(fn _cli -> ch = Client.connect("127.0.0.1", port, interceptors: [GRPCPrometheus.ClientInterceptor, GRPC.Logger.Client]) - run = fn(i) -> - IO.puts("Client##{cli}, Round #{i}") + + for _ <- 1..rounds do Client.empty_unary!(ch) Client.cacheable_unary!(ch) Client.large_unary!(ch) @@ -32,14 +40,9 @@ stream = Task.async_stream(1..concurrency, fn cli -> Client.cancel_after_first_response!(ch) Client.timeout_on_sleeping_server!(ch) end - Enum.each(1..rounds, run) :ok end, max_concurrency: concurrency, ordered: false, timeout: :infinity) - -Enum.map(stream, fn result -> - result -end) -|> IO.inspect +|> Enum.to_list() # defmodule Helper do # def flush() do @@ -54,5 +57,5 @@ end) # end # Helper.flush() -IO.puts("Succeed!") +Logger.info("Succeed!") :ok = GRPC.Server.stop_endpoint(Interop.Endpoint) diff --git a/lib/grpc/channel.ex b/lib/grpc/channel.ex index 09194cc1..d7818b07 100644 --- a/lib/grpc/channel.ex +++ b/lib/grpc/channel.ex @@ -11,23 +11,23 @@ defmodule GRPC.Channel do * `:port` - server's port to connect * `:scheme` - scheme of connection, like `http` * `:cred` - credentials used for authentication - * `:adapter` - a client adapter module, like `GRPC.Adapter.Gun` + * `:adapter` - a client adapter module, like `GRPC.Client.Adapters.Gun` * `:codec` - a default codec for this channel * `:adapter_payload` - payload the adapter uses """ @type t :: %__MODULE__{ host: String.t(), - port: non_neg_integer, + port: non_neg_integer(), scheme: String.t(), cred: GRPC.Credential.t(), - adapter: atom, - adapter_payload: any, - codec: module, + adapter: atom(), + adapter_payload: any(), + codec: module(), interceptors: [], - compressor: module, - accepted_compressors: [module], - headers: list + compressor: module(), + accepted_compressors: [module()], + headers: list() } defstruct host: nil, port: nil, diff --git a/lib/grpc/client/adapter.ex b/lib/grpc/client/adapter.ex new file mode 100644 index 00000000..b1f684a6 --- /dev/null +++ b/lib/grpc/client/adapter.ex @@ -0,0 +1,25 @@ +defmodule GRPC.Client.Adapter do + @moduledoc """ + HTTP client adapter for GRPC. + """ + + alias GRPC.Client.Stream + alias GRPC.Channel + + @typedoc "Determines if the headers have finished being read." + @type fin :: :fin | :nofin + + @callback connect(channel :: Channel.t(), opts :: keyword()) :: + {:ok, Channel.t()} | {:error, any()} + + @callback disconnect(channel :: Channel.t()) :: {:ok, Channel.t()} | {:error, any()} + + @callback send_request(stream :: Stream.t(), contents :: binary(), opts :: keyword()) :: + Stream.t() + + @doc """ + Check `GRPC.Stub.recv/2` for more context about the return types + """ + @callback receive_data(stream :: Stream.t(), opts :: keyword()) :: + GRPC.Stub.receive_data_return() | {:error, any()} +end diff --git a/lib/grpc/adapter/gun.ex b/lib/grpc/client/adapters/gun.ex similarity index 51% rename from lib/grpc/adapter/gun.ex rename to lib/grpc/client/adapters/gun.ex index bd7234aa..b602b704 100644 --- a/lib/grpc/adapter/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -1,59 +1,62 @@ -defmodule GRPC.Adapter.Gun do - @moduledoc false +defmodule GRPC.Client.Adapters.Gun do + @moduledoc """ + A client adapter using Gun - # A client adapter using Gun. - # conn_pid and stream_ref is stored in `GRPC.Server.Stream`. + `conn_pid` and `stream_ref` are stored in `GRPC.Server.Stream`. + """ + + @behaviour GRPC.Client.Adapter @default_transport_opts [nodelay: true] - @default_http2_opts %{settings_timeout: :infinity} @max_retries 100 - @spec connect(GRPC.Channel.t(), any) :: {:ok, GRPC.Channel.t()} | {:error, any} - def connect(channel, nil), do: connect(channel, %{}) - def connect(%{scheme: "https"} = channel, opts), do: connect_securely(channel, opts) - def connect(channel, opts), do: connect_insecurely(channel, opts) + @impl true + def connect(channel, opts) when is_list(opts) do + # handle opts as a map due to :gun.open + opts = Map.new(opts) + + case channel do + %{scheme: "https"} -> connect_securely(channel, opts) + _ -> connect_insecurely(channel, opts) + end + end defp connect_securely(%{cred: %{ssl: ssl}} = channel, opts) do - transport_opts = Map.get(opts, :transport_opts, @default_transport_opts ++ ssl) - open_opts = %{transport: :ssl, protocols: [:http2]} + transport_opts = Map.get(opts, :transport_opts) || [] - open_opts = - if gun_v2?() do - Map.put(open_opts, :tls_opts, transport_opts) - else - Map.put(open_opts, :transport_opts, transport_opts) - end + tls_opts = Keyword.merge(@default_transport_opts ++ ssl, transport_opts) - open_opts = Map.merge(opts, open_opts) + open_opts = + opts + |> Map.delete(:transport_opts) + |> Map.merge(%{transport: :ssl, protocols: [:http2], tls_opts: tls_opts}) do_connect(channel, open_opts) end defp connect_insecurely(channel, opts) do - opts = Map.update(opts, :http2_opts, @default_http2_opts, &Map.merge(&1, @default_http2_opts)) + opts = + Map.update( + opts, + :http2_opts, + %{settings_timeout: :infinity}, + &Map.put(&1, :settings_timeout, :infinity) + ) - transport_opts = Map.get(opts, :transport_opts, @default_transport_opts) - open_opts = %{transport: :tcp, protocols: [:http2]} + transport_opts = Map.get(opts, :transport_opts) || [] - open_opts = - if gun_v2?() do - Map.put(open_opts, :tcp_opts, transport_opts) - else - Map.put(open_opts, :transport_opts, transport_opts) - end + tcp_opts = Keyword.merge(@default_transport_opts, transport_opts) - open_opts = Map.merge(opts, open_opts) + open_opts = + opts + |> Map.delete(:transport_opts) + |> Map.merge(%{transport: :tcp, protocols: [:http2], tcp_opts: tcp_opts}) do_connect(channel, open_opts) end defp do_connect(%{host: host, port: port} = channel, open_opts) do - open_opts = - if gun_v2?() do - Map.merge(%{retry: @max_retries, retry_fun: &__MODULE__.retry_fun/2}, open_opts) - else - open_opts - end + open_opts = Map.merge(%{retry: @max_retries, retry_fun: &__MODULE__.retry_fun/2}, open_opts) {:ok, conn_pid} = open(host, port, open_opts) @@ -67,10 +70,11 @@ defmodule GRPC.Adapter.Gun do {:error, reason} -> :gun.shutdown(conn_pid) - {:error, "Error when opening connection: #{inspect(reason)}"} + {:error, reason} end end + @impl true def disconnect(%{adapter_payload: %{conn_pid: gun_pid}} = channel) when is_pid(gun_pid) do :ok = :gun.shutdown(gun_pid) @@ -87,7 +91,7 @@ defmodule GRPC.Adapter.Gun do defp open(host, port, open_opts), do: :gun.open(String.to_charlist(host), port, open_opts) - @spec send_request(GRPC.Client.Stream.t(), binary, map) :: GRPC.Client.Stream.t() + @impl true def send_request(stream, message, opts) do stream_ref = do_send_request(stream, message, opts) GRPC.Client.Stream.put_payload(stream, :stream_ref, stream_ref) @@ -130,12 +134,44 @@ defmodule GRPC.Adapter.Gun do :gun.cancel(conn_pid, stream_ref) end - def recv_headers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do + @impl true + def receive_data( + %{server_stream: true} = stream, + opts + ) do + %{channel: %{adapter_payload: adapter_payload}, payload: payload} = stream + + with {:ok, headers, is_fin} <- recv_headers(adapter_payload, payload, opts) do + response = response_stream(is_fin, stream, opts) + + if(opts[:return_headers]) do + {:ok, response, %{headers: headers}} + else + {:ok, response} + end + end + end + + def receive_data(stream, opts) do + %{payload: payload, channel: %{adapter_payload: adapter_payload}} = stream + + with {:ok, headers, _is_fin} <- recv_headers(adapter_payload, payload, opts), + {:ok, body, trailers} <- recv_body(adapter_payload, payload, opts), + {:ok, response} <- parse_response(stream, headers, body, trailers) do + if(opts[:return_headers]) do + {:ok, response, %{headers: headers, trailers: trailers}} + else + {:ok, response} + end + end + end + + defp recv_headers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do case await(conn_pid, stream_ref, opts[:timeout]) do {:response, headers, fin} -> {:ok, headers, fin} - error = {:error, _} -> + {:error, _} = error -> error other -> @@ -147,7 +183,7 @@ defmodule GRPC.Adapter.Gun do end end - def recv_data_or_trailers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do + defp recv_data_or_trailers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do case await(conn_pid, stream_ref, opts[:timeout]) do data = {:data, _} -> data @@ -155,7 +191,7 @@ defmodule GRPC.Adapter.Gun do trailers = {:trailers, _} -> trailers - error = {:error, _} -> + {:error, _} = error -> error other -> @@ -179,7 +215,7 @@ defmodule GRPC.Adapter.Gun do case :gun.await(conn_pid, stream_ref, timeout) do {:response, :fin, status, headers} -> if status == 200 do - headers = Enum.into(headers, %{}) + headers = GRPC.Transport.HTTP2.decode_headers(headers) case headers["grpc-status"] do nil -> @@ -209,7 +245,7 @@ defmodule GRPC.Adapter.Gun do {:response, :nofin, status, headers} -> if status == 200 do - headers = Enum.into(headers, %{}) + headers = GRPC.Transport.HTTP2.decode_headers(headers) if headers["grpc-status"] && headers["grpc-status"] != "0" do {:error, @@ -261,17 +297,6 @@ defmodule GRPC.Adapter.Gun do end end - @char_2 List.first('2') - def gun_v2?() do - case :application.get_key(:gun, :vsn) do - {:ok, [@char_2 | _]} -> - true - - _ -> - false - end - end - def retry_fun(retries, _opts) do curr = @max_retries - retries + 1 @@ -292,4 +317,143 @@ defmodule GRPC.Adapter.Gun do timeout = round(timeout + jitter * timeout) %{retries: retries - 1, timeout: timeout} end + + defp recv_body(conn_payload, stream_payload, opts) do + recv_body(conn_payload, stream_payload, "", opts) + end + + defp recv_body(conn_payload, stream_payload, acc, opts) do + case recv_data_or_trailers(conn_payload, stream_payload, opts) do + {:data, data} -> + recv_body(conn_payload, stream_payload, <>, opts) + + {:trailers, trailers} -> + {:ok, acc, GRPC.Transport.HTTP2.decode_headers(trailers)} + + err -> + err + end + end + + defp response_stream(:fin, _stream, _opts), do: [] + + defp response_stream( + :nofin, + %{ + channel: %{adapter_payload: ap}, + response_mod: res_mod, + codec: codec, + payload: payload + }, + opts + ) do + state = %{ + adapter_payload: ap, + payload: payload, + buffer: <<>>, + fin: false, + need_more: true, + opts: opts, + response_mod: res_mod, + codec: codec + } + + Stream.unfold(state, fn s -> read_stream(s) end) + end + + defp read_stream(%{buffer: <<>>, fin: true, fin_resp: nil}), do: nil + + defp read_stream(%{buffer: <<>>, fin: true, fin_resp: fin_resp} = s), + do: {fin_resp, Map.put(s, :fin_resp, nil)} + + defp read_stream( + %{ + adapter_payload: ap, + payload: payload, + buffer: buffer, + need_more: true, + opts: opts + } = stream + ) do + case recv_data_or_trailers(ap, payload, opts) do + {:data, data} -> + stream + |> Map.put(:need_more, false) + |> Map.put(:buffer, buffer <> data) + |> read_stream() + + {:trailers, trailers} -> + update_stream_with_trailers(stream, trailers, opts[:return_headers]) + + error = {:error, _} -> + {error, %{buffer: <<>>, fin: true, fin_resp: nil}} + end + end + + defp read_stream(%{buffer: buffer, need_more: false, response_mod: res_mod, codec: codec} = s) do + case GRPC.Message.get_message(buffer) do + {{_, message}, rest} -> + reply = codec.decode(message, res_mod) + new_s = Map.put(s, :buffer, rest) + {{:ok, reply}, new_s} + + _ -> + read_stream(Map.put(s, :need_more, true)) + end + end + + defp parse_response( + %{response_mod: res_mod, codec: codec, accepted_compressors: accepted_compressors}, + headers, + body, + trailers + ) do + with :ok <- parse_trailers(trailers), + compressor <- get_compressor(headers, accepted_compressors), + body <- get_body(codec, body), + {:ok, msg} <- GRPC.Message.from_data(%{compressor: compressor}, body) do + {:ok, codec.decode(msg, res_mod)} + end + end + + defp update_stream_with_trailers(stream, trailers, return_headers?) do + trailers = GRPC.Transport.HTTP2.decode_headers(trailers) + + case parse_trailers(trailers) do + :ok -> + fin_resp = if return_headers?, do: {:trailers, trailers} + + stream + |> Map.put(:fin, true) + |> Map.put(:fin_resp, fin_resp) + |> read_stream() + + error -> + {error, %{buffer: <<>>, fin: true, fin_resp: nil}} + end + end + + defp parse_trailers(trailers) do + status = String.to_integer(trailers["grpc-status"]) + + if status == GRPC.Status.ok() do + :ok + else + {:error, %GRPC.RPCError{status: status, message: trailers["grpc-message"]}} + end + end + + defp get_compressor(%{"grpc-encoding" => encoding} = _headers, accepted_compressors) do + Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding end) + end + + defp get_compressor(_headers, _accepted_compressors), do: nil + + defp get_body(codec, body) do + if function_exported?(codec, :unpack_from_channel, 1) do + codec.unpack_from_channel(body) + else + body + end + end end diff --git a/lib/grpc/client/stream.ex b/lib/grpc/client/stream.ex index c75e6f39..e52842f4 100644 --- a/lib/grpc/client/stream.ex +++ b/lib/grpc/client/stream.ex @@ -14,24 +14,24 @@ defmodule GRPC.Client.Stream do * `:res_stream` - indicates if reply is streaming """ - @typep stream_payload :: any + @typep stream_payload :: any() @type t :: %__MODULE__{ channel: GRPC.Channel.t(), service_name: String.t(), method_name: String.t(), - grpc_type: atom, - rpc: tuple, + grpc_type: atom(), + rpc: tuple(), payload: stream_payload, path: String.t(), - request_mod: atom, - response_mod: atom, - codec: atom, - server_stream: boolean, - canceled: boolean, - compressor: module, - accepted_compressors: [module], - headers: map, - __interface__: map + request_mod: atom(), + response_mod: atom(), + codec: atom(), + server_stream: boolean(), + canceled: boolean(), + compressor: module(), + accepted_compressors: [module()], + headers: map(), + __interface__: map() } defstruct channel: nil, @@ -50,7 +50,10 @@ defmodule GRPC.Client.Stream do compressor: nil, accepted_compressors: [], headers: %{}, - __interface__: %{send_request: &__MODULE__.send_request/3, recv: &GRPC.Stub.do_recv/2} + __interface__: %{ + send_request: &__MODULE__.send_request/3, + receive_data: &__MODULE__.receive_data/2 + } @doc false def put_payload(%{payload: payload} = stream, key, val) do @@ -75,7 +78,7 @@ defmodule GRPC.Client.Stream do opts ) do encoded = codec.encode(request) - send_end_stream = Keyword.get(opts, :end_stream, false) + send_end_stream = Keyword.get(opts, :end_stream) || false # If compressor exists, compress is true by default compressor = @@ -90,4 +93,8 @@ defmodule GRPC.Client.Stream do compressor: compressor ) end + + def receive_data(%{channel: %{adapter: adapter}} = stream, opts) do + adapter.receive_data(stream, opts) + end end diff --git a/lib/grpc/codec.ex b/lib/grpc/codec.ex index ce4a2a99..b7a15ab8 100644 --- a/lib/grpc/codec.ex +++ b/lib/grpc/codec.ex @@ -7,4 +7,23 @@ defmodule GRPC.Codec do @callback name() :: String.t() @callback encode(any) :: binary @callback decode(any, module :: atom) :: any + + @doc """ + This function is invoked before the gRPC payload is transformed into a protobuf message whenever it is defined. + + This can be used to apply a transform over the gRPC message before decoding it. For instance grpc-web using the `application/grpc-web-text` + content type requires the message to be Base64-encoded, so a server receving messages using grpc-web-text will be required to + do a Base64 decode on the payload before decoding the gRPC message. + """ + @callback unpack_from_channel(binary) :: binary + + @doc """ + This function is invoked whenever it is defined after the protobuf message has been transformed into a gRPC payload. + + This can be used to apply a transform over the gRPC message before sending it. + For instance grpc-web using the `application/grpc-web-text` content type requires the message to be Base64-encoded, so a server sending messages using grpc-web-text will be required to + do a Base64 encode on the payload before sending the gRPC message. + """ + @callback pack_for_channel(iodata()) :: binary + @optional_callbacks unpack_from_channel: 1, pack_for_channel: 1 end diff --git a/lib/grpc/codec/proto.ex b/lib/grpc/codec/proto.ex index d3a9748a..d3de8b16 100644 --- a/lib/grpc/codec/proto.ex +++ b/lib/grpc/codec/proto.ex @@ -5,11 +5,11 @@ defmodule GRPC.Codec.Proto do "proto" end - def encode(struct) do - Protobuf.Encoder.encode(struct) + def encode(%mod{} = struct) do + mod.encode(struct) end def decode(binary, module) do - Protobuf.Decoder.decode(binary, module) + module.decode(binary) end end diff --git a/lib/grpc/codec/web_text.ex b/lib/grpc/codec/web_text.ex new file mode 100644 index 00000000..398e9856 --- /dev/null +++ b/lib/grpc/codec/web_text.ex @@ -0,0 +1,29 @@ +defmodule GRPC.Codec.WebText do + @behaviour GRPC.Codec + + def name() do + "text" + end + + def encode(struct) do + Protobuf.Encoder.encode(struct) + end + + def pack_for_channel(data) when is_list(data) do + data + |> IO.iodata_to_binary() + |> Base.encode64() + end + + def pack_for_channel(binary) do + Base.encode64(binary) + end + + def unpack_from_channel(binary) do + Base.decode64!(binary) + end + + def decode(binary, module) do + Protobuf.Decoder.decode(binary, module) + end +end diff --git a/lib/grpc/credential.ex b/lib/grpc/credential.ex index e9eebe0d..1df78ea7 100644 --- a/lib/grpc/credential.ex +++ b/lib/grpc/credential.ex @@ -1,8 +1,15 @@ defmodule GRPC.Credential do @moduledoc """ - Stores credentials for authentication. It can be used to establish secure connections + Stores credentials for authentication. + + It can be used to establish secure connections by passed to `GRPC.Stub.connect/2` as an argument. + Some client and server adapter implementations may + choose to let request options override some of the + configuration here, but this is left as a choice + for each adapter. + ## Examples iex> cred = GRPC.Credential.new(ssl: [cacertfile: ca_path]) @@ -16,6 +23,6 @@ defmodule GRPC.Credential do Creates credential. """ def new(opts) do - %__MODULE__{ssl: Keyword.get(opts, :ssl, [])} + %__MODULE__{ssl: Keyword.get(opts, :ssl) || []} end end diff --git a/lib/grpc/interceptor.ex b/lib/grpc/interceptor.ex index 4e4c1555..ac64dc45 100644 --- a/lib/grpc/interceptor.ex +++ b/lib/grpc/interceptor.ex @@ -4,9 +4,10 @@ defmodule GRPC.ServerInterceptor do """ alias GRPC.Server.Stream - @type options :: any - @type rpc_return :: {:ok, Stream.t(), struct} | {:ok, Stream.t()} | {:error, GRPC.RPCError.t()} - @type next :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return) + @type options :: any() + @type rpc_return :: + {:ok, Stream.t(), struct()} | {:ok, Stream.t()} | {:error, GRPC.RPCError.t()} + @type next :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return()) @callback init(options) :: options @callback call(GRPC.Server.rpc_req(), stream :: Stream.t(), next, options) :: rpc_return @@ -18,8 +19,8 @@ defmodule GRPC.ClientInterceptor do """ alias GRPC.Client.Stream - @type options :: any - @type req :: struct | nil + @type options :: any() + @type req :: struct() | nil @type next :: (Stream.t(), req -> GRPC.Stub.rpc_return()) @callback init(options) :: options diff --git a/lib/grpc/logger.ex b/lib/grpc/logger.ex deleted file mode 100644 index 0c1e9b03..00000000 --- a/lib/grpc/logger.ex +++ /dev/null @@ -1,95 +0,0 @@ -defmodule GRPC.Logger.Server do - @moduledoc """ - Print log around server rpc calls, like: - - 17:18:45.151 [info] Handled by HelloServer.say_hello - 17:18:45.151 [info] Response :ok in 11µs - - ## Usage - - defmodule Your.Endpoint do - use GRPC.Endpoint - - intercept GRPC.Logger.Server, level: :info - end - """ - require Logger - @behaviour GRPC.ServerInterceptor - - def init(opts) do - Keyword.get(opts, :level, :info) - end - - def call(req, stream, next, level) do - if Logger.compare_levels(level, Logger.level()) != :lt do - Logger.log(level, fn -> - ["Handled by ", inspect(stream.server), ".", to_string(elem(stream.rpc, 0))] - end) - - start = System.monotonic_time() - result = next.(req, stream) - stop = System.monotonic_time() - - status = elem(result, 0) - - Logger.log(level, fn -> - diff = System.convert_time_unit(stop - start, :native, :microsecond) - - ["Response ", inspect(status), " in ", formatted_diff(diff)] - end) - - result - else - next.(req, stream) - end - end - - def formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] - def formatted_diff(diff), do: [Integer.to_string(diff), "µs"] -end - -defmodule GRPC.Logger.Client do - require Logger - - @moduledoc """ - Print log around client rpc calls, like - - 17:13:33.021 [info] Call say_hello of helloworld.Greeter - 17:13:33.079 [info] Got :ok in 58ms - - ## Usage - - {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) - {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [{GRPC.Logger.Client, level: :info}]) - """ - - def init(opts) do - Keyword.get(opts, :level, :info) - end - - def call(%{grpc_type: grpc_type} = stream, req, next, level) do - if Logger.compare_levels(level, Logger.level()) != :lt do - Logger.log(level, fn -> - ["Call ", to_string(elem(stream.rpc, 0)), " of ", stream.service_name] - end) - - start = System.monotonic_time() - result = next.(stream, req) - stop = System.monotonic_time() - - if grpc_type == :unary do - status = elem(result, 0) - - Logger.log(level, fn -> - diff = System.convert_time_unit(stop - start, :native, :microsecond) - - ["Got ", inspect(status), " in ", GRPC.Logger.Server.formatted_diff(diff)] - end) - end - - result - else - next.(stream, req) - end - end -end diff --git a/lib/grpc/logger/client.ex b/lib/grpc/logger/client.ex new file mode 100644 index 00000000..a841e342 --- /dev/null +++ b/lib/grpc/logger/client.ex @@ -0,0 +1,65 @@ +defmodule GRPC.Logger.Client do + @moduledoc """ + Print log around client rpc calls, like + + 17:13:33.021 [info] Call say_hello of helloworld.Greeter + 17:13:33.079 [info] Got :ok in 58ms + + ## Options + + * `:level` - the desired log level. Defaults to `:info` + * `:accepted_comparators` - a list with the accepted `Logger.compare_levels(configured_level, Logger.level())` results. + Defaults to `[:lt, :eq]` + + ## Usage + + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) + # This will log on `:info` and greater priority + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [{GRPC.Logger.Client, level: :info}]) + # This will log only on `:info` + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [{GRPC.Logger.Client, level: :info, accepted_comparators: [:eq]}]) + # This will log on `:info` and lower priority + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [{GRPC.Logger.Client, level: :info, accepted_comparators: [:eq, :gt]}]) + """ + + require Logger + + @behaviour GRPC.ClientInterceptor + + @impl true + def init(opts) do + level = Keyword.get(opts, :level) || :info + accepted_comparators = Keyword.get(opts, :accepted_comparators) || [:lt, :eq] + [level: level, accepted_comparators: accepted_comparators] + end + + @impl true + def call(%{grpc_type: grpc_type} = stream, req, next, opts) do + level = Keyword.fetch!(opts, :level) + accepted_comparators = Keyword.fetch!(opts, :accepted_comparators) + + if Logger.compare_levels(level, Logger.level()) in accepted_comparators do + Logger.log(level, fn -> + ["Call ", to_string(elem(stream.rpc, 0)), " of ", stream.service_name] + end) + + start = System.monotonic_time() + result = next.(stream, req) + stop = System.monotonic_time() + + if grpc_type == :unary do + status = elem(result, 0) + + Logger.log(level, fn -> + diff = System.convert_time_unit(stop - start, :native, :microsecond) + + ["Got ", inspect(status), " in ", GRPC.Logger.Server.formatted_diff(diff)] + end) + end + + result + else + next.(stream, req) + end + end +end diff --git a/lib/grpc/logger/server.ex b/lib/grpc/logger/server.ex new file mode 100644 index 00000000..b9070eaa --- /dev/null +++ b/lib/grpc/logger/server.ex @@ -0,0 +1,75 @@ +defmodule GRPC.Logger.Server do + @moduledoc """ + Print log around server rpc calls, like: + + 17:18:45.151 [info] Handled by HelloServer.say_hello + 17:18:45.151 [info] Response :ok in 11µs + + ## Options + + * `:level` - the desired log level. Defaults to `:info` + * `:accepted_comparators` - a list with the accepted `Logger.compare_levels(configured_level, Logger.level())` results. + Defaults to `[:lt, :eq]` + + ## Usage + + defmodule Your.Endpoint do + use GRPC.Endpoint + + intercept GRPC.Logger.Server, level: :info + end + + defmodule Your.Endpoint do + use GRPC.Endpoint + + # logs on :info and higher priority (warn, error...) + intercept GRPC.Logger.Server, level: :info, accepted_comparators: [:lt, :eq] + end + + defmodule Your.Endpoint do + use GRPC.Endpoint + + # logs only on :error + intercept GRPC.Logger.Server, level: :error, accepted_comparators: [:eq] + end + """ + + require Logger + + @behaviour GRPC.ServerInterceptor + + @impl true + def init(opts) do + level = Keyword.get(opts, :level) || :info + accepted_comparators = Keyword.get(opts, :accepted_comparators) || [:lt, :eq] + [level: level, accepted_comparators: accepted_comparators] + end + + @impl true + def call(req, stream, next, opts) do + level = Keyword.fetch!(opts, :level) + accepted_comparators = opts[:accepted_comparators] + + if Logger.compare_levels(level, Logger.level()) in accepted_comparators do + Logger.metadata(request_id: Logger.metadata()[:request_id] || stream.request_id) + + Logger.log(level, "Handled by #{inspect(stream.server)}.#{elem(stream.rpc, 0)}") + + start = System.monotonic_time() + result = next.(req, stream) + stop = System.monotonic_time() + + status = elem(result, 0) + diff = System.convert_time_unit(stop - start, :native, :microsecond) + + Logger.log(level, "Response #{inspect(status)} in #{formatted_diff(diff)}") + + result + else + next.(req, stream) + end + end + + def formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] + def formatted_diff(diff), do: [Integer.to_string(diff), "µs"] +end diff --git a/lib/grpc/message.ex b/lib/grpc/message.ex index ba908c7a..cb75a343 100644 --- a/lib/grpc/message.ex +++ b/lib/grpc/message.ex @@ -1,37 +1,51 @@ defmodule GRPC.Message do + @moduledoc """ + Transform data between encoded protobuf and HTTP/2 body of gRPC. + + gRPC body format is: + + http://www.grpc.io/docs/guides/wire.html + Delimited-Message -> Compressed-Flag Message-Length Message + Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer + Message-Length -> {length of Message} # encoded as 4 byte unsigned integer + Message -> *{binary octet} + """ use Bitwise, only_operators: true @max_message_length 1 <<< (32 - 1) alias GRPC.RPCError - @moduledoc false + @doc """ + Transforms Protobuf data into a gRPC body binary. - # Transform data between encoded protobuf and HTTP/2 body of gRPC. - # - # gRPC body format is: - # - # # http://www.grpc.io/docs/guides/wire.html - # Delimited-Message -> Compressed-Flag Message-Length Message - # Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer - # Message-Length -> {length of Message} # encoded as 4 byte unsigned integer - # Message -> *{binary octet} + ## Options - @doc """ - Transform protobuf data to gRPC body + * `:compressor` - the optional `GRPC.Compressor` to be used. + * `:iolist` - if `true`, encodes the data as an `t:iolist()` instead of a `t:binary()` + * `:max_message_length` - the maximum number of bytes for the encoded message. ## Examples - iex> message = <<1, 2, 3, 4, 5, 6, 7, 8>> + iex> message = ["m", [["es", "sa"], "ge"]] iex> GRPC.Message.to_data(message) - {:ok, <<0, 0, 0, 0, 8, 1, 2, 3, 4, 5, 6, 7, 8>>, 13} + {:ok, <<0, 0, 0, 0, 7, "message">>, 12} + iex> GRPC.Message.to_data(message, iolist: true) + {:ok, [0, <<0, 0, 0, 7>>, ["m", [["es", "sa"], "ge"]]], 12} + + Error cases: + iex> message = <<1, 2, 3, 4, 5, 6, 7, 8, 9>> iex> GRPC.Message.to_data(message, %{max_message_length: 8}) {:error, "Encoded message is too large (9 bytes)"} + """ - @spec to_data(iodata, map | Keyword.t()) :: - {:ok, binary, non_neg_integer} | {:error, String.t()} - def to_data(message, opts \\ %{}) do + @spec to_data(iodata, keyword()) :: + {:ok, iodata, non_neg_integer} | {:error, String.t()} + def to_data(message, opts \\ []) do compressor = opts[:compressor] + iolist = opts[:iolist] + codec = opts[:codec] + max_length = opts[:max_message_length] || @max_message_length {compress_flag, message} = if compressor do @@ -40,19 +54,26 @@ defmodule GRPC.Message do {0, message} end - length = byte_size(message) - max_length = opts[:max_message_length] || @max_message_length + length = IO.iodata_length(message) if length > max_length do {:error, "Encoded message is too large (#{length} bytes)"} else - result = <> + result = [compress_flag, <>, message] + + result = + if function_exported?(codec, :pack_for_channel, 1), + do: codec.pack_for_channel(result), + else: result + + result = if iolist, do: result, else: IO.iodata_to_binary(result) + {:ok, result, length + 5} end end @doc """ - Transform gRPC body to protobuf data + Transforms gRPC body into Protobuf data. ## Examples @@ -66,7 +87,7 @@ defmodule GRPC.Message do end @doc """ - Transform gRPC body to protobuf data with compressing + Transform gRPC body into Protobuf data with compression. ## Examples @@ -130,7 +151,7 @@ defmodule GRPC.Message do end @doc """ - Get message data from data buffer + Get message data from data buffer. ## Examples diff --git a/lib/grpc/message/protobuf.ex b/lib/grpc/message/protobuf.ex deleted file mode 100644 index 3a36537f..00000000 --- a/lib/grpc/message/protobuf.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule GRPC.Message.Protobuf do - @moduledoc false - - # Module for encoding or decoding message using Protobuf. - - @spec encode(atom, struct) :: binary - def encode(mod, struct) do - apply(mod, :encode, [struct]) - end - - @spec decode(atom, binary) :: struct - def decode(mod, message) do - apply(mod, :decode, [message]) - end -end diff --git a/lib/grpc/server.ex b/lib/grpc/server.ex index 557686a0..0cbed721 100644 --- a/lib/grpc/server.ex +++ b/lib/grpc/server.ex @@ -33,18 +33,17 @@ defmodule GRPC.Server do require Logger - alias GRPC.Server.Stream alias GRPC.RPCError @type rpc_req :: struct | Enumerable.t() @type rpc_return :: struct | any - @type rpc :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return) + @type rpc :: (GRPC.Server.rpc_req(), GRPC.Server.Stream.t() -> rpc_return) defmacro __using__(opts) do quote bind_quoted: [opts: opts], location: :keep do service_mod = opts[:service] service_name = service_mod.__meta__(:name) - codecs = opts[:codecs] || [GRPC.Codec.Proto] + codecs = opts[:codecs] || [GRPC.Codec.Proto, GRPC.Codec.WebText] compressors = opts[:compressors] || [] Enum.each(service_mod.__rpc_calls__, fn {name, _, _} = rpc -> @@ -77,22 +76,38 @@ defmodule GRPC.Server do end end - @type servers_map :: %{String.t() => [module]} - @type servers_list :: module | [module] - @doc false - @spec call(atom, Stream.t(), tuple, atom) :: {:ok, Stream.t(), struct} | {:ok, struct} + @spec call(atom(), GRPC.Server.Stream.t(), tuple(), atom()) :: + {:ok, GRPC.Server.Stream.t(), struct()} | {:ok, struct()} def call( _service_mod, stream, {_, {req_mod, req_stream}, {res_mod, res_stream}} = rpc, func_name ) do - stream = %{stream | request_mod: req_mod, response_mod: res_mod, rpc: rpc} + request_id = generate_request_id() + + stream = %{ + stream + | request_mod: req_mod, + request_id: request_id, + response_mod: res_mod, + rpc: rpc + } handle_request(req_stream, res_stream, stream, func_name) end + defp generate_request_id do + binary = << + System.system_time(:nanosecond)::64, + :erlang.phash2({node(), self()}, 16_777_216)::24, + :erlang.unique_integer()::32 + >> + + Base.url_encode64(binary, padding: false) + end + defp handle_request(req_s, res_s, %{server: server} = stream, func_name) do if function_exported?(server, func_name, 2) do do_handle_request(req_s, res_s, stream, func_name) @@ -109,7 +124,14 @@ defmodule GRPC.Server do ) do {:ok, data} = adapter.read_body(payload) - case GRPC.Message.from_data(stream, data) do + body = + if function_exported?(codec, :unpack_from_channel, 1) do + codec.unpack_from_channel(data) + else + data + end + + case GRPC.Message.from_data(stream, body) do {:ok, message} -> request = codec.decode(message, req_mod) @@ -214,21 +236,26 @@ defmodule GRPC.Server do # # * `:cred` - a credential created by functions of `GRPC.Credential`, # an insecure server will be created without this option - # * `:adapter` - use a custom server adapter instead of default `GRPC.Adapter.Cowboy` + # * `:adapter` - use a custom server adapter instead of default `GRPC.Server.Adapters.Cowboy` + # * `:adapter_opts` - configuration for the specified adapter. + # * `:status_handler` - adds a status handler that could be listening on HTTP/1, if necessary. + # It should follow the format defined by cowboy_router:compile/3 @doc false - @spec start(servers_list, non_neg_integer, Keyword.t()) :: {atom, any, non_neg_integer} + @spec start(module() | [module()], non_neg_integer(), Keyword.t()) :: + {atom(), any(), non_neg_integer()} | {:error, any()} def start(servers, port, opts \\ []) do - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) + adapter = Keyword.get(opts, :adapter) || GRPC.Server.Adapters.Cowboy servers = GRPC.Server.servers_to_map(servers) adapter.start(nil, servers, port, opts) end @doc false - @spec start_endpoint(atom, non_neg_integer, Keyword.t()) :: {atom, any, non_neg_integer} + @spec start_endpoint(atom(), non_neg_integer(), Keyword.t()) :: + {atom(), any(), non_neg_integer()} def start_endpoint(endpoint, port, opts \\ []) do servers = endpoint.__meta__(:servers) servers = GRPC.Server.servers_to_map(servers) - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) + adapter = Keyword.get(opts, :adapter) || GRPC.Server.Adapters.Cowboy adapter.start(endpoint, servers, port, opts) end @@ -240,19 +267,19 @@ defmodule GRPC.Server do # # ## Options # - # * `:adapter` - use a custom adapter instead of default `GRPC.Adapter.Cowboy` + # * `:adapter` - use a custom adapter instead of default `GRPC.Server.Adapters.Cowboy` @doc false - @spec stop(servers_list, Keyword.t()) :: any + @spec stop(module() | [module()], Keyword.t()) :: any() def stop(servers, opts \\ []) do - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) + adapter = Keyword.get(opts, :adapter) || GRPC.Server.Adapters.Cowboy servers = GRPC.Server.servers_to_map(servers) adapter.stop(nil, servers) end @doc false - @spec stop_endpoint(atom, Keyword.t()) :: any + @spec stop_endpoint(atom(), Keyword.t()) :: any() def stop_endpoint(endpoint, opts \\ []) do - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) + adapter = Keyword.get(opts, :adapter) || GRPC.Server.Adapters.Cowboy servers = endpoint.__meta__(:servers) servers = GRPC.Server.servers_to_map(servers) adapter.stop(endpoint, servers) @@ -273,7 +300,7 @@ defmodule GRPC.Server do iex> GRPC.Server.send_reply(stream, reply) """ - @spec send_reply(Stream.t(), struct) :: Stream.t() + @spec send_reply(GRPC.Server.Stream.t(), struct()) :: GRPC.Server.Stream.t() def send_reply(%{__interface__: interface} = stream, reply, opts \\ []) do interface[:send_reply].(stream, reply, opts) end @@ -283,7 +310,7 @@ defmodule GRPC.Server do You can send headers only once, before that you can set headers using `set_headers/2`. """ - @spec send_headers(Stream.t(), map) :: Stream.t() + @spec send_headers(GRPC.Server.Stream.t(), map()) :: GRPC.Server.Stream.t() def send_headers(%{adapter: adapter} = stream, headers) do adapter.send_headers(stream.payload, headers) stream @@ -294,7 +321,7 @@ defmodule GRPC.Server do You can set headers more than once. """ - @spec set_headers(Stream.t(), map) :: Stream.t() + @spec set_headers(GRPC.Server.Stream.t(), map()) :: GRPC.Server.Stream.t() def set_headers(%{adapter: adapter} = stream, headers) do adapter.set_headers(stream.payload, headers) stream @@ -303,7 +330,7 @@ defmodule GRPC.Server do @doc """ Set custom trailers, which will be sent in the end. """ - @spec set_trailers(Stream.t(), map) :: Stream.t() + @spec set_trailers(GRPC.Server.Stream.t(), map()) :: GRPC.Server.Stream.t() def set_trailers(%{adapter: adapter} = stream, trailers) do adapter.set_resp_trailers(stream.payload, trailers) stream @@ -313,7 +340,7 @@ defmodule GRPC.Server do Set compressor to compress responses. An accepted compressor will be set if clients use one, even if `set_compressor` is not called. But this can be called to override the chosen. """ - @spec set_compressor(Stream.t(), module) :: Stream.t() + @spec set_compressor(GRPC.Server.Stream.t(), module()) :: GRPC.Server.Stream.t() def set_compressor(%{adapter: adapter} = stream, compressor) do adapter.set_compressor(stream.payload, compressor) stream @@ -333,7 +360,7 @@ defmodule GRPC.Server do end @doc false - @spec servers_to_map(servers_list) :: servers_map + @spec servers_to_map(module() | [module()]) :: %{String.t() => [module()]} def servers_to_map(servers) do Enum.reduce(List.wrap(servers), %{}, fn s, acc -> Map.put(acc, s.__meta__(:service).__meta__(:name), s) diff --git a/lib/grpc/server/adapter.ex b/lib/grpc/server/adapter.ex new file mode 100644 index 00000000..9884ac9f --- /dev/null +++ b/lib/grpc/server/adapter.ex @@ -0,0 +1,27 @@ +defmodule GRPC.Server.Adapter do + @moduledoc """ + HTTP server adapter for GRPC. + """ + + @type state :: %{ + pid: pid, + handling_timer: reference | nil, + resp_trailers: map, + compressor: atom | nil, + pending_reader: nil + } + + @callback start( + atom(), + %{String.t() => [module()]}, + port :: non_neg_integer(), + opts :: keyword() + ) :: + {atom(), any(), non_neg_integer()} + + @callback stop(atom(), %{String.t() => [module()]}) :: :ok | {:error, :not_found} + + @callback send_reply(state, content :: binary(), opts :: keyword()) :: any() + + @callback send_headers(state, headers :: map()) :: any() +end diff --git a/lib/grpc/adapter/cowboy.ex b/lib/grpc/server/adapters/cowboy.ex similarity index 68% rename from lib/grpc/adapter/cowboy.ex rename to lib/grpc/server/adapters/cowboy.ex index 31bc0994..51caa7f0 100644 --- a/lib/grpc/adapter/cowboy.ex +++ b/lib/grpc/server/adapters/cowboy.ex @@ -1,21 +1,23 @@ -defmodule GRPC.Adapter.Cowboy do - @moduledoc false +defmodule GRPC.Server.Adapters.Cowboy do + @moduledoc """ + A server (`b:GRPC.Server.Adapter`) adapter using `:cowboy`. - # A server(`GRPC.Server`) adapter using Cowboy. - # Cowboy req will be stored in `:payload` of `GRPC.Server.Stream`. + Cowboy requests will be stored in the `:payload` field of the `GRPC.Server.Stream`. + """ - # Waiting for this is released on hex https://github.com/ninenines/ranch/pull/227 - @dialyzer {:nowarn_function, running_info: 4} + @behaviour GRPC.Server.Adapter + + # ignore a specific warning generated by the case :ranch.child_spec call + @dialyzer {:no_match, child_spec: 4} require Logger - alias GRPC.Adapter.Cowboy.Handler, as: Handler + alias GRPC.Server.Adapters.Cowboy.Handler @default_num_acceptors 20 @default_max_connections 16384 # Only used in starting a server manually using `GRPC.Server.start(servers)` - @spec start(atom, GRPC.Server.servers_map(), non_neg_integer, keyword) :: - {:ok, pid, non_neg_integer} + @impl true def start(endpoint, servers, port, opts) do start_args = cowboy_start_args(endpoint, servers, port, opts) start_func = if opts[:cred], do: :start_tls, else: :start_clear @@ -30,8 +32,8 @@ defmodule GRPC.Adapter.Cowboy do end end - @spec child_spec(atom, GRPC.Server.servers_map(), non_neg_integer, Keyword.t()) :: - Supervisor.Spec.spec() + @spec child_spec(atom(), %{String.t() => [module()]}, non_neg_integer(), Keyword.t()) :: + Supervisor.child_spec() def child_spec(endpoint, servers, port, opts) do [ref, trans_opts, proto_opts] = cowboy_start_args(endpoint, servers, port, opts) trans_opts = Map.put(trans_opts, :connection_type, :supervisor) @@ -43,17 +45,31 @@ defmodule GRPC.Adapter.Cowboy do {:ranch_tcp, :cowboy_clear} end - {ref, mfa, type, timeout, kind, modules} = - :ranch.child_spec(ref, transport, trans_opts, protocol, proto_opts) + # Ideally, we would just update Ranch, but compatibility issues with cowboy hold us back on this + # So we just support both child spec versions here instead + case :ranch.child_spec(ref, transport, trans_opts, protocol, proto_opts) do + {ref, mfa, type, timeout, kind, modules} -> + scheme = if opts[:cred], do: :https, else: :http + # Wrap real mfa to print starting log + wrapped_mfa = {__MODULE__, :start_link, [scheme, endpoint, servers, mfa]} - scheme = if opts[:cred], do: :https, else: :http - # Wrap real mfa to print starting log - mfa = {__MODULE__, :start_link, [scheme, endpoint, servers, mfa]} - {ref, mfa, type, timeout, kind, modules} + %{ + id: ref, + start: wrapped_mfa, + restart: type, + shutdown: timeout, + type: kind, + modules: modules + } + + child_spec when is_map(child_spec) -> + child_spec + end end # spec: :supervisor.mfargs doesn't work - @spec start_link(atom, atom, GRPC.Server.servers_map(), any) :: {:ok, pid} | {:error, any} + @spec start_link(atom(), atom(), %{String.t() => [module()]}, any()) :: + {:ok, pid()} | {:error, any()} def start_link(scheme, endpoint, servers, {m, f, [ref | _] = a}) do case apply(m, f, a) do {:ok, pid} -> @@ -73,17 +89,17 @@ defmodule GRPC.Adapter.Cowboy do end end - @spec stop(atom, GRPC.Server.servers_map()) :: :ok | {:error, :not_found} + @impl true def stop(endpoint, servers) do :cowboy.stop_listener(servers_name(endpoint, servers)) end - @spec read_body(GRPC.Adapter.Cowboy.Handler.state()) :: {:ok, binary} + @spec read_body(GRPC.Server.Adapter.state()) :: {:ok, binary()} def read_body(%{pid: pid}) do Handler.read_full_body(pid) end - @spec reading_stream(GRPC.Adapter.Cowboy.Handler.state()) :: Enumerable.t() + @spec reading_stream(GRPC.Server.Adapter.state()) :: Enumerable.t() def reading_stream(%{pid: pid}) do Stream.unfold(%{pid: pid, need_more: true, buffer: <<>>}, fn acc -> read_stream(acc) end) end @@ -115,11 +131,12 @@ defmodule GRPC.Adapter.Cowboy do end end - @spec send_reply(GRPC.Adapter.Cowboy.Handler.state(), binary, keyword) :: any + @impl true def send_reply(%{pid: pid}, data, opts) do Handler.stream_body(pid, data, opts, :nofin) end + @impl true def send_headers(%{pid: pid}, headers) do Handler.stream_reply(pid, 200, headers) end @@ -153,14 +170,25 @@ defmodule GRPC.Adapter.Cowboy do end defp cowboy_start_args(endpoint, servers, port, opts) do - dispatch = - :cowboy_router.compile([ - {:_, [{:_, GRPC.Adapter.Cowboy.Handler, {endpoint, servers, Enum.into(opts, %{})}}]} - ]) + # Custom handler to be able to listen in the same port, more info: + # https://github.com/containous/traefik/issues/6211 + {adapter_opts, opts} = Keyword.pop(opts, :adapter_opts, []) + status_handler = Keyword.get(adapter_opts, :status_handler) + + handlers = + if status_handler do + [ + status_handler, + {:_, GRPC.Server.Adapters.Cowboy.Handler, {endpoint, servers, Enum.into(opts, %{})}} + ] + else + [{:_, GRPC.Server.Adapters.Cowboy.Handler, {endpoint, servers, Enum.into(opts, %{})}}] + end - idle_timeout = Keyword.get(opts, :idle_timeout, :infinity) - num_acceptors = Keyword.get(opts, :num_acceptors, @default_num_acceptors) - max_connections = Keyword.get(opts, :max_connections, @default_max_connections) + dispatch = :cowboy_router.compile([{:_, handlers}]) + idle_timeout = Keyword.get(opts, :idle_timeout) || :infinity + num_acceptors = Keyword.get(opts, :num_acceptors) || @default_num_acceptors + max_connections = Keyword.get(opts, :max_connections) || @default_max_connections # https://ninenines.eu/docs/en/cowboy/2.7/manual/cowboy_http2/ opts = @@ -215,8 +243,8 @@ defmodule GRPC.Adapter.Cowboy do addr_str = case addr do - :local -> - port + :undefined -> + raise "undefined address for ranch server" addr -> "#{:inet.ntoa(addr)}:#{port}" diff --git a/lib/grpc/adapter/cowboy/handler.ex b/lib/grpc/server/adapters/cowboy/handler.ex similarity index 93% rename from lib/grpc/adapter/cowboy/handler.ex rename to lib/grpc/server/adapters/cowboy/handler.ex index 495a4f3d..f0f5641b 100644 --- a/lib/grpc/adapter/cowboy/handler.ex +++ b/lib/grpc/server/adapters/cowboy/handler.ex @@ -1,4 +1,4 @@ -defmodule GRPC.Adapter.Cowboy.Handler do +defmodule GRPC.Server.Adapters.Cowboy.Handler do @moduledoc false # A cowboy handler accepting all requests and calls corresponding functions @@ -8,17 +8,13 @@ defmodule GRPC.Adapter.Cowboy.Handler do alias GRPC.RPCError require Logger - @adapter GRPC.Adapter.Cowboy + @adapter GRPC.Server.Adapters.Cowboy @default_trailers HTTP2.server_trailers() - @type state :: %{ - pid: pid, - handling_timer: reference | nil, - resp_trailers: map, - compressor: atom | nil, - pending_reader: nil - } - - @spec init(map, {atom, GRPC.Server.servers_map(), map}) :: {:cowboy_loop, map, map} + + @spec init( + map(), + state :: {endpoint :: atom(), servers :: %{String.t() => [module()]}, opts :: keyword()} + ) :: {:cowboy_loop, map(), map()} def init(req, {endpoint, servers, opts} = state) do path = :cowboy_req.path(req) @@ -249,14 +245,12 @@ defmodule GRPC.Adapter.Cowboy.Handler do if compressor && !Enum.member?(accepted_encodings, compressor.name()) do msg = - "A unaccepted encoding #{compressor.name()} is set, valid are: #{ - :cowboy_req.header("grpc-accept-encoding", req) - }" + "A unaccepted encoding #{compressor.name()} is set, valid are: #{:cowboy_req.header("grpc-accept-encoding", req)}" req = send_error(req, state, msg) {:stop, req, state} else - case GRPC.Message.to_data(data, compressor: compressor) do + case GRPC.Message.to_data(data, compressor: compressor, codec: opts[:codec]) do {:ok, data, _size} -> req = check_sent_resp(req) :cowboy_req.stream_body(data, is_fin, req) @@ -453,10 +447,16 @@ defmodule GRPC.Adapter.Cowboy.Handler do defp extract_subtype("application/grpc"), do: {:ok, "proto"} defp extract_subtype("application/grpc+"), do: {:ok, "proto"} defp extract_subtype("application/grpc;"), do: {:ok, "proto"} - defp extract_subtype(<<"application/grpc+", rest::binary>>), do: {:ok, rest} defp extract_subtype(<<"application/grpc;", rest::binary>>), do: {:ok, rest} + defp extract_subtype("application/grpc-web"), do: {:ok, "proto"} + defp extract_subtype("application/grpc-web+"), do: {:ok, "proto"} + defp extract_subtype("application/grpc-web;"), do: {:ok, "proto"} + defp extract_subtype("application/grpc-web-text"), do: {:ok, "text"} + defp extract_subtype("application/grpc-web+" <> rest), do: {:ok, rest} + defp extract_subtype("application/grpc-web-text+" <> rest), do: {:ok, rest} + defp extract_subtype(type) do Logger.warn("Got unknown content-type #{type}, please create an issue.") {:ok, "proto"} @@ -481,8 +481,8 @@ defmodule GRPC.Adapter.Cowboy.Handler do end defp async_read_body(req, opts) do - length = Map.get(opts, :length, 8_000_000) - period = Map.get(opts, :period, 15000) + length = opts[:length] || 8_000_000 + period = opts[:period] || 15000 ref = make_ref() :cowboy_req.cast({:read_body, self(), ref, length, period}, req) diff --git a/lib/grpc/server/stream.ex b/lib/grpc/server/stream.ex index 9463ecf9..bd349722 100644 --- a/lib/grpc/server/stream.ex +++ b/lib/grpc/server/stream.ex @@ -9,7 +9,7 @@ defmodule GRPC.Server.Stream do ## Fields * `:server` - user defined gRPC server module - * `:adapter` - a server adapter module, like `GRPC.Adapter.Cowboy` + * `:adapter` - a server adapter module, like `GRPC.Server.Adapters.Cowboy` * `request_mod` - the request module, or nil for untyped protocols * `response_mod` - the response module, or nil for untyped protocols * `:codec` - the codec @@ -18,22 +18,23 @@ defmodule GRPC.Server.Stream do """ @type t :: %__MODULE__{ - server: atom, + server: atom(), service_name: String.t(), method_name: String.t(), - grpc_type: atom, - endpoint: atom, - rpc: tuple, - request_mod: atom, - response_mod: atom, - codec: atom, - payload: any, - adapter: atom, - local: any, + grpc_type: atom(), + endpoint: atom(), + rpc: tuple(), + request_mod: atom(), + request_id: String.t() | nil, + response_mod: atom(), + codec: atom(), + payload: any(), + adapter: atom(), + local: any(), # compressor mainly is used in client decompressing, responses compressing should be set by # `GRPC.Server.set_compressor` - compressor: module | nil, - __interface__: map + compressor: module() | nil, + __interface__: map() } defstruct server: nil, @@ -43,6 +44,7 @@ defmodule GRPC.Server.Stream do endpoint: nil, rpc: nil, request_mod: nil, + request_id: nil, response_mod: nil, codec: GRPC.Codec.Proto, payload: nil, @@ -54,7 +56,7 @@ defmodule GRPC.Server.Stream do def send_reply(%{adapter: adapter, codec: codec} = stream, reply, opts) do # {:ok, data, _size} = reply |> codec.encode() |> GRPC.Message.to_data() data = codec.encode(reply) - adapter.send_reply(stream.payload, data, opts) + adapter.send_reply(stream.payload, data, Keyword.put(opts, :codec, codec)) stream end end diff --git a/lib/grpc/server/supervisor.ex b/lib/grpc/server/supervisor.ex index c380a3e0..5d97b1de 100644 --- a/lib/grpc/server/supervisor.ex +++ b/lib/grpc/server/supervisor.ex @@ -1,55 +1,77 @@ defmodule GRPC.Server.Supervisor do - use Supervisor - @moduledoc """ A simple supervisor to start your servers. - You can add it to your OTP tree as below. But to make the servers start, you have to config `grpc` + You can add it to your OTP tree as below. + To start the server, you can pass `start_server: true` and an option defmodule Your.App do use Application def start(_type, _args) do - import Supervisor.Spec - children = [ - supervisor(GRPC.Server.Supervisor, [{Your.Endpoint, 50051(, opts)}]) - ] + {GRPC.Server.Supervisor, endpoint: Your.Endpoint, port: 50051, start_server: true, ...}] - opts = [strategy: :one_for_one, name: __MODULE__] - Supervisor.start_link(children, opts) + Supervisor.start_link(children, strategy: :one_for_one, name: __MODULE__) end end - # config.exs - config :grpc, start_server: true - or - run `mix grpc.server` on local - - View `child_spec/3` for opts. """ - @default_adapter GRPC.Adapter.Cowboy + use Supervisor + + @default_adapter GRPC.Server.Adapters.Cowboy require Logger def start_link(endpoint) do Supervisor.start_link(__MODULE__, endpoint) end - @spec init({module | [module], integer}) :: - {:ok, {:supervisor.sup_flags(), [:supervisor.child_spec()]}} | :ignore - def init({endpoint, port}) do - init({endpoint, port, []}) + @doc """ + ## Options + + * `:endpoint` - defines the endpoint module that will be started. + * `:port` - the HTTP port for the endpoint. + * `:servers` - the list of servers that will be be started. + + Either `:endpoint` or `:servers` must be present, but not both. + """ + @spec init(tuple()) :: no_return + @spec init(keyword()) :: {:ok, {:supervisor.sup_flags(), [:supervisor.child_spec()]}} | :ignore + def init(opts \\ []) + + def init(opts) when is_tuple(opts) do + raise ArgumentError, + "passing a tuple as configuration for GRPC.Server.Supervisor is no longer supported. See the documentation for more information on how to configure." end - @spec init({module | [module], integer, Keyword.t()}) :: - {:ok, {:supervisor.sup_flags(), [:supervisor.child_spec()]}} | :ignore - def init({endpoint, port, opts}) do - check_deps_version() + def init(opts) when is_list(opts) do + unless is_nil(Application.get_env(:grpc, :start_server)) do + raise "the :start_server config key has been deprecated.\ + The currently supported way is to configure it\ + through the :start_server option for the GRPC.Server.Supervisor" + end + + endpoint_or_servers = + case {opts[:endpoint], opts[:servers]} do + {endpoint, servers} + when (not is_nil(endpoint) and not is_nil(servers)) or + (is_nil(endpoint) and is_nil(servers)) -> + raise ArgumentError, "either :endpoint or :servers must be passed, but not both." + + {endpoint, nil} -> + endpoint + + {nil, servers} when not is_list(servers) -> + raise ArgumentError, "either :servers must be a list of modules" + + {nil, servers} when is_list(servers) -> + servers + end children = - if Application.get_env(:grpc, :start_server, false) do - [child_spec(endpoint, port, opts)] + if opts[:start_server] do + [child_spec(endpoint_or_servers, opts[:port], opts)] else [] end @@ -64,9 +86,13 @@ defmodule GRPC.Server.Supervisor do * `:cred` - a credential created by functions of `GRPC.Credential`, an insecure server will be created without this option + * `:start_server` - determines if the server will be started. + If present, has more precedence then the `config :gprc, :start_server` + config value (i.e. `start_server: false` will not start the server in any case). """ - @spec child_spec(atom | [atom], integer, Keyword.t()) :: Supervisor.Spec.spec() - def child_spec(endpoint, port, opts \\ []) + @spec child_spec(endpoint_or_servers :: atom() | [atom()], port :: integer, opts :: keyword()) :: + Supervisor.Spec.spec() + def child_spec(endpoint_or_servers, port, opts \\ []) def child_spec(endpoint, port, opts) when is_atom(endpoint) do {endpoint, servers} = @@ -81,32 +107,14 @@ defmodule GRPC.Server.Supervisor do {nil, endpoint} end - adapter = Keyword.get(opts, :adapter, @default_adapter) + adapter = Keyword.get(opts, :adapter) || @default_adapter servers = GRPC.Server.servers_to_map(servers) adapter.child_spec(endpoint, servers, port, opts) end def child_spec(servers, port, opts) when is_list(servers) do - adapter = Keyword.get(opts, :adapter, @default_adapter) + adapter = Keyword.get(opts, :adapter) || @default_adapter servers = GRPC.Server.servers_to_map(servers) adapter.child_spec(nil, servers, port, opts) end - - defp check_deps_version() do - # cowlib - case :application.get_key(:cowlib, :vsn) do - {:ok, vsn} -> - ver = to_string(vsn) - - unless Version.match?(ver, ">= 2.9.0") do - Logger.warn("cowlib should be >= 2.9.0, it's #{ver} now. See grpc's README for details") - end - - _ -> - :ok - end - rescue - _ -> - :ok - end end diff --git a/lib/grpc/status.ex b/lib/grpc/status.ex index 9c2b7e40..877d5030 100644 --- a/lib/grpc/status.ex +++ b/lib/grpc/status.ex @@ -10,11 +10,13 @@ defmodule GRPC.Status do @doc """ Not an error; returned on success. """ + @spec ok :: t def ok, do: 0 @doc """ The operation was cancelled (typically by the caller). """ + @spec cancelled() :: t() def cancelled, do: 1 @doc """ @@ -22,10 +24,11 @@ defmodule GRPC.Status do An example of where this error may be returned is if a Status value received from another address space belongs to - an error-space that is not known in this address space. Also + an error-space that is not known in this address space. Also errors raised by APIs that do not return enough error information may be converted to this error. """ + @spec unknown :: t def unknown, do: 2 @doc """ @@ -35,6 +38,7 @@ defmodule GRPC.Status do INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the system (e.g., a malformed file name). """ + @spec invalid_argument :: t def invalid_argument, do: 3 @doc """ @@ -45,16 +49,19 @@ defmodule GRPC.Status do successful response from a server could have been delayed long enough for the deadline to expire. """ + @spec deadline_exceeded :: t def deadline_exceeded, do: 4 @doc """ Some requested entity (e.g., file or directory) was not found. """ + @spec not_found :: t def not_found, do: 5 @doc """ Some entity that we attempted to create (e.g., file or directory) already exists. """ + @spec already_exists :: t def already_exists, do: 6 @doc """ @@ -67,12 +74,14 @@ defmodule GRPC.Status do used if the caller can not be identified (use UNAUTHENTICATED instead for those errors). """ + @spec permission_denied :: t def permission_denied, do: 7 @doc """ Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. """ + @spec resource_exhausted :: t def resource_exhausted, do: 8 @doc """ @@ -82,6 +91,7 @@ defmodule GRPC.Status do For example, directory to be deleted may be non-empty, an rmdir operation is applied to a non-directory, etc. """ + @spec failed_precondition :: t def failed_precondition, do: 9 @doc """ @@ -90,6 +100,7 @@ defmodule GRPC.Status do Typically due to a concurrency issue like sequencer check failures, transaction aborts, etc. """ + @spec aborted() :: t() def aborted, do: 10 @doc """ @@ -97,11 +108,13 @@ defmodule GRPC.Status do E.g., seeking or reading past end of file. """ + @spec out_of_range :: t def out_of_range, do: 11 @doc """ Operation is not implemented or not supported/enabled in this service. """ + @spec unimplemented :: t def unimplemented, do: 12 @doc """ @@ -110,6 +123,7 @@ defmodule GRPC.Status do Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. """ + @spec internal :: t def internal, do: 13 @doc """ @@ -118,18 +132,22 @@ defmodule GRPC.Status do This is a most likely a transient condition and may be corrected by retrying with a backoff. """ + @spec unavailable :: t def unavailable, do: 14 @doc """ Unrecoverable data loss or corruption. """ + @spec data_loss :: t def data_loss, do: 15 @doc """ The request does not have valid authentication credentials for the operation. """ + @spec unauthenticated :: t def unauthenticated, do: 16 + @spec code_name(t()) :: binary() def code_name(0), do: "OK" def code_name(1), do: "Canceled" def code_name(2), do: "Unknown" diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 22a65243..db050c0a 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -3,7 +3,7 @@ defmodule GRPC.Stub do A module acting as the interface for gRPC client. You can do everything in the client side via `GRPC.Stub`, including connecting, - sending/receiving steaming or non-steaming requests, canceling calls and so on. + sending/receiving streaming or non-streaming requests, canceling calls and so on. A service is needed to define a stub: @@ -44,13 +44,16 @@ defmodule GRPC.Stub do # 10 seconds @default_timeout 10000 - @type rpc_return :: - {:ok, struct} - | {:ok, struct, map} - | GRPC.Client.Stream.t() + @type receive_data_return :: + {:ok, struct()} + | {:ok, struct(), map()} | {:ok, Enumerable.t()} - | {:ok, Enumerable.t(), map} + | {:ok, Enumerable.t(), map()} + + @type rpc_return :: + GRPC.Client.Stream.t() | {:error, GRPC.RPCError.t()} + | receive_data_return require Logger @@ -121,11 +124,11 @@ defmodule GRPC.Stub do * `:interceptors` - client interceptors * `:codec` - client will use this to encode and decode binary message * `:compressor` - the client will use this to compress requests and decompress responses. If this is set, accepted_compressors - will be appended also, so this can be used safely without `:accesspted_compressors`. + will be appended also, so this can be used safely without `:accepted_compressors`. * `:accepted_compressors` - tell servers accepted compressors, this can be used without `:compressor` * `:headers` - headers to attach to each request """ - @spec connect(String.t(), Keyword.t()) :: {:ok, GRPC.Channel.t()} | {:error, any} + @spec connect(String.t(), keyword()) :: {:ok, Channel.t()} | {:error, any()} def connect(addr, opts \\ []) when is_binary(addr) and is_list(opts) do {host, port} = case String.split(addr, ":") do @@ -136,24 +139,25 @@ defmodule GRPC.Stub do connect(host, port, opts) end - @spec connect(String.t(), binary | non_neg_integer, keyword) :: - {:ok, Channel.t()} | {:error, any} + @spec connect(String.t(), binary() | non_neg_integer(), keyword()) :: + {:ok, Channel.t()} | {:error, any()} def connect(host, port, opts) when is_binary(port) do connect(host, String.to_integer(port), opts) end def connect(host, port, opts) when is_integer(port) do - adapter = - Keyword.get( - opts, - :adapter, - Application.get_env(:grpc, :http2_client_adapter, GRPC.Adapter.Gun) - ) + if Application.get_env(:grpc, :http2_client_adapter) do + raise "the :http2_client_adapter config key has been deprecated.\ + The currently supported way is to configure it\ + through the :adapter option for GRPC.Stub.connect/3" + end + + adapter = Keyword.get(opts, :adapter) || GRPC.Client.Adapters.Gun cred = Keyword.get(opts, :cred) scheme = if cred, do: @secure_scheme, else: @insecure_scheme - interceptors = Keyword.get(opts, :interceptors, []) |> init_interceptors - codec = Keyword.get(opts, :codec, GRPC.Codec.Proto) + interceptors = (Keyword.get(opts, :interceptors) || []) |> init_interceptors + codec = Keyword.get(opts, :codec) || GRPC.Codec.Proto compressor = Keyword.get(opts, :compressor) accepted_compressors = Keyword.get(opts, :accepted_compressors) || [] headers = Keyword.get(opts, :headers) || [] @@ -165,6 +169,12 @@ defmodule GRPC.Stub do accepted_compressors end + adapter_opts = opts[:adapter_opts] || [] + + unless is_list(adapter_opts) do + raise ArgumentError, ":adapter_opts must be a keyword list if present" + end + %Channel{ host: host, port: port, @@ -177,7 +187,7 @@ defmodule GRPC.Stub do accepted_compressors: accepted_compressors, headers: headers } - |> adapter.connect(opts[:adapter_opts]) + |> adapter.connect(adapter_opts) end def retry_timeout(curr) when curr < 11 do @@ -210,7 +220,7 @@ defmodule GRPC.Stub do @doc """ Disconnects the adapter and frees any resources the adapter is consuming """ - @spec disconnect(Channel.t()) :: {:ok, Channel.t()} | {:error, any} + @spec disconnect(Channel.t()) :: {:ok, Channel.t()} | {:error, any()} def disconnect(%Channel{adapter: adapter} = channel) do adapter.disconnect(channel) end @@ -234,7 +244,7 @@ defmodule GRPC.Stub do with the last elem being a map of headers `%{headers: headers, trailers: trailers}`(unary) or `%{headers: headers}`(server streaming) """ - @spec call(atom, tuple, GRPC.Client.Stream.t(), struct | nil, keyword) :: rpc_return + @spec call(atom(), tuple(), GRPC.Client.Stream.t(), struct() | nil, keyword()) :: rpc_return def call(_service_mod, rpc, %{channel: channel} = stream, request, opts) do {_, {req_mod, req_stream}, {res_mod, response_stream}} = rpc @@ -247,8 +257,8 @@ defmodule GRPC.Stub do parse_req_opts([{:timeout, @default_timeout} | opts]) end - compressor = Map.get(opts, :compressor, channel.compressor) - accepted_compressors = Map.get(opts, :accepted_compressors, []) + compressor = Keyword.get(opts, :compressor, channel.compressor) + accepted_compressors = Keyword.get(opts, :accepted_compressors, []) accepted_compressors = if compressor do @@ -259,8 +269,8 @@ defmodule GRPC.Stub do stream = %{ stream - | codec: Map.get(opts, :codec, channel.codec), - compressor: Map.get(opts, :compressor, channel.compressor), + | codec: Keyword.get(opts, :codec, channel.codec), + compressor: Keyword.get(opts, :compressor, channel.compressor), accepted_compressors: accepted_compressors } @@ -275,7 +285,7 @@ defmodule GRPC.Stub do ) do last = fn %{codec: codec, compressor: compressor} = s, _ -> message = codec.encode(request) - opts = Map.put(opts, :compressor, compressor) + opts = Keyword.put(opts, :compressor, compressor) s |> channel.adapter.send_request(message, opts) @@ -322,7 +332,7 @@ defmodule GRPC.Stub do * `:end_stream` - indicates it's the last one request, then the stream will be in half_closed state. Default is false. """ - @spec send_request(GRPC.Client.Stream.t(), struct, Keyword.t()) :: GRPC.Client.Stream.t() + @spec send_request(GRPC.Client.Stream.t(), struct, keyword()) :: GRPC.Client.Stream.t() def send_request(%{__interface__: interface} = stream, request, opts \\ []) do interface[:send_request].(stream, request, opts) end @@ -379,12 +389,12 @@ defmodule GRPC.Stub do * `:deadline` - when the request is timeout, will override timeout * `:return_headers` - when true, headers will be returned. """ - @spec recv(GRPC.Client.Stream.t(), keyword | map) :: - {:ok, struct} - | {:ok, struct, map} + @spec recv(GRPC.Client.Stream.t(), keyword()) :: + {:ok, struct()} + | {:ok, struct(), map()} | {:ok, Enumerable.t()} - | {:ok, Enumerable.t(), map} - | {:error, any} + | {:ok, Enumerable.t(), map()} + | {:error, any()} def recv(stream, opts \\ []) def recv(%{canceled: true}, _) do @@ -394,266 +404,56 @@ defmodule GRPC.Stub do def recv(%{__interface__: interface} = stream, opts) do opts = if is_list(opts) do - parse_recv_opts(opts) + parse_recv_opts(Keyword.put_new(opts, :timeout, @default_timeout)) else opts end - interface[:recv].(stream, opts) - end - - @doc false - def do_recv(%{server_stream: true, channel: channel, payload: payload} = stream, opts) do - case recv_headers(channel.adapter, channel.adapter_payload, payload, opts) do - {:ok, headers, is_fin} -> - res_enum = - case is_fin do - :fin -> [] - :nofin -> response_stream(stream, opts) - end - - if opts[:return_headers] do - {:ok, res_enum, %{headers: headers}} - else - {:ok, res_enum} - end - - {:error, reason} -> - {:error, reason} - end - end - - def do_recv( - %{payload: payload, channel: channel} = stream, - opts - ) do - with {:ok, headers, _is_fin} <- - recv_headers(channel.adapter, channel.adapter_payload, payload, opts), - {:ok, body, trailers} <- - recv_body(channel.adapter, channel.adapter_payload, payload, opts) do - {status, msg} = parse_response(stream, headers, body, trailers) - - if opts[:return_headers] do - {status, msg, %{headers: headers, trailers: trailers}} - else - {status, msg} - end - else - error = {:error, _} -> - error - end - end - - defp recv_headers(adapter, conn_payload, stream_payload, opts) do - case adapter.recv_headers(conn_payload, stream_payload, opts) do - {:ok, headers, is_fin} -> - {:ok, GRPC.Transport.HTTP2.decode_headers(headers), is_fin} - - other -> - other - end - end - - defp recv_body(adapter, conn_payload, stream_payload, opts) do - recv_body(adapter, conn_payload, stream_payload, "", opts) - end - - defp recv_body(adapter, conn_payload, stream_payload, acc, opts) do - case adapter.recv_data_or_trailers(conn_payload, stream_payload, opts) do - {:data, data} -> - recv_body(adapter, conn_payload, stream_payload, <>, opts) - - {:trailers, trailers} -> - {:ok, acc, GRPC.Transport.HTTP2.decode_headers(trailers)} - end - end - - defp parse_response( - %{response_mod: res_mod, codec: codec, accepted_compressors: accepted_compressors}, - headers, - body, - trailers - ) do - case parse_trailers(trailers) do - :ok -> - compressor = - case headers do - %{"grpc-encoding" => encoding} -> - Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding end) - - _ -> - nil - end - - case GRPC.Message.from_data(%{compressor: compressor}, body) do - {:ok, msg} -> - {:ok, codec.decode(msg, res_mod)} - - err -> - err - end - - error -> - error - end - end - - defp parse_trailers(trailers) do - status = String.to_integer(trailers["grpc-status"]) - - if status == GRPC.Status.ok() do - :ok - else - {:error, %GRPC.RPCError{status: status, message: trailers["grpc-message"]}} - end - end - - defp response_stream( - %{ - channel: %{adapter: adapter, adapter_payload: ap}, - response_mod: res_mod, - codec: codec, - payload: payload - }, - opts - ) do - state = %{ - adapter: adapter, - adapter_payload: ap, - payload: payload, - buffer: <<>>, - fin: false, - need_more: true, - opts: opts, - response_mod: res_mod, - codec: codec - } - - Stream.unfold(state, fn s -> read_stream(s) end) - end - - defp read_stream(%{buffer: <<>>, fin: true, fin_resp: nil}), do: nil - - defp read_stream(%{buffer: <<>>, fin: true, fin_resp: fin_resp} = s), - do: {fin_resp, Map.put(s, :fin_resp, nil)} - - defp read_stream( - %{ - adapter: adapter, - adapter_payload: ap, - payload: payload, - buffer: buffer, - need_more: true, - opts: opts - } = s - ) do - case adapter.recv_data_or_trailers(ap, payload, opts) do - {:data, data} -> - buffer = buffer <> data - new_s = s |> Map.put(:need_more, false) |> Map.put(:buffer, buffer) - read_stream(new_s) - - {:trailers, trailers} -> - trailers = GRPC.Transport.HTTP2.decode_headers(trailers) - - case parse_trailers(trailers) do - :ok -> - fin_resp = - if opts[:return_headers] do - {:trailers, trailers} - end - - new_s = s |> Map.put(:fin, true) |> Map.put(:fin_resp, fin_resp) - read_stream(new_s) - - error -> - {error, %{buffer: <<>>, fin: true, fin_resp: nil}} - end - - error = {:error, _} -> - {error, %{buffer: <<>>, fin: true, fin_resp: nil}} - end - end - - defp read_stream(%{buffer: buffer, need_more: false, response_mod: res_mod, codec: codec} = s) do - case GRPC.Message.get_message(buffer) do - # TODO - {{_, message}, rest} -> - reply = codec.decode(message, res_mod) - new_s = Map.put(s, :buffer, rest) - {{:ok, reply}, new_s} - - _ -> - read_stream(Map.put(s, :need_more, true)) - end - end - - defp parse_req_opts(list) when is_list(list) do - parse_req_opts(list, %{}) - end - - defp parse_req_opts([{:timeout, timeout} | t], acc) do - parse_req_opts(t, Map.put(acc, :timeout, timeout)) - end - - defp parse_req_opts([{:deadline, deadline} | t], acc) do - parse_req_opts(t, Map.put(acc, :timeout, GRPC.TimeUtils.to_relative(deadline))) - end - - defp parse_req_opts([{:compressor, compressor} | t], acc) do - parse_req_opts(t, Map.put(acc, :compressor, compressor)) - end - - defp parse_req_opts([{:accepted_compressors, compressors} | t], acc) do - parse_req_opts(t, Map.put(acc, :accepted_compressors, compressors)) - end - - defp parse_req_opts([{:grpc_encoding, grpc_encoding} | t], acc) do - parse_req_opts(t, Map.put(acc, :grpc_encoding, grpc_encoding)) - end - - defp parse_req_opts([{:metadata, metadata} | t], acc) do - parse_req_opts(t, Map.put(acc, :metadata, metadata)) - end - - defp parse_req_opts([{:content_type, content_type} | t], acc) do - Logger.warn(":content_type has been deprecated, please use :codec") - parse_req_opts(t, Map.put(acc, :content_type, content_type)) - end - - defp parse_req_opts([{:codec, codec} | t], acc) do - parse_req_opts(t, Map.put(acc, :codec, codec)) - end - - defp parse_req_opts([{:return_headers, return_headers} | t], acc) do - parse_req_opts(t, Map.put(acc, :return_headers, return_headers)) - end - - defp parse_req_opts([{key, _} | _], _) do - raise ArgumentError, "option #{inspect(key)} is not supported" + interface[:receive_data].(stream, opts) + end + + @valid_req_opts [ + :timeout, + :deadline, + :compressor, + :accepted_compressors, + :grpc_encoding, + :metadata, + :codec, + :return_headers + ] + defp parse_req_opts(opts) when is_list(opts) do + # Map.new is used so we can keep the last value + # passed for a given key + opts + |> Map.new(fn + {:deadline, deadline} -> + {:timeout, GRPC.TimeUtils.to_relative(deadline)} + + {key, value} when key in @valid_req_opts -> + {key, value} + + {key, _} -> + raise ArgumentError, "option #{inspect(key)} is not supported" + end) + |> Map.to_list() end - defp parse_req_opts(_, acc), do: acc - defp parse_recv_opts(list) when is_list(list) do - parse_recv_opts(list, %{timeout: @default_timeout}) - end + # Map.new is used so we can keep the last value + # passed for a given key - defp parse_recv_opts([{:timeout, timeout} | t], acc) do - parse_recv_opts(t, Map.put(acc, :timeout, timeout)) - end + list + |> Map.new(fn + {:deadline, deadline} -> + {:deadline, GRPC.TimeUtils.to_relative(deadline)} - defp parse_recv_opts([{:deadline, deadline} | t], acc) do - parse_recv_opts(t, Map.put(acc, :deadline, GRPC.TimeUtils.to_relative(deadline))) - end - - defp parse_recv_opts([{:return_headers, return_headers} | t], acc) do - parse_recv_opts(t, Map.put(acc, :return_headers, return_headers)) - end + {key, _} when key not in @valid_req_opts -> + raise ArgumentError, "option #{inspect(key)} is not supported" - defp parse_recv_opts([{key, _} | _], _) do - raise ArgumentError, "option #{inspect(key)} is not supported" + kv -> + kv + end) + |> Map.to_list() end - - defp parse_recv_opts(_, acc), do: acc end diff --git a/lib/grpc/transport/http2.ex b/lib/grpc/transport/http2.ex index 189830fc..dd0eea40 100644 --- a/lib/grpc/transport/http2.ex +++ b/lib/grpc/transport/http2.ex @@ -8,23 +8,27 @@ defmodule GRPC.Transport.HTTP2 do require Logger + def server_headers(%{codec: GRPC.Codec.WebText = codec}) do + %{"content-type" => "application/grpc-web-#{codec.name()}"} + end + def server_headers(%{codec: codec}) do - %{"content-type" => "application/grpc+#{codec.name}"} + %{"content-type" => "application/grpc+#{codec.name()}"} end @spec server_trailers(integer, String.t()) :: map def server_trailers(status \\ Status.ok(), message \\ "") do %{ "grpc-status" => Integer.to_string(status), - "grpc-message" => message + "grpc-message" => URI.encode(message) } end @doc """ Now we may not need this because gun already handles the pseudo headers. """ - @spec client_headers(GRPC.Client.Stream.t(), map) :: [{String.t(), String.t()}] - def client_headers(%{channel: channel, path: path} = s, opts \\ %{}) do + @spec client_headers(GRPC.Client.Stream.t(), keyword()) :: [{String.t(), String.t()}] + def client_headers(%{channel: channel, path: path} = s, opts \\ []) do [ {":method", "POST"}, {":scheme", channel.scheme}, @@ -33,8 +37,10 @@ defmodule GRPC.Transport.HTTP2 do ] ++ client_headers_without_reserved(s, opts) end - @spec client_headers_without_reserved(GRPC.Client.Stream.t(), map) :: [{String.t(), String.t()}] - def client_headers_without_reserved(%{codec: codec} = stream, opts \\ %{}) do + @spec client_headers_without_reserved(GRPC.Client.Stream.t(), keyword()) :: [ + {String.t(), String.t()} + ] + def client_headers_without_reserved(%{codec: codec} = stream, opts \\ []) do [ # It seems only gRPC implemenations only support "application/grpc", so we support :content_type now. {"content-type", content_type(opts[:content_type], codec)}, @@ -55,15 +61,11 @@ defmodule GRPC.Transport.HTTP2 do defp content_type(custom, _codec) when is_binary(custom), do: custom - defp content_type(_, codec) do - # Some gRPC implementations don't support application/grpc+xyz, - # to avoid this kind of trouble, use application/grpc by default - if codec == GRPC.Codec.Proto do - "application/grpc" - else - "application/grpc+#{codec.name}" - end - end + # Some gRPC implementations don't support application/grpc+xyz, + # to avoid this kind of trouble, use application/grpc by default + defp content_type(_, GRPC.Codec.Proto), do: "application/grpc" + defp content_type(_, codec = GRPC.Codec.WebText), do: "application/grpc-web-#{codec.name()}" + defp content_type(_, codec), do: "application/grpc+#{codec.name()}" def extract_metadata(headers) do headers @@ -76,7 +78,7 @@ defmodule GRPC.Transport.HTTP2 do if is_metadata(k) do decode_metadata({k, v}) else - {k, v} + decode_reserved({k, v}) end end) end @@ -140,6 +142,12 @@ defmodule GRPC.Transport.HTTP2 do {key, val} end + defp decode_reserved({"grpc-message" = key, val}) do + {key, URI.decode(val)} + end + + defp decode_reserved(kv), do: kv + defp is_reserved_header(":" <> _), do: true defp is_reserved_header("grpc-" <> _), do: true defp is_reserved_header("content-type"), do: true diff --git a/lib/mix/tasks/grpc.server.ex b/lib/mix/tasks/grpc.server.ex deleted file mode 100644 index d0e856ed..00000000 --- a/lib/mix/tasks/grpc.server.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Mix.Tasks.Grpc.Server do - use Mix.Task - - @shortdoc "Starts applications and their servers" - - @moduledoc """ - Starts the application by configuring `start_server` to true. - - The `--no-halt` flag is automatically added. - """ - @impl true - def run(args) do - Application.put_env(:grpc, :start_server, true, persistent: true) - Mix.Task.run("run", run_args() ++ args) - end - - defp run_args do - if iex_running?(), do: [], else: ["--no-halt"] - end - - defp iex_running? do - Code.ensure_loaded?(IEx) and IEx.started?() - end -end diff --git a/mix.exs b/mix.exs index 980c755f..af6eed0b 100644 --- a/mix.exs +++ b/mix.exs @@ -1,13 +1,13 @@ defmodule GRPC.Mixfile do use Mix.Project - @version "0.5.0-beta.1" + @version "0.5.0" def project do [ app: :grpc, version: @version, - elixir: "~> 1.5", + elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, @@ -21,7 +21,10 @@ defmodule GRPC.Mixfile do source_url: "https://github.com/elixir-grpc/grpc" ], dialyzer: [ - plt_add_apps: [:mix, :iex] + plt_add_deps: :apps_tree, + plt_add_apps: [:iex, :mix, :ex_unit], + list_unused_filters: true, + plt_file: {:no_warn, "_build/#{Mix.env()}/plts/dialyzer.plt"} ], xref: [exclude: [IEx]] ] @@ -36,14 +39,14 @@ defmodule GRPC.Mixfile do defp deps do [ - {:protobuf, "~> 0.5"}, - {:cowboy, "~> 2.7"}, - {:gun, "~> 2.0.0", hex: :grpc_gun}, - # 2.9.0 fixes some important bugs, so it's better to use ~> 2.9.0 - # {:cowlib, "~> 2.9.0", override: true}, - {:ex_doc, "~> 0.23", only: :dev}, - {:inch_ex, "~> 2.0", only: [:dev, :test]}, - {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false} + {:cowboy, "~> 2.9"}, + # This is the same as :gun 2.0.0-rc.2, + # but we can't depend on an RC for releases + {:gun, "~> 2.0.1", hex: :grpc_gun}, + {:cowlib, "~> 2.11"}, + {:protobuf, "~> 0.10", only: [:dev, :test]}, + {:ex_doc, "~> 0.28.0", only: :dev}, + {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 5e365221..43cfde44 100644 --- a/mix.lock +++ b/mix.lock @@ -1,19 +1,17 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, - "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"}, - "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, - "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "03dbbca1a9c604a0267a40ea1d69986225091acb822de0b2dbea21d5815e410b"}, - "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, - "protobuf": {:hex, :protobuf, "0.7.1", "7d1b9f7d9ecb32eccd96b0c58572de4d1c09e9e3bc414e4cb15c2dce7013f195", [:mix], [], "hexpm", "6eff7a5287963719521c82e5d5b4583fd1d7cdd89ad129f0ea7d503a50a4d13f"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, + "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, + "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "protobuf": {:hex, :protobuf, "0.10.0", "4e8e3cf64c5be203b329f88bb8b916cb8d00fb3a12b2ac1f545463ae963c869f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4ae21a386142357aa3d31ccf5f7d290f03f3fa6f209755f6e87fc2c58c147893"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, } diff --git a/src/grpc_stream_h.erl b/src/grpc_stream_h.erl index b231fca3..4e2652e7 100644 --- a/src/grpc_stream_h.erl +++ b/src/grpc_stream_h.erl @@ -4,10 +4,6 @@ -module(grpc_stream_h). -behavior(cowboy_stream). --ifdef(OTP_RELEASE). --compile({nowarn_deprecated_function, [{erlang, get_stacktrace, 0}]}). --endif. - -export([init/3]). -export([data/4]). -export([info/3]). @@ -325,21 +321,13 @@ send_request_body(Pid, Ref, fin, BodyLen, Data) -> %% @todo Better spec. -spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok. request_process(Req, Env, Middlewares) -> - OTP = erlang:system_info(otp_release), try execute(Req, Env, Middlewares) catch - exit:Reason -> - Stacktrace = erlang:get_stacktrace(), - erlang:raise(exit, {Reason, Stacktrace}, Stacktrace); - %% OTP 19 does not propagate any exception stacktraces, - %% we therefore add it for every class of exception. - _:Reason when OTP =:= "19" -> - Stacktrace = erlang:get_stacktrace(), + exit:Reason:Stacktrace -> erlang:raise(exit, {Reason, Stacktrace}, Stacktrace); - %% @todo I don't think this clause is necessary. - Class:Reason -> - erlang:raise(Class, Reason, erlang:get_stacktrace()) + Class:Reason:Stacktrace -> + erlang:raise(Class, {Reason, Stacktrace}, Stacktrace) end. execute(_, _, []) -> diff --git a/test/grpc/adapter/gun_test.exs b/test/grpc/adapter/gun_test.exs new file mode 100644 index 00000000..b3d089e0 --- /dev/null +++ b/test/grpc/adapter/gun_test.exs @@ -0,0 +1,87 @@ +defmodule GRPC.Client.Adapters.GunTest do + use ExUnit.Case, async: true + + import GRPC.Factory + + alias GRPC.Client.Adapters.Gun + + describe "connect/2" do + setup do + server_credential = build(:credential) + {:ok, _, port} = GRPC.Server.start(FeatureServer, 0, cred: server_credential) + + on_exit(fn -> + :ok = GRPC.Server.stop(FeatureServer) + end) + + %{ + port: port, + credential: + build(:credential, + ssl: Keyword.take(server_credential.ssl, [:certfile, :keyfile, :versions]) + ) + } + end + + test "connects insecurely (default options)", %{port: port, credential: credential} do + channel = build(:channel, port: port, host: "localhost", cred: credential) + + assert {:ok, result} = Gun.connect(channel, []) + + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + end + + test "connects insecurely (custom options)", %{port: port, credential: credential} do + channel = build(:channel, port: port, host: "localhost", cred: credential) + + # Ensure that it works + assert {:ok, result} = Gun.connect(channel, transport_opts: [ip: :loopback]) + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + + # Ensure that changing one of the options breaks things + assert {:error, {:down, :badarg}} == + Gun.connect(channel, transport_opts: [ip: "256.0.0.0"]) + end + + test "connects securely (default options)", %{port: port, credential: credential} do + channel = + build(:channel, + port: port, + scheme: "https", + host: "localhost", + cred: credential + ) + + assert {:ok, result} = Gun.connect(channel, tls_opts: channel.cred.ssl) + + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + end + + test "connects securely (custom options)", %{port: port, credential: credential} do + channel = + build(:channel, + port: port, + scheme: "https", + host: "localhost", + cred: credential + ) + + # Ensure that it works + assert {:ok, result} = + Gun.connect(channel, + transport_opts: [certfile: credential.ssl[:certfile], ip: :loopback] + ) + + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + + # Ensure that changing one of the options breaks things + assert {:error, :timeout} == + Gun.connect(channel, + transport_opts: [ + certfile: credential.ssl[:certfile] <> "invalidsuffix", + ip: :loopback + ] + ) + end + end +end diff --git a/test/grpc/integration/codec_test.exs b/test/grpc/integration/codec_test.exs index ef28b96a..31fbf967 100644 --- a/test/grpc/integration/codec_test.exs +++ b/test/grpc/integration/codec_test.exs @@ -20,32 +20,30 @@ defmodule GRPC.Integration.CodecTest do defmodule HelloServer do use GRPC.Server, service: Helloworld.Greeter.Service, - codecs: [GRPC.Codec.Proto, GRPC.Codec.Erlpack] + codecs: [GRPC.Codec.Proto, GRPC.Codec.Erlpack, GRPC.Codec.WebText] def say_hello(req, _stream) do Helloworld.HelloReply.new(message: "Hello, #{req.name}") end end - defmodule HelloErlpackStub do + defmodule HelloStub do use GRPC.Stub, service: Helloworld.Greeter.Service end - test "Says hello over erlpack" do + test "Says hello over erlpack, GRPC-web-text" do run_server(HelloServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") name = "Mairbek" req = Helloworld.HelloRequest.new(name: name) - {:ok, reply} = channel |> HelloErlpackStub.say_hello(req, codec: GRPC.Codec.Erlpack) - assert reply.message == "Hello, #{name}" - - # verify that proto still works - {:ok, reply} = channel |> HelloErlpackStub.say_hello(req, codec: GRPC.Codec.Proto) - assert reply.message == "Hello, #{name}" + for codec <- [GRPC.Codec.Erlpack, GRPC.Codec.WebText, GRPC.Codec.Proto] do + {:ok, reply} = HelloStub.say_hello(channel, req, codec: codec) + assert reply.message == "Hello, #{name}" + end # codec not registered - {:error, reply} = channel |> HelloErlpackStub.say_hello(req, codec: NotRegisteredCodec) + {:error, reply} = HelloStub.say_hello(channel, req, codec: NotRegisteredCodec) assert %GRPC.RPCError{ status: GRPC.Status.unimplemented(), @@ -53,4 +51,23 @@ defmodule GRPC.Integration.CodecTest do } == reply end) end + + test "sets the correct content-type based on codec name" do + run_server(HelloServer, fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + name = "Mairbek" + req = Helloworld.HelloRequest.new(name: name) + + for {expected_content_type, codec} <- [ + {"grpc-web-text", GRPC.Codec.WebText}, + {"grpc+erlpack", GRPC.Codec.Erlpack}, + {"grpc+proto", GRPC.Codec.Proto} + ] do + {:ok, _reply, headers} = + HelloStub.say_hello(channel, req, codec: codec, return_headers: true) + + assert headers[:headers]["content-type"] == "application/#{expected_content_type}" + end + end) + end end diff --git a/test/grpc/integration/connection_test.exs b/test/grpc/integration/connection_test.exs index 94a46186..fe68d005 100644 --- a/test/grpc/integration/connection_test.exs +++ b/test/grpc/integration/connection_test.exs @@ -5,19 +5,11 @@ defmodule GRPC.Integration.ConnectionTest do @key_path Path.expand("./tls/server1.key", :code.priv_dir(:grpc)) @ca_path Path.expand("./tls/ca.pem", :code.priv_dir(:grpc)) - defmodule FeatureServer do - use GRPC.Server, service: Routeguide.RouteGuide.Service - - def get_feature(point, _stream) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") - end - end - test "reconnection works" do server = FeatureServer {:ok, _, port} = GRPC.Server.start(server, 0) point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", adapter_opts: %{retry_timeout: 10}) + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", adapter_opts: [retry_timeout: 10]) assert {:ok, _} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) :ok = GRPC.Server.stop(server) {:ok, _, _} = reconnect_server(server, port) @@ -31,7 +23,7 @@ defmodule GRPC.Integration.ConnectionTest do File.rm(socket_path) {:ok, _, _} = GRPC.Server.start(server, 0, ip: {:local, socket_path}) - {:ok, channel} = GRPC.Stub.connect(socket_path, adapter_opts: %{retry_timeout: 10}) + {:ok, channel} = GRPC.Stub.connect(socket_path, adapter_opts: [retry_timeout: 10]) point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) assert {:ok, _} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) @@ -41,6 +33,8 @@ defmodule GRPC.Integration.ConnectionTest do test "authentication works" do server = FeatureServer + tls_versions = [:"tlsv1.2"] + cred = GRPC.Credential.new( ssl: [ @@ -48,7 +42,8 @@ defmodule GRPC.Integration.ConnectionTest do cacertfile: @ca_path, keyfile: @key_path, verify: :verify_peer, - fail_if_no_peer_cert: true + fail_if_no_peer_cert: true, + versions: tls_versions ] ) @@ -56,7 +51,12 @@ defmodule GRPC.Integration.ConnectionTest do try do point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) - client_cred = GRPC.Credential.new(ssl: [certfile: @cert_path, keyfile: @key_path]) + + client_cred = + GRPC.Credential.new( + ssl: [certfile: @cert_path, keyfile: @key_path, versions: tls_versions] + ) + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", cred: client_cred) assert {:ok, _} = Routeguide.RouteGuide.Stub.get_feature(channel, point) catch diff --git a/test/grpc/integration/endpoint_test.exs b/test/grpc/integration/endpoint_test.exs index f5055bd4..a0015cf1 100644 --- a/test/grpc/integration/endpoint_test.exs +++ b/test/grpc/integration/endpoint_test.exs @@ -13,7 +13,7 @@ defmodule GRPC.Integration.EndpointTest do defmodule HelloEndpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server, level: :info + intercept GRPC.Logger.Server, level: :info, accepted_comparators: [:lt, :eq, :gt] run HelloServer end @@ -51,14 +51,14 @@ defmodule GRPC.Integration.EndpointTest do defmodule FeatureEndpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server + intercept GRPC.Logger.Server, accepted_comparators: [:lt, :eq, :gt] run FeatureServer end defmodule FeatureAndHelloHaltEndpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server + intercept GRPC.Logger.Server, accepted_comparators: [:lt, :eq, :gt] run HelloServer, interceptors: [HelloHaltInterceptor] run FeatureServer end diff --git a/test/grpc/integration/server_test.exs b/test/grpc/integration/server_test.exs index c80f814b..e2455719 100644 --- a/test/grpc/integration/server_test.exs +++ b/test/grpc/integration/server_test.exs @@ -98,6 +98,12 @@ defmodule GRPC.Integration.ServerTest do end end + defmodule HTTP1Server do + def init(req, state) do + {:ok, :cowboy_req.reply(200, %{}, "OK", req), state} + end + end + test "multiple servers works" do run_server([FeatureServer, HelloServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") @@ -111,6 +117,27 @@ defmodule GRPC.Integration.ServerTest do end) end + test "HTTP/1 status handler can be started along a gRPC server" do + status_handler = {"/status", HTTP1Server, []} + + run_server( + [HelloServer], + fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + req = Helloworld.HelloRequest.new(name: "Elixir") + {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) + assert reply.message == "Hello, Elixir" + + {:ok, conn_pid} = :gun.open('localhost', port) + stream_ref = :gun.get(conn_pid, "/status") + + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + end, + 0, + adapter_opts: [status_handler: status_handler] + ) + end + test "returns appropriate error for unary requests" do run_server([HelloErrorServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") diff --git a/test/grpc/integration/service_test.exs b/test/grpc/integration/service_test.exs index 3da896f9..497bf1cd 100644 --- a/test/grpc/integration/service_test.exs +++ b/test/grpc/integration/service_test.exs @@ -132,7 +132,7 @@ defmodule GRPC.Integration.ServiceTest do FeatureServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - stream = channel |> Routeguide.RouteGuide.Stub.async_route_chat() + stream = channel |> Routeguide.RouteGuide.Stub.route_chat() task = Task.async(fn -> @@ -152,7 +152,7 @@ defmodule GRPC.Integration.ServiceTest do notes = Enum.map(result_enum, fn {:ok, note} -> - assert "Reply: " <> msg = note.message + assert "Reply: " <> _msg = note.message if note.message == "Reply: Message 5" do point = Routeguide.Point.new(latitude: 0, longitude: rem(6, 3) + 1) diff --git a/test/grpc/logger/server_test.exs b/test/grpc/logger/server_test.exs new file mode 100644 index 00000000..b6667622 --- /dev/null +++ b/test/grpc/logger/server_test.exs @@ -0,0 +1,77 @@ +defmodule GRPC.Logger.ServerTest do + use ExUnit.Case, async: false + + import ExUnit.CaptureLog + + alias GRPC.Logger.Server + + alias GRPC.Server.Stream + + test "request id is only set if not previously set" do + assert Logger.metadata() == [] + + request_id = to_string(System.monotonic_time()) + stream = %Stream{server: :server, rpc: {1, 2, 3}, request_id: request_id} + + Server.call( + :request, + stream, + fn :request, ^stream -> {:ok, :ok} end, + Server.init(level: :info) + ) + + assert [request_id: request_id] == Logger.metadata() + + stream = %{stream | request_id: nil} + + Server.call( + :request, + stream, + fn :request, ^stream -> {:ok, :ok} end, + Server.init(level: :info) + ) + + assert request_id == Logger.metadata()[:request_id] + end + + test "accepted_comparators filter logs correctly" do + for {configured_level, accepted_comparators, should_log} <- + [ + {:error, [:lt], false}, + {:error, [:eq], false}, + {:error, [:gt], true}, + {:debug, [:eq], false}, + {:debug, [:eq, :gt], false}, + {:info, [:lt, :eq], true} + ] do + server_name = :"server_#{System.unique_integer()}" + + logger_level = Logger.level() + assert logger_level == :info + + logs = + capture_log(fn -> + stream = %Stream{server: server_name, rpc: {1, 2, 3}, request_id: "1234"} + + Server.call( + :request, + stream, + fn :request, ^stream -> {:ok, :ok} end, + Server.init( + level: configured_level, + accepted_comparators: accepted_comparators + ) + ) + end) + + if should_log do + assert Regex.match?( + ~r/\[#{configured_level}\]\s+Handled by #{inspect(server_name)}/, + logs + ) + else + assert logs == "" + end + end + end +end diff --git a/test/grpc/message/protobuf_test.exs b/test/grpc/message/protobuf_test.exs deleted file mode 100644 index 3e1b8dae..00000000 --- a/test/grpc/message/protobuf_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -defmodule GRPC.Message.ProtobufTest do - use ExUnit.Case, async: true - - defmodule Helloworld.HelloRequest do - use Protobuf - - defstruct [:name] - field(:name, 1, optional: true, type: :string) - end - - defmodule Helloworld.HelloReply do - use Protobuf - - defstruct [:message] - field(:message, 1, optional: true, type: :string) - end - - test "encode/2 works for matched arguments" do - request = Helloworld.HelloRequest.new(name: "elixir") - - assert <<10, 6, 101, 108, 105, 120, 105, 114>> = - GRPC.Message.Protobuf.encode(Helloworld.HelloRequest, request) - end - - test "decode/2 works" do - msg = <<10, 6, 101, 108, 105, 120, 105, 114>> - request = Helloworld.HelloRequest.new(name: "elixir") - assert ^request = GRPC.Message.Protobuf.decode(Helloworld.HelloRequest, msg) - end - - test "decode/2 returns wrong result for mismatched arguments" do - # encoded HelloRequest - msg = <<10, 6, 101, 108, 105, 120, 105, 114>> - request = Helloworld.HelloReply.new(message: "elixir") - assert ^request = GRPC.Message.Protobuf.decode(Helloworld.HelloReply, msg) - end -end diff --git a/test/grpc/message_test.exs b/test/grpc/message_test.exs index 765e76f3..04b29f32 100644 --- a/test/grpc/message_test.exs +++ b/test/grpc/message_test.exs @@ -15,4 +15,23 @@ defmodule GRPC.MessageTest do assert {:ok, message} == GRPC.Message.from_data(%{compressor: GRPC.Compressor.Gzip}, data) end + + test "iodata can be passed to and returned from `to_data/2`" do + message = List.duplicate("foo", 100) + + assert {:ok, data, 32} = + GRPC.Message.to_data(message, iolist: true, compressor: GRPC.Compressor.Gzip) + + assert is_list(data) + binary = IO.iodata_to_binary(data) + + assert {:ok, IO.iodata_to_binary(message)} == + GRPC.Message.from_data(%{compressor: GRPC.Compressor.Gzip}, binary) + end + + test "to_data/2 invokes codec.pack_for_channel on the gRPC body if codec implements it" do + message = "web-text" + assert {:ok, base64_payload, _} = GRPC.Message.to_data(message, %{codec: GRPC.Codec.WebText}) + assert message == GRPC.Message.from_data(Base.decode64!(base64_payload)) + end end diff --git a/test/grpc/server/supervisor_test.exs b/test/grpc/server/supervisor_test.exs new file mode 100644 index 00000000..495b6b0d --- /dev/null +++ b/test/grpc/server/supervisor_test.exs @@ -0,0 +1,44 @@ +defmodule GRPC.Server.SupervisorTest do + use ExUnit.Case, async: false + + alias GRPC.Server.Supervisor + + defmodule MockEndpoint do + def __meta__(_), do: [FeatureServer] + end + + describe "init/1" do + test "does not start children if opts sets false" do + assert {:ok, {%{strategy: :one_for_one}, []}} = + Supervisor.init(endpoint: MockEndpoint, port: 1234, start_server: false) + end + + test "fails if a tuple is passed" do + assert_raise ArgumentError, + "passing a tuple as configuration for GRPC.Server.Supervisor is no longer supported. See the documentation for more information on how to configure.", + fn -> + Supervisor.init({MockEndpoint, 1234}) + end + + assert_raise ArgumentError, + "passing a tuple as configuration for GRPC.Server.Supervisor is no longer supported. See the documentation for more information on how to configure.", + fn -> + Supervisor.init({MockEndpoint, 1234, start_server: true}) + end + end + + test "starts children if opts sets true" do + endpoint_str = "#{Macro.to_string(MockEndpoint)}" + + assert {:ok, + {%{strategy: :one_for_one}, + [ + %{ + id: {:ranch_listener_sup, ^endpoint_str}, + start: _, + type: :supervisor + } + ]}} = Supervisor.init(endpoint: MockEndpoint, port: 1234, start_server: true) + end + end +end diff --git a/test/grpc/transport/http2_test.exs b/test/grpc/transport/http2_test.exs index b8fcd169..07ca5fe6 100644 --- a/test/grpc/transport/http2_test.exs +++ b/test/grpc/transport/http2_test.exs @@ -1,10 +1,12 @@ defmodule GRPC.Transport.HTTP2Test do use ExUnit.Case, async: true - alias GRPC.Channel + alias GRPC.{Channel, Status} alias GRPC.Transport.HTTP2 - @channel %Channel{scheme: "http", host: "grpc.io"} alias GRPC.Client.Stream + alias GRPC.Server.Stream, as: ServerStream + + @channel %Channel{scheme: "http", host: "grpc.io"} defp assert_header({key, _v} = pair, headers) do assert pair == Enum.find(headers, nil, fn {k, _v} -> if k == key, do: true end) @@ -99,4 +101,28 @@ defmodule GRPC.Transport.HTTP2Test do assert {_, "application/grpc+custom-codec"} = Enum.find(headers, fn {key, _} -> key == "content-type" end) end + + test "server_headers/3 sets content-type based on the codec name" do + for {expected_content_type, codec} <- [ + {"grpc-web-text", GRPC.Codec.WebText}, + {"grpc+erlpack", GRPC.Codec.Erlpack} + ] do + stream = %ServerStream{codec: codec} + + assert %{"content-type" => "application/" <> ^expected_content_type} = + HTTP2.server_headers(stream) + end + end + + test "decode_headers/2 url decodes grpc-message" do + trailers = HTTP2.server_trailers(Status.unknown(), "Unknown error") + assert %{"grpc-message" => "Unknown error"} = HTTP2.decode_headers(trailers) + end + + test "server_trailers/3 sets url encoded grpc-message" do + assert %{"grpc-message" => "Ok"} = HTTP2.server_trailers(Status.ok(), "Ok") + + assert %{"grpc-message" => "Unknown%20error"} = + HTTP2.server_trailers(Status.unknown(), "Unknown error") + end end diff --git a/test/support/factory.ex b/test/support/factory.ex new file mode 100644 index 00000000..fceaae03 --- /dev/null +++ b/test/support/factory.ex @@ -0,0 +1,52 @@ +defmodule GRPC.Factory do + @moduledoc false + + alias GRPC.Channel + alias GRPC.Credential + + @cert_path Path.expand("./tls/server1.pem", :code.priv_dir(:grpc)) + @key_path Path.expand("./tls/server1.key", :code.priv_dir(:grpc)) + @ca_path Path.expand("./tls/ca.pem", :code.priv_dir(:grpc)) + + def build(resource, attrs \\ %{}) do + name = :"#{resource}_factory" + + data = + if function_exported?(__MODULE__, name, 1) do + apply(__MODULE__, name, [attrs]) + else + apply(__MODULE__, name, []) + end + + Map.merge(data, Map.new(attrs)) + end + + def channel_factory do + %Channel{ + host: "localhost", + port: 1337, + scheme: "http", + cred: build(:credential), + adapter: GRPC.Client.Adapters.Gun, + adapter_payload: %{}, + codec: GRPC.Codec.Proto, + interceptors: [], + compressor: nil, + accepted_compressors: [], + headers: [] + } + end + + def credential_factory do + %Credential{ + ssl: [ + certfile: @cert_path, + cacertfile: @ca_path, + keyfile: @key_path, + verify: :verify_peer, + fail_if_no_peer_cert: true, + versions: [:"tlsv1.2"] + ] + } + end +end diff --git a/test/support/feature_server.ex b/test/support/feature_server.ex new file mode 100644 index 00000000..3951ea8b --- /dev/null +++ b/test/support/feature_server.ex @@ -0,0 +1,7 @@ +defmodule FeatureServer do + use GRPC.Server, service: Routeguide.RouteGuide.Service + + def get_feature(point, _stream) do + Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + end +end diff --git a/test/support/helloworld.pb.ex b/test/support/helloworld.pb.ex index f8758c31..1da2351c 100644 --- a/test/support/helloworld.pb.ex +++ b/test/support/helloworld.pb.ex @@ -1,52 +1,35 @@ defmodule Helloworld.HelloRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - name: String.t() - } - defstruct [:name] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :name, 1, type: :string end defmodule Helloworld.HelloReply do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - message: String.t() - } - defstruct [:message] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :message, 1, type: :string end defmodule Helloworld.HeaderRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{} - defstruct [] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 end defmodule Helloworld.HeaderReply do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - authorization: String.t() - } - defstruct [:authorization] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :authorization, 1, type: :string end defmodule Helloworld.Greeter.Service do @moduledoc false - use GRPC.Service, name: "helloworld.Greeter" + use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.10.0" rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply + rpc :CheckHeaders, Helloworld.HeaderRequest, Helloworld.HeaderReply end diff --git a/test/support/integration_test_case.ex b/test/support/integration_test_case.ex index df1daaa3..24467ca2 100644 --- a/test/support/integration_test_case.ex +++ b/test/support/integration_test_case.ex @@ -9,8 +9,8 @@ defmodule GRPC.Integration.TestCase do end end - def run_server(servers, func, port \\ 0) do - {:ok, _pid, port} = GRPC.Server.start(servers, port) + def run_server(servers, func, port \\ 0, opts \\ []) do + {:ok, _pid, port} = GRPC.Server.start(servers, port, opts) try do func.(port) diff --git a/test/support/route_guide.pb.ex b/test/support/route_guide.pb.ex index 6ad6f559..66a64c91 100644 --- a/test/support/route_guide.pb.ex +++ b/test/support/route_guide.pb.ex @@ -1,82 +1,59 @@ defmodule Routeguide.Point do - use Protobuf + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - latitude: integer, - longitude: integer - } - defstruct [:latitude, :longitude] - - field :latitude, 1, optional: true, type: :int32 - field :longitude, 2, optional: true, type: :int32 + field :latitude, 1, type: :int32 + field :longitude, 2, type: :int32 end defmodule Routeguide.Rectangle do - use Protobuf - - @type t :: %__MODULE__{ - lo: Routeguide.Point.t(), - hi: Routeguide.Point.t() - } - defstruct [:lo, :hi] + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :lo, 1, optional: true, type: Routeguide.Point - field :hi, 2, optional: true, type: Routeguide.Point + field :lo, 1, type: Routeguide.Point + field :hi, 2, type: Routeguide.Point end defmodule Routeguide.Feature do - use Protobuf + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - name: String.t(), - location: Routeguide.Point.t() - } - defstruct [:name, :location] - - field :name, 1, optional: true, type: :string - field :location, 2, optional: true, type: Routeguide.Point + field :name, 1, type: :string + field :location, 2, type: Routeguide.Point end defmodule Routeguide.RouteNote do - use Protobuf - - @type t :: %__MODULE__{ - location: Routeguide.Point.t(), - message: String.t() - } - defstruct [:location, :message] + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :location, 1, optional: true, type: Routeguide.Point - field :message, 2, optional: true, type: :string + field :location, 1, type: Routeguide.Point + field :message, 2, type: :string end defmodule Routeguide.RouteSummary do - use Protobuf + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - point_count: integer, - feature_count: integer, - distance: integer, - elapsed_time: integer - } - defstruct [:point_count, :feature_count, :distance, :elapsed_time] - - field :point_count, 1, optional: true, type: :int32 - field :feature_count, 2, optional: true, type: :int32 - field :distance, 3, optional: true, type: :int32 - field :elapsed_time, 4, optional: true, type: :int32 + field :point_count, 1, type: :int32, json_name: "pointCount" + field :feature_count, 2, type: :int32, json_name: "featureCount" + field :distance, 3, type: :int32 + field :elapsed_time, 4, type: :int32, json_name: "elapsedTime" end defmodule Routeguide.RouteGuide.Service do - use GRPC.Service, name: "routeguide.RouteGuide" + @moduledoc false + use GRPC.Service, name: "routeguide.RouteGuide", protoc_gen_elixir_version: "0.10.0" rpc :GetFeature, Routeguide.Point, Routeguide.Feature + rpc :ListFeatures, Routeguide.Rectangle, stream(Routeguide.Feature) + rpc :RecordRoute, stream(Routeguide.Point), Routeguide.RouteSummary + rpc :RouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote) - rpc :AsyncRouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote) end defmodule Routeguide.RouteGuide.Stub do + @moduledoc false use GRPC.Stub, service: Routeguide.RouteGuide.Service end diff --git a/test/support/test_adapter.exs b/test/support/test_adapter.exs index 38453434..3f5d84d0 100644 --- a/test/support/test_adapter.exs +++ b/test/support/test_adapter.exs @@ -1,8 +1,15 @@ defmodule GRPC.Test.ClientAdapter do + @behaviour GRPC.Client.Adapter + def connect(channel, _opts), do: {:ok, channel} + def disconnect(channel), do: {:ok, channel} + def send_request(stream, _message, _opts), do: stream + def receive_data(_stream, _opts), do: {:ok, nil} end defmodule GRPC.Test.ServerAdapter do + @behaviour GRPC.Server.Adapter + def start(s, h, p, opts) do {s, h, p, opts} end diff --git a/test/test_helper.exs b/test/test_helper.exs index 4c5aef46..805a2a64 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,10 @@ Code.require_file("./support/test_adapter.exs", __DIR__) -ExUnit.start() +codecs = [ + GRPC.Codec.Erlpack, + GRPC.Codec.WebText, + GRPC.Codec.Proto +] + +Enum.each(codecs, &Code.ensure_loaded/1) +ExUnit.start(capture_log: true)