From 26daa75590614962014f652f113b6d43446192a6 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Date: Wed, 24 Aug 2022 09:12:57 -0300 Subject: [PATCH 01/82] remove gun specific code from stub to improve adapter flexilibity --- lib/grpc/client/adapter.ex | 15 +-- lib/grpc/client/adapters/gun.ex | 182 ++++++++++++++++++++++++++++- lib/grpc/client/stream.ex | 9 +- lib/grpc/stub.ex | 198 +------------------------------- test/support/test_adapter.exs | 3 +- 5 files changed, 195 insertions(+), 212 deletions(-) diff --git a/lib/grpc/client/adapter.ex b/lib/grpc/client/adapter.ex index f623be62..62b995e4 100644 --- a/lib/grpc/client/adapter.ex +++ b/lib/grpc/client/adapter.ex @@ -17,13 +17,10 @@ defmodule GRPC.Client.Adapter do @callback send_request(stream :: Stream.t(), contents :: binary(), opts :: keyword()) :: Stream.t() - @callback recv_headers(stream :: map(), headers :: map(), opts :: keyword()) :: - {:ok, %{String.t() => String.t()}, fin()} | {:error, GRPC.RPCError.t()} - - @callback recv_data_or_trailers( - stream :: map(), - trailers_or_metadata :: map(), - opts :: keyword() - ) :: - {:data, binary()} | {:trailers, binary()} | {:error, GRPC.RPCError.t()} + @callback receive_data(stream :: Stream.t(), opts :: keyword()) :: + {:ok, struct()} + | {:ok, struct(), map()} + | {:ok, Enumerable.t()} + | {:ok, Enumerable.t(), map()} + | {:error, any()} end diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index 088abeed..bb1b2acf 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -135,7 +135,42 @@ defmodule GRPC.Client.Adapters.Gun do end @impl true - def recv_headers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do + def receive_data( + %{server_stream: true, channel: %{adapter_payload: adapter_payload}, payload: payload} = + stream, + opts + ) do + with {:ok, headers, is_fin} <- recv_headers(adapter_payload, payload, opts) do + case is_fin do + :fin -> [] + :nofin -> response_stream(stream, opts) + end + |> then(&{:ok, &1}) + |> maybe_return_headers(opts[:return_headers], %{headers: headers}) + end + end + + def receive_data( + %{payload: payload, channel: %{adapter_payload: adapter_payload}} = stream, + opts + ) do + with {:ok, headers, _is_fin} <- recv_headers(adapter_payload, payload, opts), + {:ok, body, trailers} <- recv_body(adapter_payload, payload, opts) do + stream + |> parse_response(headers, body, trailers) + |> maybe_return_headers(opts[:return_headers], %{headers: headers, trailers: trailers}) + end + end + + defp maybe_return_headers({status, response}, true = _return_headers?, headers) do + {status, response, headers} + end + + defp maybe_return_headers({status, response}, _return_headers?, _headers) do + {status, response} + 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} @@ -152,8 +187,7 @@ defmodule GRPC.Client.Adapters.Gun do end end - @impl true - 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 @@ -287,4 +321,146 @@ defmodule GRPC.Client.Adapters.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( + %{ + 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 + } = s + ) do + case 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_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 + + body = + if function_exported?(codec, :unpack_from_channel, 1) do + codec.unpack_from_channel(body) + else + body + 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 end diff --git a/lib/grpc/client/stream.ex b/lib/grpc/client/stream.ex index d11433e0..e52842f4 100644 --- a/lib/grpc/client/stream.ex +++ b/lib/grpc/client/stream.ex @@ -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 @@ -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/stub.ex b/lib/grpc/stub.ex index 6dab7dd7..9f17c96f 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -406,203 +406,7 @@ defmodule GRPC.Stub do 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)} - - err -> - err - 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 - - body = - if function_exported?(codec, :unpack_from_channel, 1) do - codec.unpack_from_channel(body) - else - body - 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 + interface[:receive_data].(stream, opts) end @valid_req_opts [ diff --git a/test/support/test_adapter.exs b/test/support/test_adapter.exs index 63cc7683..3f5d84d0 100644 --- a/test/support/test_adapter.exs +++ b/test/support/test_adapter.exs @@ -4,8 +4,7 @@ defmodule GRPC.Test.ClientAdapter do def connect(channel, _opts), do: {:ok, channel} def disconnect(channel), do: {:ok, channel} def send_request(stream, _message, _opts), do: stream - def recv_headers(_stream, _, _opts), do: {:ok, %{}, :fin} - def recv_data_or_trailers(_stream, _, _opts), do: {:data, ""} + def receive_data(_stream, _opts), do: {:ok, nil} end defmodule GRPC.Test.ServerAdapter do From d8ed8fa29b56722d957dd6b08b52097524d78ab6 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 3 Sep 2022 15:16:28 -0300 Subject: [PATCH 02/82] remove maybe_return_headers in gun adapter for clairty --- lib/grpc/client/adapter.ex | 9 ++++----- lib/grpc/client/adapters/gun.ex | 35 +++++++++++++++------------------ lib/grpc/stub.ex | 7 +++++-- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/grpc/client/adapter.ex b/lib/grpc/client/adapter.ex index 62b995e4..b1f684a6 100644 --- a/lib/grpc/client/adapter.ex +++ b/lib/grpc/client/adapter.ex @@ -17,10 +17,9 @@ defmodule GRPC.Client.Adapter do @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()) :: - {:ok, struct()} - | {:ok, struct(), map()} - | {:ok, Enumerable.t()} - | {:ok, Enumerable.t(), map()} - | {:error, any()} + GRPC.Stub.receive_data_return() | {:error, any()} end diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index bb1b2acf..99bc5af7 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -140,13 +140,13 @@ defmodule GRPC.Client.Adapters.Gun do stream, opts ) do - with {:ok, headers, is_fin} <- recv_headers(adapter_payload, payload, opts) do - case is_fin do - :fin -> [] - :nofin -> response_stream(stream, opts) + with {:ok, headers, is_fin} <- recv_headers(adapter_payload, payload, opts), + response <- response_stream(is_fin, stream, opts) do + if(opts[:return_headers]) do + {:ok, response, %{headers: headers}} + else + {:ok, response} end - |> then(&{:ok, &1}) - |> maybe_return_headers(opts[:return_headers], %{headers: headers}) end end @@ -155,21 +155,16 @@ defmodule GRPC.Client.Adapters.Gun do opts ) do with {:ok, headers, _is_fin} <- recv_headers(adapter_payload, payload, opts), - {:ok, body, trailers} <- recv_body(adapter_payload, payload, opts) do - stream - |> parse_response(headers, body, trailers) - |> maybe_return_headers(opts[:return_headers], %{headers: headers, trailers: trailers}) + {: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 maybe_return_headers({status, response}, true = _return_headers?, headers) do - {status, response, headers} - end - - defp maybe_return_headers({status, response}, _return_headers?, _headers) do - {status, response} - 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} -> @@ -339,7 +334,10 @@ defmodule GRPC.Client.Adapters.Gun do end end + defp response_stream(:fin, _stream, _opts), do: [] + defp response_stream( + :nofin, %{ channel: %{adapter_payload: ap}, response_mod: res_mod, @@ -406,7 +404,6 @@ defmodule GRPC.Client.Adapters.Gun do 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) diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 9f17c96f..db050c0a 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -44,13 +44,16 @@ defmodule GRPC.Stub do # 10 seconds @default_timeout 10000 - @type rpc_return :: + @type receive_data_return :: {:ok, struct()} | {:ok, struct(), map()} - | GRPC.Client.Stream.t() | {:ok, Enumerable.t()} | {:ok, Enumerable.t(), map()} + + @type rpc_return :: + GRPC.Client.Stream.t() | {:error, GRPC.RPCError.t()} + | receive_data_return require Logger From 3be870e8277f98eaadf800900ca82464a923b53a Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 3 Sep 2022 15:27:28 -0300 Subject: [PATCH 03/82] refactor parse_response from gun adapter to improve readability --- lib/grpc/client/adapters/gun.ex | 47 +++++++++++++-------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index 99bc5af7..199e41f7 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -420,34 +420,11 @@ defmodule GRPC.Client.Adapters.Gun do 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 - - body = - if function_exported?(codec, :unpack_from_channel, 1) do - codec.unpack_from_channel(body) - else - body - end - - case GRPC.Message.from_data(%{compressor: compressor}, body) do - {:ok, msg} -> - {:ok, codec.decode(msg, res_mod)} - - err -> - err - end - - error -> - error + 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 @@ -460,4 +437,18 @@ defmodule GRPC.Client.Adapters.Gun do {: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 From a84e576cbc67309c2798177c23479a2f2b7d74b8 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 3 Sep 2022 15:49:25 -0300 Subject: [PATCH 04/82] refactor read_stream from gun adapter to improve readability --- lib/grpc/client/adapters/gun.ex | 42 ++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index 199e41f7..6bb5b0d1 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -372,30 +372,17 @@ defmodule GRPC.Client.Adapters.Gun do buffer: buffer, need_more: true, opts: opts - } = s + } = stream ) do case 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) + stream + |> Map.put(:need_more, false) + |> Map.put(:buffer, buffer <> data) + |> read_stream() {: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 + update_stream_with_trailers(stream, trailers, opts[:return_headers]) error = {:error, _} -> {error, %{buffer: <<>>, fin: true, fin_resp: nil}} @@ -428,6 +415,23 @@ defmodule GRPC.Client.Adapters.Gun do 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"]) From 05655c522cbda6c6e2b313c3e338fbc2830dfb2e Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 6 Oct 2022 09:31:18 -0300 Subject: [PATCH 05/82] remove unecessary response parse from inside with condition --- lib/grpc/client/adapters/gun.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index 6bb5b0d1..f8d1785c 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -140,8 +140,8 @@ defmodule GRPC.Client.Adapters.Gun do stream, opts ) do - with {:ok, headers, is_fin} <- recv_headers(adapter_payload, payload, opts), - response <- response_stream(is_fin, stream, opts) do + 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 From b3ba6cca1f4578544291f6a9dc6dc8b1170d5279 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 3 Sep 2022 18:11:57 -0300 Subject: [PATCH 06/82] add mint adapter and implement draft for open connection --- examples/helloworld/mix.lock | 2 ++ examples/helloworld/priv/client.exs | 2 +- lib/grpc/client/adapters/mint.ex | 0 lib/grpc/client/adapters/mint/connection_process.ex | 0 4 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 lib/grpc/client/adapters/mint.ex create mode 100644 lib/grpc/client/adapters/mint/connection_process.ex diff --git a/examples/helloworld/mix.lock b/examples/helloworld/mix.lock index f96a70d1..3cd58c87 100644 --- a/examples/helloworld/mix.lock +++ b/examples/helloworld/mix.lock @@ -5,6 +5,8 @@ "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"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "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 dc6bea5d..3fc7eb26 100644 --- a/examples/helloworld/priv/client.exs +++ b/examples/helloworld/priv/client.exs @@ -1,4 +1,4 @@ -{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) +{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client], adapter: GRPC.Client.Adapters.Mint) {:ok, reply} = channel diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex new file mode 100644 index 00000000..e69de29b diff --git a/lib/grpc/client/adapters/mint/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process.ex new file mode 100644 index 00000000..e69de29b From 358c4d6fae35b5e9ac62b6ef47611d3a8e2600de Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 3 Sep 2022 18:12:31 -0300 Subject: [PATCH 07/82] add mint adapter and implement draft for open connection --- lib/grpc/client/adapters/mint.ex | 42 +++++++++++ .../adapters/mint/connection_process.ex | 75 +++++++++++++++++++ lib/grpc/credential.ex | 2 +- mix.exs | 1 + 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index e69de29b..8dea5bd4 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -0,0 +1,42 @@ +defmodule GRPC.Client.Adapters.Mint do + @moduledoc """ + A client adapter using mint + """ + + alias GRPC.{Channel, Credential} + alias __MODULE__.ConnectionProcess + + @behaviour GRPC.Client.Adapter + + @default_connect_opts [protocols: [:http2]] + @default_transport_opts [timeout: :infinity] + + @impl true + def connect(%{host: host, port: port} = channel, opts \\ []) do + opts = Keyword.merge(@default_connect_opts, connect_opts(channel, opts)) + pid = + channel + |>mint_scheme() + |>ConnectionProcess.start_link(host, port, opts) + + {:ok, Map.put(channel, :adapter_payload, %{conn_pid: pid})} + end + + defp connect_opts(%Channel{scheme: "https"} = channel, opts) do + %Credential{ssl: ssl} = Map.get(channel, :cred, %Credential{}) + transport_opts = + opts + |> Keyword.get(:transport_opts, []) + |> Keyword.merge(ssl) + + [transport_opts: Keyword.merge(@default_transport_opts, transport_opts)] + end + + defp connect_opts(_channel, opts) do + transport_opts = Keyword.get(opts, :transport_opts, []) + [transport_opts: Keyword.merge(@default_transport_opts, transport_opts)] + end + + defp mint_scheme(%Channel{scheme: "https"} = _channel), do: :https + defp mint_scheme(_channel), do: :http +end \ No newline at end of file diff --git a/lib/grpc/client/adapters/mint/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process.ex index e69de29b..6906ec9a 100644 --- a/lib/grpc/client/adapters/mint/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process.ex @@ -0,0 +1,75 @@ +defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do + use GenServer + + require Logger + + defstruct [:conn, requests: %{}] + + def start_link(scheme, host, port, opts \\ []) do + GenServer.start_link(__MODULE__, {scheme, host, port, opts}) + end + + def request(pid, method, path, headers, body) do + GenServer.call(pid, {:request, method, path, headers, body}) + end + + ## Callbacks + + @impl true + def init({scheme, host, port, opts}) do + case Mint.HTTP.connect(scheme, host, port, opts) do + {:ok, conn} -> + state = %__MODULE__{conn: conn} + {:ok, state} |> IO.inspect + + {:error, reason} -> + {:stop, reason} + end + end + + @impl true + def handle_call({:request, method, path, headers, body}, from, state) do + case Mint.HTTP.request(state.conn, method, path, headers, body) do + {:ok, conn, request_ref} -> + state = put_in(state.conn, conn) + state = put_in(state.requests[request_ref], %{from: from, response: %{}}) + {:noreply, state} + + {:error, conn, reason} -> + state = put_in(state.conn, conn) + {:reply, {:error, reason}, state} + end + end + + @impl true + def handle_info(message, state) do + case Mint.HTTP.stream(state.conn, message) do + :unknown -> + Logger.debug(fn -> "Received unknown message: " <> inspect(message) end) + {:noreply, state} + + {:ok, conn, responses} -> + state = put_in(state.conn, conn) + state = Enum.reduce(responses, state, &process_response/2) + {:noreply, state} + end + end + + defp process_response({:status, request_ref, status}, state) do + put_in(state.requests[request_ref].response[:status], status) + end + + defp process_response({:headers, request_ref, headers}, state) do + put_in(state.requests[request_ref].response[:headers], headers) + end + + defp process_response({:data, request_ref, new_data}, state) do + update_in(state.requests[request_ref].response[:data], fn data -> (data || "") <> new_data end) + end + + defp process_response({:done, request_ref}, state) do + {%{response: response, from: from}, state} = pop_in(state.requests[request_ref]) + GenServer.reply(from, {:ok, response}) + state + end +end \ No newline at end of file diff --git a/lib/grpc/credential.ex b/lib/grpc/credential.ex index 1df78ea7..c48664c6 100644 --- a/lib/grpc/credential.ex +++ b/lib/grpc/credential.ex @@ -17,7 +17,7 @@ defmodule GRPC.Credential do """ @type t :: %__MODULE__{ssl: [:ssl.tls_option()]} - defstruct [:ssl] + defstruct [ssl: []] @doc """ Creates credential. diff --git a/mix.exs b/mix.exs index af6eed0b..05b93764 100644 --- a/mix.exs +++ b/mix.exs @@ -43,6 +43,7 @@ defmodule GRPC.Mixfile do # 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}, + {:mint, "~> 1.4.2"}, {:cowlib, "~> 2.11"}, {:protobuf, "~> 0.10", only: [:dev, :test]}, {:ex_doc, "~> 0.28.0", only: :dev}, From c6e726e1d04b190a6046dd697f5393511139d072 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 4 Sep 2022 21:42:49 -0300 Subject: [PATCH 08/82] add simple request and response handlers --- lib/grpc/client/adapters/mint.ex | 75 +++++++++++++++++-- .../adapters/mint/connection_process.ex | 16 +++- lib/grpc/credential.ex | 2 +- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 8dea5bd4..43886cba 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -4,6 +4,7 @@ defmodule GRPC.Client.Adapters.Mint do """ alias GRPC.{Channel, Credential} + alias GRPC.Client.Stream alias __MODULE__.ConnectionProcess @behaviour GRPC.Client.Adapter @@ -14,16 +15,62 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def connect(%{host: host, port: port} = channel, opts \\ []) do opts = Keyword.merge(@default_connect_opts, connect_opts(channel, opts)) - pid = - channel - |>mint_scheme() - |>ConnectionProcess.start_link(host, port, opts) - {:ok, Map.put(channel, :adapter_payload, %{conn_pid: pid})} + channel + |> mint_scheme() + |> ConnectionProcess.start_link(host, port, opts) + |> case do + {:ok, pid} -> {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} + # TODO add proper error handling + _error -> {:ok, %{channel | adapter_payload: %{conn_pid: nil}}} + end end - + + @impl true + def disconnect(%{adapter_payload: %{conn_pid: pid}} = channel) + when is_pid(pid) do + ConnectionProcess.disconnect(pid) + {:ok, %{channel | adapter_payload: %{conn_pid: nil}}} + end + + def disconnect(%{adapter_payload: %{conn_pid: nil}} = channel) do + {:ok, channel} + end + + @impl true + def send_request( + %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, + message, + opts + ) + when is_pid(pid) do + headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + {:ok, data, _} = GRPC.Message.to_data(message, opts) + {:ok, response} = ConnectionProcess.request(pid, "POST", path, headers, data) + Stream.put_payload(stream, :response, response) + end + + @impl true + def receive_data( + %{ + response_mod: res_mod, + codec: codec, + accepted_compressors: accepted_compressors, + payload: %{response: response} + } = stream, + opts + ) do + with %{data: body, headers: headers} <- response, + 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 connect_opts(%Channel{scheme: "https"} = channel, opts) do %Credential{ssl: ssl} = Map.get(channel, :cred, %Credential{}) + transport_opts = opts |> Keyword.get(:transport_opts, []) @@ -39,4 +86,18 @@ defmodule GRPC.Client.Adapters.Mint do defp mint_scheme(%Channel{scheme: "https"} = _channel), do: :https defp mint_scheme(_channel), do: :http -end \ No newline at end of file + + 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/adapters/mint/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process.ex index 6906ec9a..5bdc03e8 100644 --- a/lib/grpc/client/adapters/mint/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process.ex @@ -13,6 +13,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.call(pid, {:request, method, path, headers, body}) end + def disconnect(pid) do + GenServer.call(pid, {:disconnect, :brutal}) + end + ## Callbacks @impl true @@ -20,14 +24,22 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do case Mint.HTTP.connect(scheme, host, port, opts) do {:ok, conn} -> state = %__MODULE__{conn: conn} - {:ok, state} |> IO.inspect + {:ok, state} {:error, reason} -> + # TODO check what's better: add to state map if connection is alive? + # TODO Or simply stop the process and handle the error on caller? {:stop, reason} end end @impl true + def handle_call({:disconnect, :brutal}, _from, state) do + # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests + Mint.HTTP.close(state.conn) + {:stop, :normal, :ok, state} + end + def handle_call({:request, method, path, headers, body}, from, state) do case Mint.HTTP.request(state.conn, method, path, headers, body) do {:ok, conn, request_ref} -> @@ -72,4 +84,4 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.reply(from, {:ok, response}) state end -end \ No newline at end of file +end diff --git a/lib/grpc/credential.ex b/lib/grpc/credential.ex index c48664c6..db15d368 100644 --- a/lib/grpc/credential.ex +++ b/lib/grpc/credential.ex @@ -17,7 +17,7 @@ defmodule GRPC.Credential do """ @type t :: %__MODULE__{ssl: [:ssl.tls_option()]} - defstruct [ssl: []] + defstruct ssl: [] @doc """ Creates credential. From f0d64c50c91e35cf879c71838046cb537cece848 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Mon, 5 Sep 2022 18:13:04 -0300 Subject: [PATCH 09/82] add server stream connection handling --- examples/route_guide/mix.lock | 3 + lib/grpc/client/adapters/mint.ex | 78 +++++++++++++++++-- .../adapters/mint/connection_process.ex | 56 +++++++++++-- 3 files changed, 123 insertions(+), 14 deletions(-) diff --git a/examples/route_guide/mix.lock b/examples/route_guide/mix.lock index 3d14fba0..cc5b6e12 100644 --- a/examples/route_guide/mix.lock +++ b/examples/route_guide/mix.lock @@ -1,10 +1,13 @@ %{ + "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, "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"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "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/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 43886cba..735a9023 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -4,7 +4,6 @@ defmodule GRPC.Client.Adapters.Mint do """ alias GRPC.{Channel, Credential} - alias GRPC.Client.Stream alias __MODULE__.ConnectionProcess @behaviour GRPC.Client.Adapter @@ -39,26 +38,38 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def send_request( - %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, + %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path, server_stream: stream?} = + stream, message, opts ) when is_pid(pid) do headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) {:ok, data, _} = GRPC.Message.to_data(message, opts) - {:ok, response} = ConnectionProcess.request(pid, "POST", path, headers, data) - Stream.put_payload(stream, :response, response) + {:ok, response} = ConnectionProcess.request(pid, "POST", path, headers, data, stream?) + GRPC.Client.Stream.put_payload(stream, :response, response) end @impl true + def receive_data(%{server_stream: true, payload: %{response: response}} = stream, opts) do + with {%{headers: headers}, request_ref} <- response, + stream_response <- build_response_stream(stream, request_ref) do + if(opts[:return_headers]) do + {:ok, stream_response, %{headers: headers}} + else + {:ok, stream_response} + end + end + end + def receive_data( %{ response_mod: res_mod, codec: codec, accepted_compressors: accepted_compressors, payload: %{response: response} - } = stream, - opts + } = _stream, + _opts ) do with %{data: body, headers: headers} <- response, compressor <- get_compressor(headers, accepted_compressors), @@ -100,4 +111,59 @@ defmodule GRPC.Client.Adapters.Mint do body end end + + defp build_response_stream(grpc_stream, request_ref) do + state = %{ + buffer: <<>>, + done: false, + request_ref: request_ref, + grpc_stream: grpc_stream + } + + Stream.unfold(state, fn s -> process_stream(s) end) + end + + defp process_stream(%{buffer: <<>>, done: true}) do + nil + end + + defp process_stream(%{done: true} = state) do + parse_message(state, "", true) + end + + defp process_stream( + %{request_ref: ref, grpc_stream: %{channel: %{adapter_payload: %{conn_pid: pid}}}} = + state + ) do + case ConnectionProcess.process_stream_data(pid, ref) do + {nil, false = _done?} -> + Process.sleep(500) + process_stream(state) + + {nil = _data, true = _done?} -> + parse_message(state, "", true) + + {data, done?} -> + parse_message(state, data, done?) + end + end + + defp parse_message( + %{buffer: buffer, grpc_stream: %{response_mod: res_mod, codec: codec}} = state, + data, + done? + ) do + case GRPC.Message.get_message(buffer <> data) do + {{_, message}, rest} -> + reply = codec.decode(message, res_mod) + new_state = %{state | buffer: rest, done: done?} + {{:ok, reply}, new_state} + + _ -> + state + |> Map.put(:buffer, buffer <> data) + |> Map.put(:done, done?) + |> process_stream() + end + end end diff --git a/lib/grpc/client/adapters/mint/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process.ex index 5bdc03e8..0bc25804 100644 --- a/lib/grpc/client/adapters/mint/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process.ex @@ -9,14 +9,18 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.start_link(__MODULE__, {scheme, host, port, opts}) end - def request(pid, method, path, headers, body) do - GenServer.call(pid, {:request, method, path, headers, body}) + def request(pid, method, path, headers, body, streamed_response?) do + GenServer.call(pid, {:request, method, path, headers, body, streamed_response?}) end def disconnect(pid) do GenServer.call(pid, {:disconnect, :brutal}) end + def process_stream_data(pid, request_ref) do + GenServer.call(pid, {:process_stream_data, request_ref}) + end + ## Callbacks @impl true @@ -40,11 +44,35 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:stop, :normal, :ok, state} end - def handle_call({:request, method, path, headers, body}, from, state) do + def handle_call({:process_stream_data, request_ref}, _from, state) do + case state.requests[request_ref] do + %{done: true} -> + {%{response: response, from: _from}, state} = pop_in(state.requests[request_ref]) + {:reply, {response.data, true}, state} + + %{request: %{data: data}} -> + state = put_in(state.requests[request_ref].response[:data], nil) + {:reply, {data, false}, state} + end + end + + def handle_call( + {:request, method, path, headers, body, streamed_response?}, + from, + state + ) do case Mint.HTTP.request(state.conn, method, path, headers, body) do {:ok, conn, request_ref} -> state = put_in(state.conn, conn) - state = put_in(state.requests[request_ref], %{from: from, response: %{}}) + + state = + put_in(state.requests[request_ref], %{ + from: from, + streamed_response: streamed_response?, + done: false, + response: %{} + }) + {:noreply, state} {:error, conn, reason} -> @@ -72,7 +100,15 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp process_response({:headers, request_ref, headers}, state) do - put_in(state.requests[request_ref].response[:headers], headers) + # For server stream connections, we wait for the headers and accumulate the data. + if state.requests[request_ref].streamed_response do + from = state.requests[request_ref].from + state = put_in(state.requests[request_ref].response[:headers], headers) + GenServer.reply(from, {:ok, {state.requests[request_ref].response, request_ref}}) + state + else + put_in(state.requests[request_ref].response[:headers], headers) + end end defp process_response({:data, request_ref, new_data}, state) do @@ -80,8 +116,12 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp process_response({:done, request_ref}, state) do - {%{response: response, from: from}, state} = pop_in(state.requests[request_ref]) - GenServer.reply(from, {:ok, response}) - state + if state.requests[request_ref].streamed_response do + put_in(state.requests[request_ref][:done], true) + else + {%{response: response, from: from}, state} = pop_in(state.requests[request_ref]) + GenServer.reply(from, {:ok, response}) + state + end end end From 1ebc7d5b1a8b0e378f708fa55f54b7532a092546 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 10 Sep 2022 14:04:41 -0300 Subject: [PATCH 10/82] refactor server stream consuption to use a short lived genserver --- lib/grpc/client/adapters/mint.ex | 109 +++++-------- .../adapters/mint/connection_process.ex | 127 --------------- .../connection_process/connection_process.ex | 148 ++++++++++++++++++ .../adapters/mint/connection_process/state.ex | 60 +++++++ .../adapters/mint/stream_response_process.ex | 141 +++++++++++++++++ mix.lock | 2 + 6 files changed, 392 insertions(+), 195 deletions(-) delete mode 100644 lib/grpc/client/adapters/mint/connection_process.ex create mode 100644 lib/grpc/client/adapters/mint/connection_process/connection_process.ex create mode 100644 lib/grpc/client/adapters/mint/connection_process/state.ex create mode 100644 lib/grpc/client/adapters/mint/stream_response_process.ex diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 735a9023..23dd5b0b 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -4,7 +4,7 @@ defmodule GRPC.Client.Adapters.Mint do """ alias GRPC.{Channel, Credential} - alias __MODULE__.ConnectionProcess + alias GRPC.Client.Adapters.Mint.{ConnectionProcess, StreamResponseProcess} @behaviour GRPC.Client.Adapter @@ -38,7 +38,7 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def send_request( - %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path, server_stream: stream?} = + %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path, server_stream: false} = stream, message, opts @@ -46,18 +46,42 @@ defmodule GRPC.Client.Adapters.Mint do when is_pid(pid) do headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) {:ok, data, _} = GRPC.Message.to_data(message, opts) - {:ok, response} = ConnectionProcess.request(pid, "POST", path, headers, data, stream?) + response = ConnectionProcess.request(pid, "POST", path, headers, data) GRPC.Client.Stream.put_payload(stream, :response, response) end + def send_request( + %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path, server_stream: true} = + stream, + message, + opts + ) + when is_pid(pid) do + headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + {:ok, data, _} = GRPC.Message.to_data(message, opts) + {:ok, stream_response_pid} = StreamResponseProcess.start_link(stream, opts[:return_headers] || false) + + response = + ConnectionProcess.request(pid, "POST", path, headers, data, + streamed_response: true, + stream_response_pid: stream_response_pid + ) + + stream + |> GRPC.Client.Stream.put_payload(:response, response) + |> GRPC.Client.Stream.put_payload(:stream_response_pid, stream_response_pid) + end + @impl true - def receive_data(%{server_stream: true, payload: %{response: response}} = stream, opts) do - with {%{headers: headers}, request_ref} <- response, - stream_response <- build_response_stream(stream, request_ref) do - if(opts[:return_headers]) do - {:ok, stream_response, %{headers: headers}} - else - {:ok, stream_response} + def receive_data( + %{server_stream: true, payload: %{response: response, stream_response_pid: pid}}, + opts + ) do + with {:ok, headers} <- response do + stream = StreamResponseProcess.build_stream(pid) + case opts[:return_headers] do + true -> {:ok, stream, headers} + _any -> {:ok, stream} end end end @@ -69,13 +93,17 @@ defmodule GRPC.Client.Adapters.Mint do accepted_compressors: accepted_compressors, payload: %{response: response} } = _stream, - _opts + opts ) do - with %{data: body, headers: headers} <- response, + with {:ok, %{data: body, headers: headers}} <- response, 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)} + if opts[:return_headers] do + {:ok, codec.decode(msg, res_mod), %{headers: headers}} + else + {:ok, codec.decode(msg, res_mod)} + end end end @@ -111,59 +139,4 @@ defmodule GRPC.Client.Adapters.Mint do body end end - - defp build_response_stream(grpc_stream, request_ref) do - state = %{ - buffer: <<>>, - done: false, - request_ref: request_ref, - grpc_stream: grpc_stream - } - - Stream.unfold(state, fn s -> process_stream(s) end) - end - - defp process_stream(%{buffer: <<>>, done: true}) do - nil - end - - defp process_stream(%{done: true} = state) do - parse_message(state, "", true) - end - - defp process_stream( - %{request_ref: ref, grpc_stream: %{channel: %{adapter_payload: %{conn_pid: pid}}}} = - state - ) do - case ConnectionProcess.process_stream_data(pid, ref) do - {nil, false = _done?} -> - Process.sleep(500) - process_stream(state) - - {nil = _data, true = _done?} -> - parse_message(state, "", true) - - {data, done?} -> - parse_message(state, data, done?) - end - end - - defp parse_message( - %{buffer: buffer, grpc_stream: %{response_mod: res_mod, codec: codec}} = state, - data, - done? - ) do - case GRPC.Message.get_message(buffer <> data) do - {{_, message}, rest} -> - reply = codec.decode(message, res_mod) - new_state = %{state | buffer: rest, done: done?} - {{:ok, reply}, new_state} - - _ -> - state - |> Map.put(:buffer, buffer <> data) - |> Map.put(:done, done?) - |> process_stream() - end - end end diff --git a/lib/grpc/client/adapters/mint/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process.ex deleted file mode 100644 index 0bc25804..00000000 --- a/lib/grpc/client/adapters/mint/connection_process.ex +++ /dev/null @@ -1,127 +0,0 @@ -defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do - use GenServer - - require Logger - - defstruct [:conn, requests: %{}] - - def start_link(scheme, host, port, opts \\ []) do - GenServer.start_link(__MODULE__, {scheme, host, port, opts}) - end - - def request(pid, method, path, headers, body, streamed_response?) do - GenServer.call(pid, {:request, method, path, headers, body, streamed_response?}) - end - - def disconnect(pid) do - GenServer.call(pid, {:disconnect, :brutal}) - end - - def process_stream_data(pid, request_ref) do - GenServer.call(pid, {:process_stream_data, request_ref}) - end - - ## Callbacks - - @impl true - def init({scheme, host, port, opts}) do - case Mint.HTTP.connect(scheme, host, port, opts) do - {:ok, conn} -> - state = %__MODULE__{conn: conn} - {:ok, state} - - {:error, reason} -> - # TODO check what's better: add to state map if connection is alive? - # TODO Or simply stop the process and handle the error on caller? - {:stop, reason} - end - end - - @impl true - def handle_call({:disconnect, :brutal}, _from, state) do - # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests - Mint.HTTP.close(state.conn) - {:stop, :normal, :ok, state} - end - - def handle_call({:process_stream_data, request_ref}, _from, state) do - case state.requests[request_ref] do - %{done: true} -> - {%{response: response, from: _from}, state} = pop_in(state.requests[request_ref]) - {:reply, {response.data, true}, state} - - %{request: %{data: data}} -> - state = put_in(state.requests[request_ref].response[:data], nil) - {:reply, {data, false}, state} - end - end - - def handle_call( - {:request, method, path, headers, body, streamed_response?}, - from, - state - ) do - case Mint.HTTP.request(state.conn, method, path, headers, body) do - {:ok, conn, request_ref} -> - state = put_in(state.conn, conn) - - state = - put_in(state.requests[request_ref], %{ - from: from, - streamed_response: streamed_response?, - done: false, - response: %{} - }) - - {:noreply, state} - - {:error, conn, reason} -> - state = put_in(state.conn, conn) - {:reply, {:error, reason}, state} - end - end - - @impl true - def handle_info(message, state) do - case Mint.HTTP.stream(state.conn, message) do - :unknown -> - Logger.debug(fn -> "Received unknown message: " <> inspect(message) end) - {:noreply, state} - - {:ok, conn, responses} -> - state = put_in(state.conn, conn) - state = Enum.reduce(responses, state, &process_response/2) - {:noreply, state} - end - end - - defp process_response({:status, request_ref, status}, state) do - put_in(state.requests[request_ref].response[:status], status) - end - - defp process_response({:headers, request_ref, headers}, state) do - # For server stream connections, we wait for the headers and accumulate the data. - if state.requests[request_ref].streamed_response do - from = state.requests[request_ref].from - state = put_in(state.requests[request_ref].response[:headers], headers) - GenServer.reply(from, {:ok, {state.requests[request_ref].response, request_ref}}) - state - else - put_in(state.requests[request_ref].response[:headers], headers) - end - end - - defp process_response({:data, request_ref, new_data}, state) do - update_in(state.requests[request_ref].response[:data], fn data -> (data || "") <> new_data end) - end - - defp process_response({:done, request_ref}, state) do - if state.requests[request_ref].streamed_response do - put_in(state.requests[request_ref][:done], true) - else - {%{response: response, from: from}, state} = pop_in(state.requests[request_ref]) - GenServer.reply(from, {:ok, response}) - state - end - end -end diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex new file mode 100644 index 00000000..4f6f1e6b --- /dev/null +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -0,0 +1,148 @@ +defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do + use GenServer + + alias GRPC.Client.Adapters.Mint.ConnectionProcess.State + alias GRPC.Client.Adapters.Mint.StreamResponseProcess + + require Logger + + def start_link(scheme, host, port, opts \\ []) do + GenServer.start_link(__MODULE__, {scheme, host, port, opts}) + end + + def request(pid, method, path, headers, body, opts \\ []) do + GenServer.call(pid, {:request, method, path, headers, body, opts}) + end + + def disconnect(pid) do + GenServer.call(pid, {:disconnect, :brutal}) + end + + ## Callbacks + + @impl true + def init({scheme, host, port, opts}) do + # The current behavior in gun is return error if the connection wasn't successful + # Should we do the same here? + case Mint.HTTP.connect(scheme, host, port, opts) do + {:ok, conn} -> + {:ok, State.new(conn)} + + {:error, reason} -> + # TODO check what's better: add to state map if connection is alive? + # TODO Or simply stop the process and handle the error on caller? + {:stop, reason} + end + end + + @impl true + def handle_call({:disconnect, :brutal}, _from, state) do + # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests + Mint.HTTP.close(state.conn) + {:stop, :normal, :ok, state} + end + + def handle_call( + {:request, method, path, headers, body, opts}, + from, + state + ) do + case Mint.HTTP.request(state.conn, method, path, headers, body) do + {:ok, conn, request_ref} -> + new_state = + state + |> State.update_conn(conn) + |> State.put_in_ref(request_ref, %{ + from: from, + streamed_response: opts[:streamed_response] || false, + stream_response_pid: opts[:stream_response_pid], + done: false, + response: %{} + }) + + {:noreply, new_state} + + {:error, conn, reason} -> + new_state = State.update_conn(state, conn) + {:reply, {:error, reason}, new_state} + end + end + + @impl true + def handle_info(message, state) do + case Mint.HTTP.stream(state.conn, message) do + :unknown -> + Logger.debug(fn -> "Received unknown message: " <> inspect(message) end) + {:noreply, state} + + {:ok, conn, responses} -> + state = State.update_conn(state, conn) + state = Enum.reduce(responses, state, &process_response/2) + {:noreply, state} + end + end + + defp process_response({:status, request_ref, status}, state) do + State.update_response_status(state, request_ref, status) + end + + defp process_response({:headers, request_ref, headers}, state) do + streamed_response? = State.steamed_response?(state, request_ref) + empty_headers? = State.empty_headers?(state, request_ref) + + # For server streamed connections, we wait until the response headers to reply the caller process. + # If we have headers set in state, it means that the response headers already arrived, + # so incoming headers message it's referring to trailers headers + cond do + streamed_response? and empty_headers? -> + new_state = State.update_response_headers(state, request_ref, headers) + response = State.get_response(state, request_ref) + + state + |> State.caller_process(request_ref) + |> GenServer.reply({:ok, response}) + + new_state + + empty_headers? -> + State.update_response_headers(state, request_ref, headers) + + streamed_response? -> + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:trailers, headers) + + state + + true -> + State.update_response_trailers(state, request_ref, headers) + end + end + + defp process_response({:data, request_ref, new_data}, state) do + if(State.steamed_response?(state, request_ref)) do + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:data, new_data) + + state + else + State.append_response_data(state, request_ref, new_data) + end + end + + defp process_response({:done, request_ref}, state) do + if State.steamed_response?(state, request_ref) do + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.done() + + {_ref, new_state} = State.pop_ref(state, request_ref) + new_state + else + {%{response: response, from: from}, state} = State.pop_ref(state, request_ref) + GenServer.reply(from, {:ok, response}) + state + end + end +end diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex new file mode 100644 index 00000000..cd5cef4b --- /dev/null +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -0,0 +1,60 @@ +defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do + defstruct [:conn, requests: %{}] + + @type t :: %__MODULE__{ + conn: Mint.HTTP.t(), + requests: map() + } + + def new(conn) do + %__MODULE__{conn: conn} + end + + def update_conn(state, conn) do + %{state | conn: conn} + end + + def put_in_ref(state, ref, ref_state) do + put_in(state.requests[ref], ref_state) + end + + def update_response_status(state, ref, status) do + put_in(state.requests[ref].response[:status], status) + end + + def update_response_headers(state, ref, headers) do + put_in(state.requests[ref].response[:headers], headers) + end + + def update_response_trailers(state, ref, trailers) do + put_in(state.requests[ref].response[:trailers], trailers) + end + + def steamed_response?(state, ref) do + state.requests[ref].streamed_response + end + + def empty_headers?(state, ref) do + is_nil(state.requests[ref].response[:headers]) + end + + def stream_response_pid(state, ref) do + state.requests[ref].stream_response_pid + end + + def caller_process(state, ref) do + state.requests[ref].from + end + + def get_response(state, ref) do + pop_in(state.requests[ref][:response]) + end + + def pop_ref(state, ref) do + pop_in(state.requests[ref]) + end + + def append_response_data(state, ref, new_data) do + update_in(state.requests[ref].response[:data], fn data -> (data || "") <> new_data end) + end +end diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex new file mode 100644 index 00000000..6fe0a0c7 --- /dev/null +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -0,0 +1,141 @@ +defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do + use GenServer + + def start_link(stream, send_trailers?) do + GenServer.start_link(__MODULE__, {stream, send_trailers?}) + end + + @doc """ + Given a pid, builds a Stream that will consume the accumulated + data inside this process + """ + @spec build_stream(pid()) :: Elixir.Stream.t() + def build_stream(pid) do + Elixir.Stream.unfold(pid, fn pid -> + case GenServer.call(pid, :get_response, :infinity) do + nil -> nil + response -> {response, pid} + end + end) + end + + @doc """ + Cast a message to process to inform that the stream has finished + """ + @spec done(pid()) :: :ok + def done(pid) do + GenServer.cast(pid, {:consume_response, :done}) + end + + @doc """ + Cast a message to process to consume an incoming data or trailers + """ + @spec consume(pid(), :data | :trailers, binary() | Mint.Types.headers()) :: :ok + def consume(pid, :data, data) do + GenServer.cast(pid, {:consume_response, {:data, data}}) + end + + def consume(pid, :trailers, trailers) do + GenServer.cast(pid, {:consume_response, {:trailers, trailers}}) + end + + # Callbacks + + def init({stream, send_trailers?}) do + state = %{ + grpc_stream: stream, + send_trailers: send_trailers?, + buffer: <<>>, + responses: [], + done: false, + from: nil + } + + {:ok, state} + end + + def handle_call(:get_response, from, state) do + {:noreply, put_in(state[:from], from), {:continue, :produce_response}} + end + + def handle_cast({:consume_response, {:data, data}}, state) do + %{ + buffer: buffer, + grpc_stream: %{response_mod: res_mod, codec: codec}, + responses: responses + } = state + + case GRPC.Message.get_message(buffer <> data) do + {{_, message}, rest} -> + response = codec.decode(message, res_mod) + new_responses = [{:ok, response} | responses] + new_state = %{state | buffer: rest, responses: new_responses} + {:noreply, new_state, {:continue, :produce_response}} + + _ -> + new_state = %{state | bufferr: buffer <> data} + {:noreply, new_state, {:continue, :produce_response}} + end + end + + def handle_cast( + {:consume_response, {:trailers, trailers}}, + %{send_trailers: true, responses: responses} = state + ) do + new_responses = [get_trailers_response(trailers) | responses] + {:noreply, %{state | responses: new_responses}, {:continue, :produce_response}} + end + + def handle_cast( + {:consume_response, {:trailers, trailers}}, + %{send_trailers: false, responses: responses} = state + ) do + with {:errors, _rpc_error} = error <- get_trailers_response(trailers) do + {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} + else + _any -> {:noreply, state, {:continue, :produce_response}} + end + end + + def handle_cast({:consume_response, :done}, state) do + {:noreply, %{state | done: true}, {:continue, :produce_response}} + end + + def handle_continue(:produce_response, state) do + case state do + %{from: nil} -> + {:noreply, state} + + %{from: from, responses: [], done: true} -> + GenServer.reply(from, nil) + {:stop, :normal, state} + + %{responses: []} -> + {:noreply, state} + + %{responses: [response | rest], from: from} -> + IO.inspect(from, label: "caller") + IO.inspect(self(), label: "self") + GenServer.reply(from, response) + {:noreply, %{state | responses: rest, from: nil}} + end + end + + defp get_trailers_response(trailers) do + decoded_trailers = GRPC.Transport.HTTP2.decode_headers(trailers) + status = String.to_integer(decoded_trailers["grpc-status"]) + + case status == GRPC.Status.ok() do + true -> + {:trailers, decoded_trailers} + + false -> + rpc_error = %GRPC.RPCError{status: status, message: decoded_trailers["grpc-message"]} + {:error, rpc_error} + end + end + + def terminate(_reason, _state) do + :normal + end +end diff --git a/mix.lock b/mix.lock index 43cfde44..e6d75a14 100644 --- a/mix.lock +++ b/mix.lock @@ -7,10 +7,12 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "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"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "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"}, + "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "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"}, From 11c8a7f46f68a63cd93abe4827191b3cbd46f000 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 10 Sep 2022 14:33:58 -0300 Subject: [PATCH 11/82] fix disconnect code for connection_process when brutal killing --- .../mint/connection_process/connection_process.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 4f6f1e6b..e3bbd8e8 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -38,8 +38,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do @impl true def handle_call({:disconnect, :brutal}, _from, state) do # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests - Mint.HTTP.close(state.conn) - {:stop, :normal, :ok, state} + {:stop, :normal, Mint.HTTP.close(state.conn), state} end def handle_call( @@ -145,4 +144,9 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do state end end + + @impl true + def terminate(_reason, _state) do + :normal + end end From 8f3bf6769bb1467f74c3efb132dc1356a943318c Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 10 Sep 2022 19:48:46 -0300 Subject: [PATCH 12/82] refactor: use stream response module to handle unary calls --- lib/grpc/client/adapters/mint.ex | 65 +++++++------------ .../connection_process/connection_process.ex | 55 +++++----------- .../adapters/mint/connection_process/state.ex | 2 +- .../adapters/mint/stream_response_process.ex | 2 - 4 files changed, 41 insertions(+), 83 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 23dd5b0b..4dea1f0f 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -38,32 +38,19 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def send_request( - %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path, server_stream: false} = - stream, + %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, message, opts ) when is_pid(pid) do headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) {:ok, data, _} = GRPC.Message.to_data(message, opts) - response = ConnectionProcess.request(pid, "POST", path, headers, data) - GRPC.Client.Stream.put_payload(stream, :response, response) - end - def send_request( - %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path, server_stream: true} = - stream, - message, - opts - ) - when is_pid(pid) do - headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) - {:ok, data, _} = GRPC.Message.to_data(message, opts) - {:ok, stream_response_pid} = StreamResponseProcess.start_link(stream, opts[:return_headers] || false) + {:ok, stream_response_pid} = + StreamResponseProcess.start_link(stream, opts[:return_headers] || false) response = ConnectionProcess.request(pid, "POST", path, headers, data, - streamed_response: true, stream_response_pid: stream_response_pid ) @@ -79,6 +66,7 @@ defmodule GRPC.Client.Adapters.Mint do ) do with {:ok, headers} <- response do stream = StreamResponseProcess.build_stream(pid) + case opts[:return_headers] do true -> {:ok, stream, headers} _any -> {:ok, stream} @@ -86,23 +74,16 @@ defmodule GRPC.Client.Adapters.Mint do end end - def receive_data( - %{ - response_mod: res_mod, - codec: codec, - accepted_compressors: accepted_compressors, - payload: %{response: response} - } = _stream, - opts - ) do - with {:ok, %{data: body, headers: headers}} <- response, - compressor <- get_compressor(headers, accepted_compressors), - body <- get_body(codec, body), - {:ok, msg} <- GRPC.Message.from_data(%{compressor: compressor}, body) do - if opts[:return_headers] do - {:ok, codec.decode(msg, res_mod), %{headers: headers}} - else - {:ok, codec.decode(msg, res_mod)} + def receive_data(%{payload: %{response: response, stream_response_pid: pid}}, opts) do + with {:ok, %{headers: headers}} <- response, + stream <- StreamResponseProcess.build_stream(pid), + responses <- Enum.into(stream, []), + :ok <- check_for_error(responses) do + {:ok, data} = Enum.find(responses, fn {status, _data} -> status == :ok end) + + case opts[:return_headers] do + true -> {:ok, data, append_trailers(headers, responses)} + _any -> {:ok, data} end end end @@ -126,17 +107,17 @@ defmodule GRPC.Client.Adapters.Mint do defp mint_scheme(%Channel{scheme: "https"} = _channel), do: :https defp mint_scheme(_channel), do: :http - defp get_compressor(%{"grpc-encoding" => encoding} = _headers, accepted_compressors) do - Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding end) + def check_for_error(responses) do + error = Enum.find(responses, fn {status, _data} -> status == :error end) + if error == nil, do: :ok, else: error 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 + defp append_trailers(headers, responses) do + responses + |> Enum.find(fn {status, _data} -> status == :trailers end) + |> case do + nil -> %{headers: headers} + {:trailers, trailers} -> %{headers: headers, trailers: trailers} end end end diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index e3bbd8e8..8f61c8fb 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -53,7 +53,6 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do |> State.update_conn(conn) |> State.put_in_ref(request_ref, %{ from: from, - streamed_response: opts[:streamed_response] || false, stream_response_pid: opts[:stream_response_pid], done: false, response: %{} @@ -86,67 +85,47 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp process_response({:headers, request_ref, headers}, state) do - streamed_response? = State.steamed_response?(state, request_ref) empty_headers? = State.empty_headers?(state, request_ref) - # For server streamed connections, we wait until the response headers to reply the caller process. - # If we have headers set in state, it means that the response headers already arrived, - # so incoming headers message it's referring to trailers headers - cond do - streamed_response? and empty_headers? -> + case empty_headers? do + true -> new_state = State.update_response_headers(state, request_ref, headers) - response = State.get_response(state, request_ref) + response = State.get_response(new_state, request_ref) - state + new_state |> State.caller_process(request_ref) |> GenServer.reply({:ok, response}) new_state - empty_headers? -> - State.update_response_headers(state, request_ref, headers) - - streamed_response? -> + false -> state |> State.stream_response_pid(request_ref) |> StreamResponseProcess.consume(:trailers, headers) state - - true -> - State.update_response_trailers(state, request_ref, headers) end end defp process_response({:data, request_ref, new_data}, state) do - if(State.steamed_response?(state, request_ref)) do - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:data, new_data) - - state - else - State.append_response_data(state, request_ref, new_data) - end + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:data, new_data) + + state end defp process_response({:done, request_ref}, state) do - if State.steamed_response?(state, request_ref) do - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.done() - - {_ref, new_state} = State.pop_ref(state, request_ref) - new_state - else - {%{response: response, from: from}, state} = State.pop_ref(state, request_ref) - GenServer.reply(from, {:ok, response}) - state - end + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.done() + + {_ref, new_state} = State.pop_ref(state, request_ref) + new_state end @impl true def terminate(_reason, _state) do - :normal + :normal end end diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index cd5cef4b..3b22e3a6 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -47,7 +47,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do end def get_response(state, ref) do - pop_in(state.requests[ref][:response]) + state.requests[ref][:response] end def pop_ref(state, ref) do diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 6fe0a0c7..e96a9faa 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -114,8 +114,6 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:noreply, state} %{responses: [response | rest], from: from} -> - IO.inspect(from, label: "caller") - IO.inspect(self(), label: "self") GenServer.reply(from, response) {:noreply, %{state | responses: rest, from: nil}} end From b2c692d4ca2605b936cc5c1a43da5ab88ae13913 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 10 Sep 2022 19:54:56 -0300 Subject: [PATCH 13/82] add todo for compressor headers --- lib/grpc/client/adapters/mint/stream_response_process.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index e96a9faa..fe11b6de 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -67,6 +67,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do case GRPC.Message.get_message(buffer <> data) do {{_, message}, rest} -> + # TODO add code here to handle compressor headers response = codec.decode(message, res_mod) new_responses = [{:ok, response} | responses] new_state = %{state | buffer: rest, responses: new_responses} From d500634d4dee6836db0e4ce84328347511ccf2db Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 10 Sep 2022 19:57:48 -0300 Subject: [PATCH 14/82] remove mint adapter code from hello world --- examples/helloworld/priv/client.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/helloworld/priv/client.exs b/examples/helloworld/priv/client.exs index 3fc7eb26..dc6bea5d 100644 --- a/examples/helloworld/priv/client.exs +++ b/examples/helloworld/priv/client.exs @@ -1,4 +1,4 @@ -{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client], adapter: GRPC.Client.Adapters.Mint) +{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) {:ok, reply} = channel From 758928093f8613fa03b3b100e54b996ef2d9d059 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 11 Sep 2022 12:47:57 -0300 Subject: [PATCH 15/82] add client streaming api for mint adapter --- examples/route_guide/lib/client.ex | 22 +++---- lib/grpc/client/adapter.ex | 4 ++ lib/grpc/client/adapters/gun.ex | 2 + lib/grpc/client/adapters/mint.ex | 59 +++++++++++++++++++ .../connection_process/connection_process.ex | 48 ++++++++++++++- .../adapters/mint/connection_process/state.ex | 2 +- .../adapters/mint/stream_response_process.ex | 30 +++++++--- 7 files changed, 147 insertions(+), 20 deletions(-) diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex index 5d33f4ba..aee40825 100644 --- a/examples/route_guide/lib/client.ex +++ b/examples/route_guide/lib/client.ex @@ -1,16 +1,16 @@ defmodule RouteGuide.Client do def main(channel) do - print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) - print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) - - # Looking for features between 40, -75 and 42, -73. - print_features( - channel, - Routeguide.Rectangle.new( - lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), - hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) - ) - ) + # print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) + # print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) + + ## # Looking for features between 40, -75 and 42, -73. + # print_features( + # channel, + # Routeguide.Rectangle.new( + # lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), + # hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) + # ) + # ) run_record_route(channel) diff --git a/lib/grpc/client/adapter.ex b/lib/grpc/client/adapter.ex index b1f684a6..b2dd3ff6 100644 --- a/lib/grpc/client/adapter.ex +++ b/lib/grpc/client/adapter.ex @@ -22,4 +22,8 @@ defmodule GRPC.Client.Adapter do """ @callback receive_data(stream :: Stream.t(), opts :: keyword()) :: GRPC.Stub.receive_data_return() | {:error, any()} + + @callback send_headers(stream :: Stream.t(), opts :: keyword()) :: Stream.t() + + @callback send_data(stream :: Stream.t(), message :: binary(), opts :: keyword()) :: Stream.t() end diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index f8d1785c..8472e667 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -107,6 +107,7 @@ defmodule GRPC.Client.Adapters.Gun do :gun.post(conn_pid, path, headers, data) end + @impl true def send_headers( %{channel: %{adapter_payload: %{conn_pid: conn_pid}}, path: path} = stream, opts @@ -116,6 +117,7 @@ defmodule GRPC.Client.Adapters.Gun do GRPC.Client.Stream.put_payload(stream, :stream_ref, stream_ref) end + @impl true def send_data(%{channel: channel, payload: %{stream_ref: stream_ref}} = stream, message, opts) do conn_pid = channel.adapter_payload[:conn_pid] fin = if opts[:send_end_stream], do: :fin, else: :nofin diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 4dea1f0f..ae1127ae 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -74,6 +74,23 @@ defmodule GRPC.Client.Adapters.Mint do end end + # for streamed requests + def receive_data( + %{payload: %{response: {:ok, %{request_ref: _ref}}, stream_response_pid: pid}}, + opts + ) do + with stream <- StreamResponseProcess.build_stream(pid), + responses <- Enum.into(stream, []), + :ok <- check_for_error(responses) do + {:ok, data} = Enum.find(responses, fn {status, _data} -> status == :ok end) + + case opts[:return_headers] do + true -> {:ok, data, get_headers(responses) |> append_trailers(responses)} + _any -> {:ok, data} + end + end + end + def receive_data(%{payload: %{response: response, stream_response_pid: pid}}, opts) do with {:ok, %{headers: headers}} <- response, stream <- StreamResponseProcess.build_stream(pid), @@ -88,6 +105,39 @@ defmodule GRPC.Client.Adapters.Mint do end end + @impl true + def send_headers(%{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, opts) do + headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + + {:ok, stream_response_pid} = + StreamResponseProcess.start_link(stream, opts[:return_headers] || false) + + response = + ConnectionProcess.request(pid, "POST", path, headers, :stream, + stream_response_pid: stream_response_pid + ) + + stream + |> GRPC.Client.Stream.put_payload(:response, response) + |> GRPC.Client.Stream.put_payload(:stream_response_pid, stream_response_pid) + end + + @impl true + def send_data( + %{ + channel: %{adapter_payload: %{conn_pid: pid}}, + payload: %{response: {:ok, %{request_ref: request_ref}}} + } = stream, + message, + opts + ) do + {:ok, data, _} = GRPC.Message.to_data(message, opts) + :ok = ConnectionProcess.stream_request_body(pid, request_ref, data) + # TODO: check for trailer headers to be sent here + if opts[:send_end_stream], do: ConnectionProcess.stream_request_body(pid, request_ref, :eof) + stream + end + defp connect_opts(%Channel{scheme: "https"} = channel, opts) do %Credential{ssl: ssl} = Map.get(channel, :cred, %Credential{}) @@ -120,4 +170,13 @@ defmodule GRPC.Client.Adapters.Mint do {:trailers, trailers} -> %{headers: headers, trailers: trailers} end end + + defp get_headers(responses) do + responses + |> Enum.find(fn {status, _data} -> status == :headers end) + |> case do + nil -> nil + {:headers, headers} -> headers + end + end end diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 8f61c8fb..81156430 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -18,6 +18,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.call(pid, {:disconnect, :brutal}) end + def stream_request_body(pid, request_ref, body) do + GenServer.call(pid, {:stream_body, request_ref, body}) + end + ## Callbacks @impl true @@ -41,6 +45,30 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:stop, :normal, Mint.HTTP.close(state.conn), state} end + def handle_call( + {:request, method, path, headers, :stream, opts}, + _from, + state + ) do + case Mint.HTTP.request(state.conn, method, path, headers, :stream) do + {:ok, conn, request_ref} -> + new_state = + state + |> State.update_conn(conn) + |> State.put_in_ref(request_ref, %{ + stream_response_pid: opts[:stream_response_pid], + done: false, + response: %{} + }) + + {:reply, {:ok, %{request_ref: request_ref}}, new_state} + + {:error, conn, reason} -> + new_state = State.update_conn(state, conn) + {:reply, {:error, reason}, new_state} + end + end + def handle_call( {:request, method, path, headers, body, opts}, from, @@ -66,6 +94,16 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end end + def handle_call({:stream_body, request_ref, body}, _from, state) do + case Mint.HTTP.stream_request_body(state.conn, request_ref, body) do + {:ok, conn} -> + {:reply, :ok, State.update_conn(state, conn)} + + {:error, conn, error} -> + {:reply, {:error, error}, State.update_conn(state, conn)} + end + end + @impl true def handle_info(message, state) do case Mint.HTTP.stream(state.conn, message) do @@ -94,7 +132,15 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do new_state |> State.caller_process(request_ref) - |> GenServer.reply({:ok, response}) + |> case do + nil -> + new_state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:headers, headers) + + ref -> + GenServer.reply(ref, {:ok, response}) + end new_state diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index 3b22e3a6..af0cac5f 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -43,7 +43,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do end def caller_process(state, ref) do - state.requests[ref].from + state.requests[ref][:from] end def get_response(state, ref) do diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index fe11b6de..48f210d5 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -1,8 +1,8 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do use GenServer - def start_link(stream, send_trailers?) do - GenServer.start_link(__MODULE__, {stream, send_trailers?}) + def start_link(stream, send_headers_or_trailers?) do + GenServer.start_link(__MODULE__, {stream, send_headers_or_trailers?}) end @doc """ @@ -30,7 +30,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do @doc """ Cast a message to process to consume an incoming data or trailers """ - @spec consume(pid(), :data | :trailers, binary() | Mint.Types.headers()) :: :ok + @spec consume(pid(), :data | :trailers | :headers, binary() | Mint.Types.headers()) :: :ok def consume(pid, :data, data) do GenServer.cast(pid, {:consume_response, {:data, data}}) end @@ -39,12 +39,16 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do GenServer.cast(pid, {:consume_response, {:trailers, trailers}}) end + def consume(pid, :headers, headers) do + GenServer.cast(pid, {:consume_response, {:headers, headers}}) + end + # Callbacks - def init({stream, send_trailers?}) do + def init({stream, send_headers_or_trailers?}) do state = %{ grpc_stream: stream, - send_trailers: send_trailers?, + send_headers_or_trailers: send_headers_or_trailers?, buffer: <<>>, responses: [], done: false, @@ -81,7 +85,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do def handle_cast( {:consume_response, {:trailers, trailers}}, - %{send_trailers: true, responses: responses} = state + %{send_headers_or_trailers: true, responses: responses} = state ) do new_responses = [get_trailers_response(trailers) | responses] {:noreply, %{state | responses: new_responses}, {:continue, :produce_response}} @@ -89,7 +93,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do def handle_cast( {:consume_response, {:trailers, trailers}}, - %{send_trailers: false, responses: responses} = state + %{send_headers_or_trailers: false, responses: responses} = state ) do with {:errors, _rpc_error} = error <- get_trailers_response(trailers) do {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} @@ -98,6 +102,18 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do end end + def handle_cast( + {:consume_response, {:headers, headers}}, + %{send_headers_or_trailers: true, responses: responses} = state + ) do + {:noreply, %{state | responses: [{:headers, headers} | responses]}, + {:continue, :produce_response}} + end + + def handle_cast({:consume_response, {:headers, _headers}}, state) do + {:noreply, state, {:continue, :produce_response}} + end + def handle_cast({:consume_response, :done}, state) do {:noreply, %{state | done: true}, {:continue, :produce_response}} end From 61f9f3d6377d5391ba8c1f8edc00a59f9f4f8bc0 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 11 Sep 2022 12:49:33 -0300 Subject: [PATCH 16/82] uncomment route_guide test client requests --- examples/route_guide/lib/client.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex index aee40825..08c22f30 100644 --- a/examples/route_guide/lib/client.ex +++ b/examples/route_guide/lib/client.ex @@ -1,16 +1,16 @@ defmodule RouteGuide.Client do def main(channel) do - # print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) - # print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) + print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) + print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) ## # Looking for features between 40, -75 and 42, -73. - # print_features( - # channel, - # Routeguide.Rectangle.new( - # lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), - # hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) - # ) - # ) + print_features( + channel, + Routeguide.Rectangle.new( + lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), + hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) + ) + ) run_record_route(channel) From c03f0bfa3e6e23efe1b18493edee1c39cbefe872 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 18 Sep 2022 12:56:39 -0300 Subject: [PATCH 17/82] raise exception for unhandled error cases --- examples/route_guide/lib/client.ex | 2 +- lib/grpc/client/adapters/mint.ex | 16 +++++++++++----- .../adapters/mint/connection_process/state.ex | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex index 08c22f30..5d33f4ba 100644 --- a/examples/route_guide/lib/client.ex +++ b/examples/route_guide/lib/client.ex @@ -3,7 +3,7 @@ defmodule RouteGuide.Client do print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) - ## # Looking for features between 40, -75 and 42, -73. + # Looking for features between 40, -75 and 42, -73. print_features( channel, Routeguide.Rectangle.new( diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index ae1127ae..8209e1a3 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -21,22 +21,25 @@ defmodule GRPC.Client.Adapters.Mint do |> case do {:ok, pid} -> {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} # TODO add proper error handling - _error -> {:ok, %{channel | adapter_payload: %{conn_pid: nil}}} + error -> raise "An error happened while trying to opening the connection: #{inspect(error)}" end end @impl true def disconnect(%{adapter_payload: %{conn_pid: pid}} = channel) when is_pid(pid) do - ConnectionProcess.disconnect(pid) - {:ok, %{channel | adapter_payload: %{conn_pid: nil}}} + :ok = ConnectionProcess.disconnect(pid) + {:ok, %{channel | adapter_payload: nil}} end - def disconnect(%{adapter_payload: %{conn_pid: nil}} = channel) do + def disconnect(%{adapter_payload: nil} = channel) do {:ok, channel} end @impl true + def send_request(%{channel: %{adapter_payload: nil}}, _message, _opts), + do: raise "Can't perform a request without a connection process" + def send_request( %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, message, @@ -80,7 +83,7 @@ defmodule GRPC.Client.Adapters.Mint do opts ) do with stream <- StreamResponseProcess.build_stream(pid), - responses <- Enum.into(stream, []), + responses <- Enum.to_list(stream), :ok <- check_for_error(responses) do {:ok, data} = Enum.find(responses, fn {status, _data} -> status == :ok end) @@ -106,6 +109,9 @@ defmodule GRPC.Client.Adapters.Mint do end @impl true + def send_headers(%{channel: %{adapter_payload: nil}}, _opts), + do: raise "Can't start a client stream without a connection process" + def send_headers(%{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, opts) do headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index af0cac5f..e0fec69e 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -57,4 +57,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do def append_response_data(state, ref, new_data) do update_in(state.requests[ref].response[:data], fn data -> (data || "") <> new_data end) end + + def ger_request_type(state, ref) do + state.requests[ref][:unary_payload_request] + end end From 4bdad6d537f0238eb7c0c63040dc3e3669ff1dd5 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 1 Oct 2022 12:12:51 -0300 Subject: [PATCH 18/82] refact(client/mint_adapter): add cond to handle the four rpc types to receive data and improve readability --- lib/grpc/client/adapters/mint.ex | 149 +++++++++++++----- .../adapters/mint/connection_process/state.ex | 4 - 2 files changed, 109 insertions(+), 44 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 8209e1a3..2b4316b4 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -38,7 +38,7 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def send_request(%{channel: %{adapter_payload: nil}}, _message, _opts), - do: raise "Can't perform a request without a connection process" + do: raise("Can't perform a request without a connection process") def send_request( %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, @@ -63,54 +63,28 @@ defmodule GRPC.Client.Adapters.Mint do end @impl true - def receive_data( - %{server_stream: true, payload: %{response: response, stream_response_pid: pid}}, - opts - ) do - with {:ok, headers} <- response do - stream = StreamResponseProcess.build_stream(pid) + def receive_data(stream, opts) do + cond do + bidirectional_stream?(stream) -> + do_receive_data(stream, :bidirectional_stream, opts) - case opts[:return_headers] do - true -> {:ok, stream, headers} - _any -> {:ok, stream} - end - end - end + unary_request_stream_response?(stream) -> + do_receive_data(stream, :unary_request_stream_response, opts) - # for streamed requests - def receive_data( - %{payload: %{response: {:ok, %{request_ref: _ref}}, stream_response_pid: pid}}, - opts - ) do - with stream <- StreamResponseProcess.build_stream(pid), - responses <- Enum.to_list(stream), - :ok <- check_for_error(responses) do - {:ok, data} = Enum.find(responses, fn {status, _data} -> status == :ok end) + stream_request_unary_response?(stream) -> + do_receive_data(stream, :stream_request_unary_response, opts) - case opts[:return_headers] do - true -> {:ok, data, get_headers(responses) |> append_trailers(responses)} - _any -> {:ok, data} - end - end - end - - def receive_data(%{payload: %{response: response, stream_response_pid: pid}}, opts) do - with {:ok, %{headers: headers}} <- response, - stream <- StreamResponseProcess.build_stream(pid), - responses <- Enum.into(stream, []), - :ok <- check_for_error(responses) do - {:ok, data} = Enum.find(responses, fn {status, _data} -> status == :ok end) + unary_request_response?(stream) -> + do_receive_data(stream, :unary_request_response, opts) - case opts[:return_headers] do - true -> {:ok, data, append_trailers(headers, responses)} - _any -> {:ok, data} - end + true -> + handle_errors_receive_data(stream, opts) end end @impl true def send_headers(%{channel: %{adapter_payload: nil}}, _opts), - do: raise "Can't start a client stream without a connection process" + do: raise("Can't start a client stream without a connection process") def send_headers(%{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, opts) do headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) @@ -185,4 +159,99 @@ defmodule GRPC.Client.Adapters.Mint do {:headers, headers} -> headers end end + + defp do_receive_data(%{payload: %{stream_response_pid: pid}}, :bidirectional_stream, _opts) do + stream = StreamResponseProcess.build_stream(pid) + {:ok, stream} + end + + defp do_receive_data( + %{payload: %{response: {:ok, headers}, stream_response_pid: pid}}, + :unary_request_stream_response, + opts + ) do + stream = StreamResponseProcess.build_stream(pid) + + if opts[:return_headers] do + {:ok, stream, headers} + else + {:ok, stream} + end + end + + defp do_receive_data( + %{payload: %{response: {:ok, %{request_ref: _ref}}, stream_response_pid: pid}}, + :stream_request_unary_response, + opts + ) do + with stream <- StreamResponseProcess.build_stream(pid), + responses <- Enum.to_list(stream), + :ok <- check_for_error(responses) do + data = Keyword.fetch!(responses, :ok) + + if opts[:return_headers] do + {:ok, data, get_headers(responses) |> append_trailers(responses)} + else + {:ok, data} + end + end + end + + defp do_receive_data( + %{payload: %{response: {:ok, %{headers: headers}}, stream_response_pid: pid}}, + :unary_request_response, + opts + ) do + responses = Enum.to_list(StreamResponseProcess.build_stream(pid)) + + with :ok <- check_for_error(responses) do + data = Keyword.fetch!(responses, :ok) + + if(opts[:return_headers]) do + {:ok, data, append_trailers(headers, responses)} + else + {:ok, data} + end + end + end + + def handle_errors_receive_data(_stream, _opts) do + raise "TODO: Implement" + end + + defp bidirectional_stream?(%GRPC.Client.Stream{ + server_stream: true, + payload: %{response: {:ok, %{request_ref: ref}}, stream_response_pid: pid} + }) + when is_reference(ref) and is_pid(pid), + do: true + + defp bidirectional_stream?(_stream), do: false + + defp unary_request_stream_response?(%GRPC.Client.Stream{ + server_stream: true, + payload: %{response: {:ok, _headers}, stream_response_pid: pid} + }) + when is_pid(pid), + do: true + + defp unary_request_stream_response?(_stream), do: false + + defp stream_request_unary_response?(%GRPC.Client.Stream{ + server_stream: false, + payload: %{response: {:ok, %{request_ref: ref}}, stream_response_pid: pid} + }) + when is_pid(pid) and is_reference(ref), + do: true + + defp stream_request_unary_response?(_stream), do: false + + defp unary_request_response?(%GRPC.Client.Stream{ + server_stream: false, + payload: %{response: {:ok, _headers}, stream_response_pid: pid} + }) + when is_pid(pid), + do: true + + defp unary_request_response?(_stream), do: false end diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index e0fec69e..af0cac5f 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -57,8 +57,4 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do def append_response_data(state, ref, new_data) do update_in(state.requests[ref].response[:data], fn data -> (data || "") <> new_data end) end - - def ger_request_type(state, ref) do - state.requests[ref][:unary_payload_request] - end end From 064052deedb900e637356165b391fcb05f48ba4e Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 1 Oct 2022 12:46:20 -0300 Subject: [PATCH 19/82] refact(client/mint_adapter): use keyword.get to return headers and trailers --- lib/grpc/client/adapters/mint.ex | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 2b4316b4..b9a6724a 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -138,25 +138,15 @@ defmodule GRPC.Client.Adapters.Mint do defp mint_scheme(_channel), do: :http def check_for_error(responses) do - error = Enum.find(responses, fn {status, _data} -> status == :error end) - if error == nil, do: :ok, else: error + error = Keyword.get(responses, :error) + + if error, do: error, else: :ok end defp append_trailers(headers, responses) do - responses - |> Enum.find(fn {status, _data} -> status == :trailers end) - |> case do + case Keyword.get(responses, :trailers) do nil -> %{headers: headers} - {:trailers, trailers} -> %{headers: headers, trailers: trailers} - end - end - - defp get_headers(responses) do - responses - |> Enum.find(fn {status, _data} -> status == :headers end) - |> case do - nil -> nil - {:headers, headers} -> headers + trailers -> %{headers: headers, trailers: trailers} end end @@ -190,7 +180,7 @@ defmodule GRPC.Client.Adapters.Mint do data = Keyword.fetch!(responses, :ok) if opts[:return_headers] do - {:ok, data, get_headers(responses) |> append_trailers(responses)} + {:ok, data, append_trailers(Keyword.get(responses, :headers), responses)} else {:ok, data} end From 3b84cc32c0ddbc449f18679bdd9e4636c89c0b95 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 5 Oct 2022 18:25:04 -0300 Subject: [PATCH 20/82] chunk and enqueue requests inside connection process --- interop/mix.lock | 2 + interop/script/run.exs | 4 +- lib/grpc/client/adapters/mint.ex | 113 +++++------ .../connection_process/connection_process.ex | 180 +++++++++++++----- .../adapters/mint/connection_process/state.ex | 45 ++++- .../adapters/mint/stream_response_process.ex | 2 +- lib/grpc/server/adapters/cowboy.ex | 3 +- 7 files changed, 236 insertions(+), 113 deletions(-) diff --git a/interop/mix.lock b/interop/mix.lock index 11f40443..afd797f2 100644 --- a/interop/mix.lock +++ b/interop/mix.lock @@ -7,6 +7,8 @@ "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.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "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"}, diff --git a/interop/script/run.exs b/interop/script/run.exs index 81cb5d53..865b04e8 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -1,9 +1,9 @@ {options, _, _} = OptionParser.parse(System.argv(), strict: [rounds: :integer, concurrency: :integer, port: :integer]) -rounds = Keyword.get(options, :rounds) || 20 +rounds = Keyword.get(options, :rounds) || 2 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 = Keyword.get(options, :log_level) || "debug" level = String.to_existing_atom(level) require Logger diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index b9a6724a..8404c458 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -40,41 +40,24 @@ defmodule GRPC.Client.Adapters.Mint do def send_request(%{channel: %{adapter_payload: nil}}, _message, _opts), do: raise("Can't perform a request without a connection process") - def send_request( - %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, - message, - opts - ) - when is_pid(pid) do - headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + def send_request(stream, message, opts) do {:ok, data, _} = GRPC.Message.to_data(message, opts) - - {:ok, stream_response_pid} = - StreamResponseProcess.start_link(stream, opts[:return_headers] || false) - - response = - ConnectionProcess.request(pid, "POST", path, headers, data, - stream_response_pid: stream_response_pid - ) - - stream - |> GRPC.Client.Stream.put_payload(:response, response) - |> GRPC.Client.Stream.put_payload(:stream_response_pid, stream_response_pid) + do_request(stream, opts, data) end @impl true def receive_data(stream, opts) do cond do - bidirectional_stream?(stream) -> + success_bidi_stream?(stream) -> do_receive_data(stream, :bidirectional_stream, opts) - unary_request_stream_response?(stream) -> + success_server_stream?(stream) -> do_receive_data(stream, :unary_request_stream_response, opts) - stream_request_unary_response?(stream) -> + success_client_stream?(stream) -> do_receive_data(stream, :stream_request_unary_response, opts) - unary_request_response?(stream) -> + success_unary_request?(stream) -> do_receive_data(stream, :unary_request_response, opts) true -> @@ -86,20 +69,8 @@ defmodule GRPC.Client.Adapters.Mint do def send_headers(%{channel: %{adapter_payload: nil}}, _opts), do: raise("Can't start a client stream without a connection process") - def send_headers(%{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, opts) do - headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) - - {:ok, stream_response_pid} = - StreamResponseProcess.start_link(stream, opts[:return_headers] || false) - - response = - ConnectionProcess.request(pid, "POST", path, headers, :stream, - stream_response_pid: stream_response_pid - ) - - stream - |> GRPC.Client.Stream.put_payload(:response, response) - |> GRPC.Client.Stream.put_payload(:stream_response_pid, stream_response_pid) + def send_headers(stream, opts) do + do_request(stream, opts, :stream) end @impl true @@ -188,7 +159,7 @@ defmodule GRPC.Client.Adapters.Mint do end defp do_receive_data( - %{payload: %{response: {:ok, %{headers: headers}}, stream_response_pid: pid}}, + %{payload: %{stream_response_pid: pid}}, :unary_request_response, opts ) do @@ -198,7 +169,7 @@ defmodule GRPC.Client.Adapters.Mint do data = Keyword.fetch!(responses, :ok) if(opts[:return_headers]) do - {:ok, data, append_trailers(headers, responses)} + {:ok, data, append_trailers(Keyword.get(responses, :headers), responses)} else {:ok, data} end @@ -209,39 +180,55 @@ defmodule GRPC.Client.Adapters.Mint do raise "TODO: Implement" end - defp bidirectional_stream?(%GRPC.Client.Stream{ - server_stream: true, - payload: %{response: {:ok, %{request_ref: ref}}, stream_response_pid: pid} - }) - when is_reference(ref) and is_pid(pid), + defp success_bidi_stream?(%GRPC.Client.Stream{ + grpc_type: :bidi_stream, + payload: %{response: {:ok, _resp}} + }), do: true - defp bidirectional_stream?(_stream), do: false + defp success_bidi_stream?(_stream), do: false - defp unary_request_stream_response?(%GRPC.Client.Stream{ - server_stream: true, - payload: %{response: {:ok, _headers}, stream_response_pid: pid} - }) - when is_pid(pid), + defp success_server_stream?(%GRPC.Client.Stream{ + grpc_type: :server_stream, + payload: %{response: {:ok, _resp}} + }), do: true - defp unary_request_stream_response?(_stream), do: false + defp success_server_stream?(_stream), do: false - defp stream_request_unary_response?(%GRPC.Client.Stream{ - server_stream: false, - payload: %{response: {:ok, %{request_ref: ref}}, stream_response_pid: pid} - }) - when is_pid(pid) and is_reference(ref), + defp success_client_stream?(%GRPC.Client.Stream{ + grpc_type: :client_stream, + payload: %{response: {:ok, _resp}} + }), do: true - defp stream_request_unary_response?(_stream), do: false + defp success_client_stream?(_stream), do: false - defp unary_request_response?(%GRPC.Client.Stream{ - server_stream: false, - payload: %{response: {:ok, _headers}, stream_response_pid: pid} - }) - when is_pid(pid), + defp success_unary_request?(%GRPC.Client.Stream{ + grpc_type: :unary, + payload: %{response: {:ok, _resp}} + }), do: true - defp unary_request_response?(_stream), do: false + defp success_unary_request?(_stream), do: false + + defp do_request( + %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, + opts, + body + ) do + headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + + {:ok, stream_response_pid} = + StreamResponseProcess.start_link(stream, opts[:return_headers] || false) + + response = + ConnectionProcess.request(pid, "POST", path, headers, body, + stream_response_pid: stream_response_pid + ) + + stream + |> GRPC.Client.Stream.put_payload(:response, response) + |> GRPC.Client.Stream.put_payload(:stream_response_pid, stream_response_pid) + end end diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 81156430..efd2cf71 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -55,11 +55,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do new_state = state |> State.update_conn(conn) - |> State.put_in_ref(request_ref, %{ - stream_response_pid: opts[:stream_response_pid], - done: false, - response: %{} - }) + |> State.put_empty_ref_state(request_ref, opts[:stream_response_pid]) {:reply, {:ok, %{request_ref: request_ref}}, new_state} @@ -71,22 +67,21 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do def handle_call( {:request, method, path, headers, body, opts}, - from, + _from, state ) do - case Mint.HTTP.request(state.conn, method, path, headers, body) do + case Mint.HTTP.request(state.conn, method, path, headers, :stream) do {:ok, conn, request_ref} -> + queue = :queue.in({request_ref, body, nil}, state.request_stream_queue) + new_state = state |> State.update_conn(conn) - |> State.put_in_ref(request_ref, %{ - from: from, - stream_response_pid: opts[:stream_response_pid], - done: false, - response: %{} - }) + |> State.update_request_stream_queue(queue) + |> State.put_empty_ref_state(request_ref, opts[:stream_response_pid]) - {:noreply, new_state} + {:reply, {:ok, %{request_ref: request_ref}}, new_state, + {:continue, :process_request_stream_queue}} {:error, conn, reason} -> new_state = State.update_conn(state, conn) @@ -94,8 +89,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end end - def handle_call({:stream_body, request_ref, body}, _from, state) do - case Mint.HTTP.stream_request_body(state.conn, request_ref, body) do + def handle_call({:stream_body, request_ref, :eof}, _from, state) do + case Mint.HTTP.stream_request_body(state.conn, request_ref, :eof) do {:ok, conn} -> {:reply, :ok, State.update_conn(state, conn)} @@ -104,6 +99,13 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end end + def handle_call({:stream_body, request_ref, body}, from, state) do + queue = :queue.in({request_ref, body, from}, state.request_stream_queue) + + {:noreply, State.update_request_stream_queue(state, queue), + {:continue, :process_request_stream_queue}} + end + @impl true def handle_info(message, state) do case Mint.HTTP.stream(state.conn, message) do @@ -111,45 +113,56 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do Logger.debug(fn -> "Received unknown message: " <> inspect(message) end) {:noreply, state} + {:ok, conn, [] = _responses} -> + check_request_stream_queue(State.update_conn(state, conn)) + {:ok, conn, responses} -> state = State.update_conn(state, conn) state = Enum.reduce(responses, state, &process_response/2) - {:noreply, state} + + check_request_stream_queue(state) + end + end + + @impl true + def handle_continue(:process_request_stream_queue, state) do + {{:value, request}, queue} = :queue.out(state.request_stream_queue) + {ref, body, _from} = request + window_size = get_window_size(state.conn, ref) + body_size = IO.iodata_length(body) + dequeued_state = State.update_request_stream_queue(state, queue) + + cond do + window_size == 0 -> {:noreply, state} + body_size > window_size -> chunk_body_and_enqueue_rest(request, dequeued_state) + true -> stream_body_and_reply(request, dequeued_state) end end + @impl true + def terminate(_reason, _state) do + :normal + end + defp process_response({:status, request_ref, status}, state) do State.update_response_status(state, request_ref, status) end defp process_response({:headers, request_ref, headers}, state) do - empty_headers? = State.empty_headers?(state, request_ref) - - case empty_headers? do - true -> - new_state = State.update_response_headers(state, request_ref, headers) - response = State.get_response(new_state, request_ref) - - new_state - |> State.caller_process(request_ref) - |> case do - nil -> - new_state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:headers, headers) - - ref -> - GenServer.reply(ref, {:ok, response}) - end + if State.empty_headers?(state, request_ref) do + new_state = State.update_response_headers(state, request_ref, headers) - new_state + new_state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:headers, headers) - false -> - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:trailers, headers) + new_state + else + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:trailers, headers) - state + state end end @@ -170,8 +183,87 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do new_state end - @impl true - def terminate(_reason, _state) do - :normal + defp chunk_body_and_enqueue_rest({request_ref, body, from}, state) do + {head, tail} = chunk_body(body, get_window_size(state.conn, request_ref)) + + case Mint.HTTP.stream_request_body(state.conn, request_ref, head) do + {:ok, conn} -> + queue = :queue.in_r({request_ref, tail, from}, state.request_stream_queue) + + new_state = + state + |> State.update_conn(conn) + |> State.update_request_stream_queue(queue) + + {:noreply, new_state} + + {:error, conn, error} -> + if is_reference(from) do + GenServer.reply(from, {:error, error}) + else + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:error, error) + end + + {:noreply, State.update_conn(state, conn)} + end + end + + defp stream_body_and_reply({request_ref, body, from}, state) do + send_eof? = from == nil + + case stream_body(state.conn, request_ref, body, send_eof?) do + {:ok, conn} -> + if is_reference(from) do + GenServer.reply(from, :ok) + end + + check_request_stream_queue(State.update_conn(state, conn)) + + {:error, conn, error} -> + if is_reference(from) do + GenServer.reply(from, {:error, error}) + else + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:error, error) + end + + check_request_stream_queue(State.update_conn(state, conn)) + end + end + + defp stream_body(conn, request_ref, body, true = _stream_eof?) do + with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, request_ref, body), + {:ok, conn} <- Mint.HTTP.stream_request_body(conn, request_ref, :eof) do + {:ok, conn} + end + end + + defp stream_body(conn, request_ref, body, false = _stream_eof?) do + with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, request_ref, body) do + {:ok, conn} + end + end + + def check_request_stream_queue(state) do + if :queue.is_empty(state.request_stream_queue) do + {:noreply, state} + else + {:noreply, state, {:continue, :process_request_stream_queue}} + end + end + + defp chunk_body(body, bytes_length) do + <> = body + {head, tail} + end + + def get_window_size(conn, ref) do + min( + Mint.HTTP2.get_window_size(conn, {:request, ref}), + Mint.HTTP2.get_window_size(conn, :connection) + ) end end diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index af0cac5f..f85337db 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -1,5 +1,5 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do - defstruct [:conn, requests: %{}] + defstruct [:conn, requests: %{}, request_stream_queue: :queue.new()] @type t :: %__MODULE__{ conn: Mint.HTTP.t(), @@ -7,13 +7,26 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do } def new(conn) do - %__MODULE__{conn: conn} + %__MODULE__{conn: conn, request_stream_queue: :queue.new()} end def update_conn(state, conn) do %{state | conn: conn} end + def update_request_stream_queue(state, queue) do + %{state | request_stream_queue: queue} + end + + def put_empty_ref_state(state, ref, response_pid) do + put_in(state.requests[ref], %{ + stream_response_pid: response_pid, + done: false, + response: %{}, + request: %{from: nil, stream_queue: []} + }) + end + def put_in_ref(state, ref, ref_state) do put_in(state.requests[ref], ref_state) end @@ -50,6 +63,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do state.requests[ref][:response] end + def get_request_stream_by_ref(state, ref) do + state.requests[ref][:request] + end + def pop_ref(state, ref) do pop_in(state.requests[ref]) end @@ -57,4 +74,28 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do def append_response_data(state, ref, new_data) do update_in(state.requests[ref].response[:data], fn data -> (data || "") <> new_data end) end + + def enqueue_request_to_stream(state, ref, body) do + update_in(state.requests[ref].request[:stream_queue], fn queue -> queue ++ [body] end) + end + + def stream_queue_not_empty?(state, ref) do + state.requests[ref].request[:stream_queue] != [] + end + + def put_request_back_to_stream_queue(state, ref, body) do + update_in(state.requests[ref].request[:stream_queue], fn queue -> [body | queue] end) + end + + def update_request_reference(state, ref, from) do + put_in(state.requests[ref].request[:from], from) + end + + def update_request_queue(state, ref, queue) do + put_in(state.requests[ref].request[:stream_queue], queue) + end + + def update_request(state, ref, request) do + put_in(state.requests[ref].request, request) + end end diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 48f210d5..f8f29ece 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -78,7 +78,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:noreply, new_state, {:continue, :produce_response}} _ -> - new_state = %{state | bufferr: buffer <> data} + new_state = %{state | buffer: buffer <> data} {:noreply, new_state, {:continue, :produce_response}} end end diff --git a/lib/grpc/server/adapters/cowboy.ex b/lib/grpc/server/adapters/cowboy.ex index 4a80c332..40fc6971 100644 --- a/lib/grpc/server/adapters/cowboy.ex +++ b/lib/grpc/server/adapters/cowboy.ex @@ -187,7 +187,8 @@ defmodule GRPC.Server.Adapters.Cowboy do # https://github.com/ninenines/cowboy/issues/1398 # If there are 1000 streams in one connection, then 1000/s frames per stream. max_received_frame_rate: {10_000_000, 10_000}, - max_reset_stream_rate: {10_000, 10_000} + max_reset_stream_rate: {10_000, 10_000}, + stream_window_update_threshold: 65_500 }, Enum.into(opts, %{}) ) From 132045d9b4ea1636f604aef4a985766b464c1a11 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 5 Oct 2022 18:30:54 -0300 Subject: [PATCH 21/82] remove unused functions from state module --- .../adapters/mint/connection_process/state.ex | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index f85337db..00afda74 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -22,15 +22,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do put_in(state.requests[ref], %{ stream_response_pid: response_pid, done: false, - response: %{}, - request: %{from: nil, stream_queue: []} + response: %{} }) end - def put_in_ref(state, ref, ref_state) do - put_in(state.requests[ref], ref_state) - end - def update_response_status(state, ref, status) do put_in(state.requests[ref].response[:status], status) end @@ -39,14 +34,6 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do put_in(state.requests[ref].response[:headers], headers) end - def update_response_trailers(state, ref, trailers) do - put_in(state.requests[ref].response[:trailers], trailers) - end - - def steamed_response?(state, ref) do - state.requests[ref].streamed_response - end - def empty_headers?(state, ref) do is_nil(state.requests[ref].response[:headers]) end @@ -55,18 +42,6 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do state.requests[ref].stream_response_pid end - def caller_process(state, ref) do - state.requests[ref][:from] - end - - def get_response(state, ref) do - state.requests[ref][:response] - end - - def get_request_stream_by_ref(state, ref) do - state.requests[ref][:request] - end - def pop_ref(state, ref) do pop_in(state.requests[ref]) end @@ -74,28 +49,4 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do def append_response_data(state, ref, new_data) do update_in(state.requests[ref].response[:data], fn data -> (data || "") <> new_data end) end - - def enqueue_request_to_stream(state, ref, body) do - update_in(state.requests[ref].request[:stream_queue], fn queue -> queue ++ [body] end) - end - - def stream_queue_not_empty?(state, ref) do - state.requests[ref].request[:stream_queue] != [] - end - - def put_request_back_to_stream_queue(state, ref, body) do - update_in(state.requests[ref].request[:stream_queue], fn queue -> [body | queue] end) - end - - def update_request_reference(state, ref, from) do - put_in(state.requests[ref].request[:from], from) - end - - def update_request_queue(state, ref, queue) do - put_in(state.requests[ref].request[:stream_queue], queue) - end - - def update_request(state, ref, request) do - put_in(state.requests[ref].request, request) - end end From db5ffb3909ffd12c3c01d785fe3ee1544e681a85 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 6 Oct 2022 09:09:59 -0300 Subject: [PATCH 22/82] fix reference check for response --- interop/script/run.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interop/script/run.exs b/interop/script/run.exs index 865b04e8..81cb5d53 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -1,9 +1,9 @@ {options, _, _} = OptionParser.parse(System.argv(), strict: [rounds: :integer, concurrency: :integer, port: :integer]) -rounds = Keyword.get(options, :rounds) || 2 +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) || "debug" +level = Keyword.get(options, :log_level) || "warn" level = String.to_existing_atom(level) require Logger From a84d8031fa3908e1831503cf5a8f22f54249a129 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 6 Oct 2022 09:17:36 -0300 Subject: [PATCH 23/82] add consume_error to stream response process for error handling --- examples/route_guide/lib/client.ex | 18 +++++++++--------- .../connection_process/connection_process.ex | 7 ++++--- .../adapters/mint/stream_response_process.ex | 14 +++++++++++++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex index 5d33f4ba..482242b8 100644 --- a/examples/route_guide/lib/client.ex +++ b/examples/route_guide/lib/client.ex @@ -1,16 +1,16 @@ defmodule RouteGuide.Client do def main(channel) do - print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) - print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) + # print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) + # print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) # Looking for features between 40, -75 and 42, -73. - print_features( - channel, - Routeguide.Rectangle.new( - lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), - hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) - ) - ) + # print_features( + # channel, + # Routeguide.Rectangle.new( + # lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), + # hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) + # ) + # ) run_record_route(channel) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index efd2cf71..97fd71c3 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -133,6 +133,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do dequeued_state = State.update_request_stream_queue(state, queue) cond do + # Do nothing, wait for server (on stream/2) to give us more window size window_size == 0 -> {:noreply, state} body_size > window_size -> chunk_body_and_enqueue_rest(request, dequeued_state) true -> stream_body_and_reply(request, dequeued_state) @@ -198,7 +199,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:noreply, new_state} {:error, conn, error} -> - if is_reference(from) do + if from != nil do GenServer.reply(from, {:error, error}) else state @@ -215,14 +216,14 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do case stream_body(state.conn, request_ref, body, send_eof?) do {:ok, conn} -> - if is_reference(from) do + if from != nil do GenServer.reply(from, :ok) end check_request_stream_queue(State.update_conn(state, conn)) {:error, conn, error} -> - if is_reference(from) do + if from != nil do GenServer.reply(from, {:error, error}) else state diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index f8f29ece..d7bfc66a 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -30,7 +30,11 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do @doc """ Cast a message to process to consume an incoming data or trailers """ - @spec consume(pid(), :data | :trailers | :headers, binary() | Mint.Types.headers()) :: :ok + @spec consume( + pid(), + :data | :trailers | :headers | :error, + binary() | Mint.Types.headers() | Mint.Types.error() + ) :: :ok def consume(pid, :data, data) do GenServer.cast(pid, {:consume_response, {:data, data}}) end @@ -43,6 +47,10 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do GenServer.cast(pid, {:consume_response, {:headers, headers}}) end + def consume(pid, :error, error) do + GenServer.cast(pid, {:consume_response, {:error, error}}) + end + # Callbacks def init({stream, send_headers_or_trailers?}) do @@ -114,6 +122,10 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:noreply, state, {:continue, :produce_response}} end + def handle_cast({:consume_response, {:error, _error} = error}, %{responses: responses} = state) do + {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} + end + def handle_cast({:consume_response, :done}, state) do {:noreply, %{state | done: true}, {:continue, :produce_response}} end From 301c01604952659a39d910689817469f57cd8afb Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 6 Oct 2022 09:37:45 -0300 Subject: [PATCH 24/82] remove comments from route guide examples --- examples/route_guide/lib/client.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex index 482242b8..5d33f4ba 100644 --- a/examples/route_guide/lib/client.ex +++ b/examples/route_guide/lib/client.ex @@ -1,16 +1,16 @@ defmodule RouteGuide.Client do def main(channel) do - # print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) - # print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) + print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906)) + print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0)) # Looking for features between 40, -75 and 42, -73. - # print_features( - # channel, - # Routeguide.Rectangle.new( - # lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), - # hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) - # ) - # ) + print_features( + channel, + Routeguide.Rectangle.new( + lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000), + hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) + ) + ) run_record_route(channel) From 267f6ad8fd8b33fb7d1e9d57bd8a59d669437a8b Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 6 Oct 2022 09:44:51 -0300 Subject: [PATCH 25/82] replace case in favor of if/else --- lib/grpc/client/adapters/gun.ex | 1 + .../client/adapters/mint/stream_response_process.ex | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index 8472e667..a4c7f969 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -144,6 +144,7 @@ defmodule GRPC.Client.Adapters.Gun do ) do 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 diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index d7bfc66a..196e322b 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -152,13 +152,11 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do decoded_trailers = GRPC.Transport.HTTP2.decode_headers(trailers) status = String.to_integer(decoded_trailers["grpc-status"]) - case status == GRPC.Status.ok() do - true -> - {:trailers, decoded_trailers} - - false -> - rpc_error = %GRPC.RPCError{status: status, message: decoded_trailers["grpc-message"]} - {:error, rpc_error} + if status == GRPC.Status.ok() do + {:trailers, decoded_trailers} + else + rpc_error = %GRPC.RPCError{status: status, message: decoded_trailers["grpc-message"]} + {:error, rpc_error} end end From 3e0dc0971d89d769bb21c926a83465ad3d8ee45c Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 6 Oct 2022 10:43:16 -0300 Subject: [PATCH 26/82] fix disconnect match on mint adapter --- .../adapters/mint/connection_process/connection_process.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 97fd71c3..047c4522 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -42,7 +42,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do @impl true def handle_call({:disconnect, :brutal}, _from, state) do # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests - {:stop, :normal, Mint.HTTP.close(state.conn), state} + {:ok, conn} = Mint.HTTP.close(state.conn) + {:stop, :normal, :ok, State.update_conn(state, conn)} end def handle_call( From 4193f68a1ac6a4e215186f194451d3dac28d1c50 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 6 Oct 2022 11:29:20 -0300 Subject: [PATCH 27/82] improve error handling for response stream --- lib/grpc/client/adapters/mint.ex | 2 +- .../adapters/mint/stream_response_process.ex | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 8404c458..09298f03 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -111,7 +111,7 @@ defmodule GRPC.Client.Adapters.Mint do def check_for_error(responses) do error = Keyword.get(responses, :error) - if error, do: error, else: :ok + if error, do: {:error, error}, else: :ok end defp append_trailers(headers, responses) do diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 196e322b..f59ccbef 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -95,7 +95,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:consume_response, {:trailers, trailers}}, %{send_headers_or_trailers: true, responses: responses} = state ) do - new_responses = [get_trailers_response(trailers) | responses] + new_responses = [get_headers_response(trailers, :trailers) | responses] {:noreply, %{state | responses: new_responses}, {:continue, :produce_response}} end @@ -103,7 +103,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:consume_response, {:trailers, trailers}}, %{send_headers_or_trailers: false, responses: responses} = state ) do - with {:errors, _rpc_error} = error <- get_trailers_response(trailers) do + with {:error, _rpc_error} = error <- get_headers_response(trailers, :trailers) do {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} else _any -> {:noreply, state, {:continue, :produce_response}} @@ -114,12 +114,16 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:consume_response, {:headers, headers}}, %{send_headers_or_trailers: true, responses: responses} = state ) do - {:noreply, %{state | responses: [{:headers, headers} | responses]}, + {:noreply, %{state | responses: [get_headers_response(headers, :headers) | responses]}, {:continue, :produce_response}} end - def handle_cast({:consume_response, {:headers, _headers}}, state) do - {:noreply, state, {:continue, :produce_response}} + def handle_cast({:consume_response, {:headers, headers}}, %{responses: responses} = state) do + with {:error, _rpc_error} = error <- get_headers_response(headers, :headers) do + {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} + else + _any -> {:noreply, state, {:continue, :produce_response}} + end end def handle_cast({:consume_response, {:error, _error} = error}, %{responses: responses} = state) do @@ -148,12 +152,12 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do end end - defp get_trailers_response(trailers) do - decoded_trailers = GRPC.Transport.HTTP2.decode_headers(trailers) - status = String.to_integer(decoded_trailers["grpc-status"]) + defp get_headers_response(headers, type) do + decoded_trailers = GRPC.Transport.HTTP2.decode_headers(headers) + status = String.to_integer(decoded_trailers["grpc-status"] || "0") if status == GRPC.Status.ok() do - {:trailers, decoded_trailers} + {type, decoded_trailers} else rpc_error = %GRPC.RPCError{status: status, message: decoded_trailers["grpc-message"]} {:error, rpc_error} From 47744a0762cd5de50d206b020e601678c15cf37b Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 8 Oct 2022 13:22:36 -0300 Subject: [PATCH 28/82] refact response module to use guards to return headers or trailers --- lib/grpc/client/adapters/mint.ex | 67 +++++-------------- .../adapters/mint/stream_response_process.ex | 42 +++++------- 2 files changed, 35 insertions(+), 74 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 09298f03..3b2634cb 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -108,68 +108,25 @@ defmodule GRPC.Client.Adapters.Mint do defp mint_scheme(%Channel{scheme: "https"} = _channel), do: :https defp mint_scheme(_channel), do: :http - def check_for_error(responses) do - error = Keyword.get(responses, :error) - - if error, do: {:error, error}, else: :ok - end - - defp append_trailers(headers, responses) do - case Keyword.get(responses, :trailers) do - nil -> %{headers: headers} - trailers -> %{headers: headers, trailers: trailers} - end - end - - defp do_receive_data(%{payload: %{stream_response_pid: pid}}, :bidirectional_stream, _opts) do + defp do_receive_data(%{payload: %{stream_response_pid: pid}}, request_type, _opts) + when request_type in [:bidirectional_stream, :unary_request_stream_response] do stream = StreamResponseProcess.build_stream(pid) {:ok, stream} end defp do_receive_data( - %{payload: %{response: {:ok, headers}, stream_response_pid: pid}}, - :unary_request_stream_response, - opts - ) do - stream = StreamResponseProcess.build_stream(pid) - - if opts[:return_headers] do - {:ok, stream, headers} - else - {:ok, stream} - end - end - - defp do_receive_data( - %{payload: %{response: {:ok, %{request_ref: _ref}}, stream_response_pid: pid}}, - :stream_request_unary_response, + %{payload: %{stream_response_pid: pid}}, + request_type, opts - ) do + ) + when request_type in [:stream_request_unary_response, :unary_request_response] do with stream <- StreamResponseProcess.build_stream(pid), responses <- Enum.to_list(stream), :ok <- check_for_error(responses) do data = Keyword.fetch!(responses, :ok) if opts[:return_headers] do - {:ok, data, append_trailers(Keyword.get(responses, :headers), responses)} - else - {:ok, data} - end - end - end - - defp do_receive_data( - %{payload: %{stream_response_pid: pid}}, - :unary_request_response, - opts - ) do - responses = Enum.to_list(StreamResponseProcess.build_stream(pid)) - - with :ok <- check_for_error(responses) do - data = Keyword.fetch!(responses, :ok) - - if(opts[:return_headers]) do - {:ok, data, append_trailers(Keyword.get(responses, :headers), responses)} + {:ok, data, get_headers_and_trailers(responses)} else {:ok, data} end @@ -231,4 +188,14 @@ defmodule GRPC.Client.Adapters.Mint do |> GRPC.Client.Stream.put_payload(:response, response) |> GRPC.Client.Stream.put_payload(:stream_response_pid, stream_response_pid) end + + defp get_headers_and_trailers(responses) do + %{headers: Keyword.get(responses, :headers), trailers: Keyword.get(responses, :trailers)} + end + + def check_for_error(responses) do + error = Keyword.get(responses, :error) + + if error, do: {:error, error}, else: :ok + end end diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index f59ccbef..f5093b86 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -1,12 +1,20 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do + @moduledoc """ + This module represents the process responsible for consuming the + incoming messages from a connection. For each request, there will be + a process responsible for consuming its messages. At the end of a stream + this process will automatically be killed. + """ + use GenServer + @spec start_link(GRPC.Client.Stream.t(), send_headers_or_trailers? :: boolean()) :: GenServer.on_start() def start_link(stream, send_headers_or_trailers?) do GenServer.start_link(__MODULE__, {stream, send_headers_or_trailers?}) end @doc """ - Given a pid, builds a Stream that will consume the accumulated + Given a pid from this process, build an Elixir.Stream that will consume the accumulated data inside this process """ @spec build_stream(pid()) :: Elixir.Stream.t() @@ -21,6 +29,8 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do @doc """ Cast a message to process to inform that the stream has finished + once all messages are produced. This process will automatically + be killed. """ @spec done(pid()) :: :ok def done(pid) do @@ -28,7 +38,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do end @doc """ - Cast a message to process to consume an incoming data or trailers + Consume an incoming data or trailers/headers """ @spec consume( pid(), @@ -92,34 +102,18 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do end def handle_cast( - {:consume_response, {:trailers, trailers}}, + {:consume_response, {type, trailers}}, %{send_headers_or_trailers: true, responses: responses} = state - ) do - new_responses = [get_headers_response(trailers, :trailers) | responses] + ) when type in [:headers, :trailers] do + new_responses = [get_headers_response(trailers, type) | responses] {:noreply, %{state | responses: new_responses}, {:continue, :produce_response}} end def handle_cast( - {:consume_response, {:trailers, trailers}}, + {:consume_response, {type, trailers}}, %{send_headers_or_trailers: false, responses: responses} = state - ) do - with {:error, _rpc_error} = error <- get_headers_response(trailers, :trailers) do - {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} - else - _any -> {:noreply, state, {:continue, :produce_response}} - end - end - - def handle_cast( - {:consume_response, {:headers, headers}}, - %{send_headers_or_trailers: true, responses: responses} = state - ) do - {:noreply, %{state | responses: [get_headers_response(headers, :headers) | responses]}, - {:continue, :produce_response}} - end - - def handle_cast({:consume_response, {:headers, headers}}, %{responses: responses} = state) do - with {:error, _rpc_error} = error <- get_headers_response(headers, :headers) do + ) when type in [:headers, :trailers] do + with {:error, _rpc_error} = error <- get_headers_response(trailers, type) do {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} else _any -> {:noreply, state, {:continue, :produce_response}} From 965a7cc8797a305b5350a8a8ea30b70b2a2caa19 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 8 Oct 2022 16:43:20 -0300 Subject: [PATCH 29/82] add factories for channel and stream, remove old factory code to use ex_machina, add tests to cast calls for stream_response --- .../adapters/mint/stream_response_process.ex | 45 ++--- mix.exs | 4 +- mix.lock | 2 + .../{adapter => client/adapters}/gun_test.exs | 4 +- .../mint/stream_response_process_test.exs | 169 ++++++++++++++++++ test/support/data_case.ex | 9 + test/support/factories/channel.ex | 41 +++++ test/support/factories/client/stream.ex | 29 +++ test/support/factories/proto/hello_world.ex | 9 + test/support/factory.ex | 51 +----- test/support/{ => proto}/helloworld.pb.ex | 0 test/support/{ => proto}/helloworld.proto | 0 test/support/{ => proto}/route_guide.pb.ex | 0 test/support/{ => proto}/route_guide.proto | 0 test/support/test_adapter.exs | 2 + test/test_helper.exs | 1 + 16 files changed, 290 insertions(+), 76 deletions(-) rename test/grpc/{adapter => client/adapters}/gun_test.exs (98%) create mode 100644 test/grpc/client/adapters/mint/stream_response_process_test.exs create mode 100644 test/support/data_case.ex create mode 100644 test/support/factories/channel.ex create mode 100644 test/support/factories/client/stream.ex create mode 100644 test/support/factories/proto/hello_world.ex rename test/support/{ => proto}/helloworld.pb.ex (100%) rename test/support/{ => proto}/helloworld.proto (100%) rename test/support/{ => proto}/route_guide.pb.ex (100%) rename test/support/{ => proto}/route_guide.proto (100%) diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index f5093b86..6ed0ba07 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -6,9 +6,16 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do this process will automatically be killed. """ + @typep accepted_types :: :data | :trailers | :headers | :error + @typep data_types :: binary() | Mint.Types.headers() | Mint.Types.error() + + @accepted_types [:data, :trailers, :headers, :error] + @header_types [:headers, :trailers] + use GenServer - @spec start_link(GRPC.Client.Stream.t(), send_headers_or_trailers? :: boolean()) :: GenServer.on_start() + @spec start_link(GRPC.Client.Stream.t(), send_headers_or_trailers? :: boolean()) :: + GenServer.on_start() def start_link(stream, send_headers_or_trailers?) do GenServer.start_link(__MODULE__, {stream, send_headers_or_trailers?}) end @@ -40,25 +47,9 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do @doc """ Consume an incoming data or trailers/headers """ - @spec consume( - pid(), - :data | :trailers | :headers | :error, - binary() | Mint.Types.headers() | Mint.Types.error() - ) :: :ok - def consume(pid, :data, data) do - GenServer.cast(pid, {:consume_response, {:data, data}}) - end - - def consume(pid, :trailers, trailers) do - GenServer.cast(pid, {:consume_response, {:trailers, trailers}}) - end - - def consume(pid, :headers, headers) do - GenServer.cast(pid, {:consume_response, {:headers, headers}}) - end - - def consume(pid, :error, error) do - GenServer.cast(pid, {:consume_response, {:error, error}}) + @spec consume(pid(), type :: accepted_types, data :: data_types) :: :ok + def consume(pid, type, data) when type in @accepted_types do + GenServer.cast(pid, {:consume_response, {type, data}}) end # Callbacks @@ -102,18 +93,20 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do end def handle_cast( - {:consume_response, {type, trailers}}, + {:consume_response, {type, headers}}, %{send_headers_or_trailers: true, responses: responses} = state - ) when type in [:headers, :trailers] do - new_responses = [get_headers_response(trailers, type) | responses] + ) + when type in @header_types do + new_responses = [get_headers_response(headers, type) | responses] {:noreply, %{state | responses: new_responses}, {:continue, :produce_response}} end def handle_cast( - {:consume_response, {type, trailers}}, + {:consume_response, {type, headers}}, %{send_headers_or_trailers: false, responses: responses} = state - ) when type in [:headers, :trailers] do - with {:error, _rpc_error} = error <- get_headers_response(trailers, type) do + ) + when type in @header_types do + with {:error, _rpc_error} = error <- get_headers_response(headers, type) do {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} else _any -> {:noreply, state, {:continue, :produce_response}} diff --git a/mix.exs b/mix.exs index 05b93764..acbc6bff 100644 --- a/mix.exs +++ b/mix.exs @@ -47,7 +47,9 @@ defmodule GRPC.Mixfile do {: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} + {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, + {:ex_machina, "~> 2.7.0", only: :test}, + {:ex_parameterized, "~> 1.3.7", only: :test} ] end diff --git a/mix.lock b/mix.lock index e6d75a14..9ffaf6d0 100644 --- a/mix.lock +++ b/mix.lock @@ -6,6 +6,8 @@ "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.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"}, + "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, + "ex_parameterized": {:hex, :ex_parameterized, "1.3.7", "801f85fc4651cb51f11b9835864c6ed8c5e5d79b1253506b5bb5421e8ab2f050", [:mix], [], "hexpm", "1fb0dc4aa9e8c12ae23806d03bcd64a5a0fc9cd3f4c5602ba72561c9b54a625c"}, "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, diff --git a/test/grpc/adapter/gun_test.exs b/test/grpc/client/adapters/gun_test.exs similarity index 98% rename from test/grpc/adapter/gun_test.exs rename to test/grpc/client/adapters/gun_test.exs index b3d089e0..405a2a3d 100644 --- a/test/grpc/adapter/gun_test.exs +++ b/test/grpc/client/adapters/gun_test.exs @@ -1,7 +1,5 @@ defmodule GRPC.Client.Adapters.GunTest do - use ExUnit.Case, async: true - - import GRPC.Factory + use GRPC.DataCase, async: true alias GRPC.Client.Adapters.Gun diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs new file mode 100644 index 00000000..79e70201 --- /dev/null +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -0,0 +1,169 @@ +defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do + use GRPC.DataCase + use ExUnit.Parameterized + + alias GRPC.Client.Adapters.Mint.StreamResponseProcess + + setup do + state = %{ + buffer: "", + done: false, + from: nil, + grpc_stream: build(:client_stream), + responses: [], + send_headers_or_trailers: false + } + + %{state: state} + end + + describe "handle_cast/2 - data" do + setup do + part_1 = <<0, 0, 0, 0, 12, 10, 10, 72, 101, 108>> + part_2 = <<108, 111, 32, 76, 117, 105, 115>> + full_message = part_1 <> part_2 + %{data: {part_1, part_2, full_message}} + end + + test "append message to buffer when message is incomplete", %{ + state: state, + data: {part1, _, _} + } do + response = StreamResponseProcess.handle_cast({:consume_response, {:data, part1}}, state) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == part1 + end + + test "decode full message when incoming date is complete", %{ + state: state, + data: {_, _, full_message} + } do + expected_response_message = build(:hello_reply_rpc) + + response = + StreamResponseProcess.handle_cast({:consume_response, {:data, full_message}}, state) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == <<>> + assert [{:ok, response_message}] = new_state.responses + assert expected_response_message == response_message + end + + test "append incoming message to existing buffer", %{state: state, data: {part1, part2, _}} do + state = %{state | buffer: part1} + expected_response_message = build(:hello_reply_rpc) + response = StreamResponseProcess.handle_cast({:consume_response, {:data, part2}}, state) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == <<>> + assert [{:ok, response_message}] = new_state.responses + assert expected_response_message == response_message + end + + test "decode message and put rest on buffer", %{state: state, data: {_, _, full}} do + extra_data = <<0, 1, 2>> + data = full <> extra_data + expected_response_message = build(:hello_reply_rpc) + response = StreamResponseProcess.handle_cast({:consume_response, {:data, data}}, state) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == extra_data + assert [{:ok, response_message}] = new_state.responses + assert expected_response_message == response_message + end + end + + describe "handle_cast/2 - headers/trailers" do + test_with_params( + "put error in responses when incoming headers has error status", + %{state: state}, + fn %{type: type, is_header_enabled: header_enabled?} -> + state = %{state | send_headers_or_trailers: header_enabled?} + + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", "Internal Server Error"}, + {"grpc-status", "2"}, + {"server", "Cowboy"} + ] + + response = + StreamResponseProcess.handle_cast( + {:consume_response, {type, headers}}, + state + ) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert [{:error, error}] = new_state.responses + assert %GRPC.RPCError{message: "Internal Server Error", status: 2} == error + end, + do: [ + {%{type: :headers, is_header_enabled: false}}, + {%{type: :headers, is_header_enabled: true}}, + {%{type: :trailers, is_header_enabled: true}}, + {%{type: :trailers, is_header_enabled: false}} + ] + ) + + test_with_params( + "append headers to response when headers are enabled", + %{state: state}, + fn type -> + state = %{state | send_headers_or_trailers: true} + + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"} + ] + + response = + StreamResponseProcess.handle_cast( + {:consume_response, {type, headers}}, + state + ) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert [{type_response, response_headers}] = new_state.responses + assert type == type_response + + assert %{ + "content-length" => "0", + "content-type" => "application/grpc+proto", + "grpc-message" => "", + "grpc-status" => "0", + "server" => "Cowboy" + } == response_headers + end, + do: [{:headers}, {:trailers}] + ) + + test_with_params( + "skip produce headers when flag is disabled and there are no errors", + %{state: state}, + fn type -> + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"} + ] + + response = + StreamResponseProcess.handle_cast( + {:consume_response, {type, headers}}, + state + ) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert [] == new_state.responses + end, + do: [{:headers}, {:trailers}] + ) + end +end diff --git a/test/support/data_case.ex b/test/support/data_case.ex new file mode 100644 index 00000000..8e0de59d --- /dev/null +++ b/test/support/data_case.ex @@ -0,0 +1,9 @@ +defmodule GRPC.DataCase do + use ExUnit.CaseTemplate + + using do + quote do + import GRPC.Factory + end + end +end diff --git a/test/support/factories/channel.ex b/test/support/factories/channel.ex new file mode 100644 index 00000000..0e479137 --- /dev/null +++ b/test/support/factories/channel.ex @@ -0,0 +1,41 @@ +defmodule GRPC.Factories.Channel do + alias GRPC.Channel + alias GRPC.Credential + + defmacro __using__(_opts) do + quote do + 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 + 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)) + + %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 + end +end diff --git a/test/support/factories/client/stream.ex b/test/support/factories/client/stream.ex new file mode 100644 index 00000000..69070a34 --- /dev/null +++ b/test/support/factories/client/stream.ex @@ -0,0 +1,29 @@ +defmodule GRPC.Factories.Client.Stream do + defmacro __using__(_opts) do + quote do + def client_stream_factory do + %GRPC.Client.Stream{ + __interface__: %{ + receive_data: &GRPC.Client.Stream.receive_data/2, + send_request: &GRPC.Client.Stream.send_request/3 + }, + accepted_compressors: [], + canceled: false, + channel: build(:channel, adapter: GRPC.Client.Adapters.Mint), + codec: GRPC.Codec.Proto, + compressor: nil, + grpc_type: :unary, + headers: %{}, + method_name: "SayHello", + path: "/helloworld.Greeter/SayHello", + payload: %{}, + request_mod: Helloworld.HelloRequest, + response_mod: Helloworld.HelloReply, + rpc: {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}}, + server_stream: false, + service_name: "helloworld.Greeter" + } + end + end + end +end diff --git a/test/support/factories/proto/hello_world.ex b/test/support/factories/proto/hello_world.ex new file mode 100644 index 00000000..a1a8aa59 --- /dev/null +++ b/test/support/factories/proto/hello_world.ex @@ -0,0 +1,9 @@ +defmodule GRPC.Factories.Proto.HelloWorld do + defmacro __using__(_opts) do + quote do + def hello_reply_rpc_factory do + %Helloworld.HelloReply{message: "Hello Luis"} + end + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index fceaae03..e65ec19d 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,52 +1,11 @@ defmodule GRPC.Factory do @moduledoc false - alias GRPC.Channel - alias GRPC.Credential + use ExMachina - @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)) + use GRPC.Factories.Channel + use GRPC.Factories.Client.Stream - 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 + # Protobuf factories + use GRPC.Factories.Proto.HelloWorld end diff --git a/test/support/helloworld.pb.ex b/test/support/proto/helloworld.pb.ex similarity index 100% rename from test/support/helloworld.pb.ex rename to test/support/proto/helloworld.pb.ex diff --git a/test/support/helloworld.proto b/test/support/proto/helloworld.proto similarity index 100% rename from test/support/helloworld.proto rename to test/support/proto/helloworld.proto diff --git a/test/support/route_guide.pb.ex b/test/support/proto/route_guide.pb.ex similarity index 100% rename from test/support/route_guide.pb.ex rename to test/support/proto/route_guide.pb.ex diff --git a/test/support/route_guide.proto b/test/support/proto/route_guide.proto similarity index 100% rename from test/support/route_guide.proto rename to test/support/proto/route_guide.proto diff --git a/test/support/test_adapter.exs b/test/support/test_adapter.exs index 3f5d84d0..6541bd75 100644 --- a/test/support/test_adapter.exs +++ b/test/support/test_adapter.exs @@ -5,6 +5,8 @@ defmodule GRPC.Test.ClientAdapter do def disconnect(channel), do: {:ok, channel} def send_request(stream, _message, _opts), do: stream def receive_data(_stream, _opts), do: {:ok, nil} + def send_data(stream, _message, _opts), do: stream + def send_headers(stream, _opts), do: stream end defmodule GRPC.Test.ServerAdapter do diff --git a/test/test_helper.exs b/test/test_helper.exs index 805a2a64..c59f523a 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,4 +1,5 @@ Code.require_file("./support/test_adapter.exs", __DIR__) +{:ok, _} = Application.ensure_all_started(:ex_machina) codecs = [ GRPC.Codec.Erlpack, From bb06048d53d0370bdebcd2f41669c839a54156a7 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 8 Oct 2022 16:56:31 -0300 Subject: [PATCH 30/82] add tests for error and done cast calls --- .../mint/stream_response_process_test.exs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs index 79e70201..f57be747 100644 --- a/test/grpc/client/adapters/mint/stream_response_process_test.exs +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -166,4 +166,38 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do do: [{:headers}, {:trailers}] ) end + + describe "handle_cast/2 - errors" do + test "add error tuple to responses", %{state: state} do + error = {:error, "howdy"} + + response = + StreamResponseProcess.handle_cast( + {:consume_response, error}, + state + ) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert [response_error] = new_state.responses + assert response_error == error + end + end + + describe "handle_cast/2 - done" do + test "set state to done", %{state: state} do + response = + StreamResponseProcess.handle_cast( + {:consume_response, :done}, + state + ) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert true == new_state.done + end + end + + describe "consume/3" do + test "mesage" do + end + end end From 63141ee7b952999f90bf3a0933ceb502361bb57e Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 8 Oct 2022 16:57:20 -0300 Subject: [PATCH 31/82] remove empty test --- .../client/adapters/mint/stream_response_process_test.exs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs index f57be747..eea20ff9 100644 --- a/test/grpc/client/adapters/mint/stream_response_process_test.exs +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -195,9 +195,4 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do assert true == new_state.done end end - - describe "consume/3" do - test "mesage" do - end - end end From 507083f5401cc70ecbc25530268bedb53bed4393 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Mon, 10 Oct 2022 15:24:17 -0300 Subject: [PATCH 32/82] add tests to stream producer for response process --- .../mint/stream_response_process_test.exs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs index eea20ff9..cc649e4b 100644 --- a/test/grpc/client/adapters/mint/stream_response_process_test.exs +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -195,4 +195,98 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do assert true == new_state.done end end + + describe "handle_continue/2 - produce_response" do + test "noreply when process ref is empty", %{state: state} do + {:noreply, new_state} = StreamResponseProcess.handle_continue(:produce_response, state) + assert new_state == state + end + + test "send nil message to caller process (ends Elixir.Stream) when all responses are sent and stream has ended (done: true)", + %{state: state} do + state = %{state | from: {self(), :tag}, done: true} + + {:stop, :normal, _new_state} = + StreamResponseProcess.handle_continue(:produce_response, state) + + assert_receive {:tag, nil} + end + + test "continue when there are no response to be sent and stream is not done yet", %{ + state: state + } do + state = %{state | from: {self(), :tag}, done: false} + {:noreply, new_state} = StreamResponseProcess.handle_continue(:produce_response, state) + assert state == new_state + end + + test "send response to caller when there are responses in the queue", %{state: state} do + state = %{state | from: {self(), :tag}, done: false, responses: [1, 2]} + {:noreply, new_state} = StreamResponseProcess.handle_continue(:produce_response, state) + %{from: from, responses: responses} = new_state + assert nil == from + assert [2] == responses + assert_receive {:tag, 1} + end + end + + describe "build_stream/1" do + setup do + {:ok, pid} = StreamResponseProcess.start_link(build(:client_stream), true) + + %{pid: pid} + end + + test "ends stream when done message is passed", %{pid: pid} do + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.done(pid) + assert Enum.to_list(stream) == [] + end + + test "emits error tuple on stream when error is given to consume", %{pid: pid} do + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.consume(pid, :error, "an error") + StreamResponseProcess.done(pid) + assert [error] = Enum.to_list(stream) + assert {:error, "an error"} == error + end + + test "emits an ok tuple with data", %{pid: pid} do + data_to_consume = <<0, 0, 0, 0, 12, 10, 10, 72, 101, 108, 108, 111, 32, 76, 117, 105, 115>> + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.consume(pid, :data, data_to_consume) + StreamResponseProcess.done(pid) + assert [data] = Enum.to_list(stream) + assert {:ok, build(:hello_reply_rpc)} == data + end + + test_with_params( + "emits headers to stream", + %{pid: pid}, + fn type -> + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"} + ] + + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.consume(pid, type, headers) + StreamResponseProcess.done(pid) + assert [{response_type, response_headers}] = Enum.to_list(stream) + assert type == response_type + + assert %{ + "content-length" => "0", + "content-type" => "application/grpc+proto", + "grpc-message" => "", + "grpc-status" => "0", + "server" => "Cowboy" + } == response_headers + end, + do: [{:headers}, {:trailers}] + ) + end end From 9f5807c37ecfaf3a1591273ab8c1dee8535c9801 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 15 Oct 2022 10:36:29 -0300 Subject: [PATCH 33/82] fix dialyzer issue --- lib/grpc/client/adapters/mint/stream_response_process.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 6ed0ba07..feeebdfb 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -24,7 +24,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do Given a pid from this process, build an Elixir.Stream that will consume the accumulated data inside this process """ - @spec build_stream(pid()) :: Elixir.Stream.t() + @spec build_stream(pid()) :: Enumerable.t() def build_stream(pid) do Elixir.Stream.unfold(pid, fn pid -> case GenServer.call(pid, :get_response, :infinity) do From 9126d674ccf8df0c9298670935674cd4be6bda3c Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 16 Oct 2022 20:09:56 -0300 Subject: [PATCH 34/82] improve documentation for connection process and add tests for disconnect and handle_call for requests --- .../connection_process/connection_process.ex | 44 ++++- .../adapters/mint/connection_process_test.exs | 159 ++++++++++++++++++ 2 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 test/grpc/client/adapters/mint/connection_process_test.exs diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 047c4522..084cd28e 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -1,4 +1,11 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do + @moduledoc """ + This module is responsible for manage a connection with a grpc server. + It's also responsible for manage requests, which also includes check for the + connection/request window size, split a given payload into appropriate sized chunks + and stream those to the server using an internal queue. + """ + use GenServer alias GRPC.Client.Adapters.Mint.ConnectionProcess.State @@ -6,18 +13,48 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do require Logger + @doc """ + Starts and link connection process + """ + @spec start_link(Mint.Types.scheme(), Mint.Types.address(), :inet.port_number(), keyword()) :: + GenServer.on_start() def start_link(scheme, host, port, opts \\ []) do GenServer.start_link(__MODULE__, {scheme, host, port, opts}) end + @doc """ + Sends a request to the connected server. + Opts: + - :stream_response_pid (required) - the process to where send the responses coming from the connection will be sent to be processed + """ + @spec request( + pid :: pid(), + method :: String.t(), + path :: String.t(), + Mint.Types.headers(), + body :: iodata() | nil | :stream, + opts :: keyword() + ) :: {:ok, %{request_ref: Mint.Types.request_ref()}} | {:error, Mint.Types.error()} def request(pid, method, path, headers, body, opts \\ []) do GenServer.call(pid, {:request, method, path, headers, body, opts}) end + @doc """ + Closes the given connection. + """ + @spec disconnect(pid :: pid()) :: :ok def disconnect(pid) do GenServer.call(pid, {:disconnect, :brutal}) end + @doc """ + Streams a chunk of the request body on the connection or signals the end of the body. + """ + @spec stream_request_body( + pid(), + Mint.Types.request_ref(), + iodata() | :eof | {:eof, trailing_headers :: Mint.Types.headers()} + ) :: :ok | {:error, Mint.Types.error()} def stream_request_body(pid, request_ref, body) do GenServer.call(pid, {:stream_body, request_ref, body}) end @@ -33,9 +70,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:ok, State.new(conn)} {:error, reason} -> - # TODO check what's better: add to state map if connection is alive? - # TODO Or simply stop the process and handle the error on caller? - {:stop, reason} + Logger.error("unable to establish a connection. reason: #{inspect(reason)}") + {:stop, :normal} end end @@ -47,7 +83,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end def handle_call( - {:request, method, path, headers, :stream, opts}, + {:request, method, path, headers, :stream, opts} = request, _from, state ) do diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs new file mode 100644 index 00000000..9171a57d --- /dev/null +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -0,0 +1,159 @@ +defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do + use GRPC.DataCase + alias GRPC.Client.Adapters.Mint.ConnectionProcess + + import ExUnit.CaptureLog + + setup do + {:ok, _, port} = GRPC.Server.start(FeatureServer, 0) + + on_exit(fn -> + :ok = GRPC.Server.stop(FeatureServer) + end) + + %{port: port} + end + + describe "start_link/4" do + test "non-successful connection stops the connection process without exit it's caller" do + logs = + capture_log(fn -> + assert {:error, :normal} == ConnectionProcess.start_link(:http, "localhost", 12345) + end) + + assert logs =~ "unable to establish a connection" + end + + test "connects insecurely (default options)", %{port: port} do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port) + assert Process.alive?(pid) + end + end + + describe "disconnect/1" do + test "stop the process when disconnecting", %{port: port} do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port) + assert :ok == ConnectionProcess.disconnect(pid) + refute Process.alive?(pid) + end + + test "close the connection when disconnect", %{port: port} do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port) + state = :sys.get_state(pid) + + assert {:stop, :normal, :ok, new_state} = + ConnectionProcess.handle_call({:disconnect, :brutal}, nil, state) + + refute Mint.HTTP.open?(new_state.conn) + end + end + + describe "handle_call/2 - request - :stream" do + setup(%{port: port}) do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) + state = :sys.get_state(pid) + version = Application.spec(:grpc) |> Keyword.get(:vsn) + + headers = [ + {"content-type", "application/grpc"}, + {"user-agent", "grpc-elixir/#{version}"}, + {"te", "trailers"} + ] + + %{ + process_pid: pid, + state: state, + request: {"POST", "/routeguide.RouteGuide/RecordRoute", headers} + } + end + + test "start stream request and put empty state for ref", %{ + request: {method, path, headers}, + state: state + } do + request = {:request, method, path, headers, :stream, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:ok, %{request_ref: request_ref}}, new_state} = response + assert is_reference(request_ref) + + assert %{ + stream_response_pid: self(), + done: false, + response: %{} + } == new_state.requests[request_ref] + + assert state.conn != new_state.conn + end + + test "returns error response when mint returns an error when starting stream request", %{ + request: {method, path, headers}, + state: state + } do + {:ok, conn} = Mint.HTTP.close(state.conn) + request = {:request, method, path, headers, :stream, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, %{state | conn: conn}) + + assert {:reply, {:error, error}, new_state} = response + assert state.conn != new_state.conn + assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error + end + end + + describe "handle_call/2 - request - payload" do + setup(%{port: port}) do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) + state = :sys.get_state(pid) + version = Application.spec(:grpc) |> Keyword.get(:vsn) + + headers = [ + {"content-type", "application/grpc"}, + {"user-agent", "grpc-elixir/#{version}"}, + {"te", "trailers"} + ] + + %{ + process_pid: pid, + state: state, + request: {"POST", "/routeguide.RouteGuide/RecordRoute", headers} + } + end + + test "start stream request, enqueue payload to be process and continue", %{ + request: {method, path, headers}, + state: state + } do + body = <<1, 2, 3>> + request = {:request, method, path, headers, body, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:ok, %{request_ref: request_ref}}, new_state, + {:continue, :process_request_stream_queue}} = response + + assert is_reference(request_ref) + + assert %{ + stream_response_pid: self(), + done: false, + response: %{} + } == new_state.requests[request_ref] + + assert {[{request_ref, body, nil}], []} == new_state.request_stream_queue + assert state.conn != new_state.conn + end + + test "returns error response when mint returns an error when starting stream request", %{ + request: {method, path, headers}, + state: state + } do + {:ok, conn} = Mint.HTTP.close(state.conn) + body = <<1, 2, 3>> + request = {:request, method, path, headers, body, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, %{state | conn: conn}) + + assert {:reply, {:error, error}, new_state} = response + assert state.conn != new_state.conn + assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error + end + end +end From bb5df5c855a6cf86e80bbbc04002fe723ad5f5d0 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 16 Oct 2022 20:45:30 -0300 Subject: [PATCH 35/82] add tests to stream_body handle_call cases on connection process module --- .../adapters/mint/connection_process_test.exs | 86 +++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index 9171a57d..0bbf6200 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -49,23 +49,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end describe "handle_call/2 - request - :stream" do - setup(%{port: port}) do - {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) - state = :sys.get_state(pid) - version = Application.spec(:grpc) |> Keyword.get(:vsn) - - headers = [ - {"content-type", "application/grpc"}, - {"user-agent", "grpc-elixir/#{version}"}, - {"te", "trailers"} - ] - - %{ - process_pid: pid, - state: state, - request: {"POST", "/routeguide.RouteGuide/RecordRoute", headers} - } - end + setup :valid_connection test "start stream request and put empty state for ref", %{ request: {method, path, headers}, @@ -101,23 +85,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end describe "handle_call/2 - request - payload" do - setup(%{port: port}) do - {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) - state = :sys.get_state(pid) - version = Application.spec(:grpc) |> Keyword.get(:vsn) - - headers = [ - {"content-type", "application/grpc"}, - {"user-agent", "grpc-elixir/#{version}"}, - {"te", "trailers"} - ] - - %{ - process_pid: pid, - state: state, - request: {"POST", "/routeguide.RouteGuide/RecordRoute", headers} - } - end + setup :valid_connection test "start stream request, enqueue payload to be process and continue", %{ request: {method, path, headers}, @@ -156,4 +124,54 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error end end + + describe "handle_call/2 - stream_body" do + setup :valid_connection + setup :valid_stream_request + + test "reply with :ok when stream :eof is successful", %{request_ref: request_ref, state: state} do + response = ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, state) + assert {:reply, :ok, new_state} = response + assert new_state.conn != state.conn + end + + test "reply with error when stream :eof is errors", %{request_ref: request_ref, state: state} do + {:ok, conn} = Mint.HTTP.close(state.conn) + response = ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, %{state| conn: conn}) + assert {:reply, {:error, error}, new_state} = response + assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error + assert new_state.conn != state.conn + end + + test "continue to process payload stream", %{request_ref: request_ref, state: state} do + response = ConnectionProcess.handle_call({:stream_body, request_ref, <<1,2,3>>}, self(), state) + assert {:noreply, new_state, {:continue, :process_request_stream_queue}} = response + assert {[{request_ref, <<1,2,3>>, self()}], []} == new_state.request_stream_queue + assert new_state.conn == state.conn + end + end + + defp valid_connection(%{port: port}) do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) + state = :sys.get_state(pid) + version = Application.spec(:grpc) |> Keyword.get(:vsn) + + headers = [ + {"content-type", "application/grpc"}, + {"user-agent", "grpc-elixir/#{version}"}, + {"te", "trailers"} + ] + + %{ + process_pid: pid, + state: state, + request: {"POST", "/routeguide.RouteGuide/RecordRoute", headers} + } + end + + defp valid_stream_request(%{request: {method, path, headers}, process_pid: pid}) do + {:ok, %{request_ref: request_ref}} = ConnectionProcess.request(pid, method, path, headers, :stream, stream_response_pid: self()) + state = :sys.get_state(pid) + %{request_ref: request_ref, state: state} + end end From 2384cef000a1e3096005b7e1b4c15c517a13f04e Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 22 Oct 2022 11:40:58 -0300 Subject: [PATCH 36/82] add test for process_request_stream_queue --- .../connection_process/connection_process.ex | 2 +- .../adapters/mint/connection_process_test.exs | 73 +++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 084cd28e..c76e77e6 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -83,7 +83,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end def handle_call( - {:request, method, path, headers, :stream, opts} = request, + {:request, method, path, headers, :stream, opts}, _from, state ) do diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index 0bbf6200..71033061 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -129,7 +129,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do setup :valid_connection setup :valid_stream_request - test "reply with :ok when stream :eof is successful", %{request_ref: request_ref, state: state} do + test "reply with :ok when stream :eof is successful", %{ + request_ref: request_ref, + state: state + } do response = ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, state) assert {:reply, :ok, new_state} = response assert new_state.conn != state.conn @@ -137,20 +140,78 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do test "reply with error when stream :eof is errors", %{request_ref: request_ref, state: state} do {:ok, conn} = Mint.HTTP.close(state.conn) - response = ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, %{state| conn: conn}) + + response = + ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, %{ + state + | conn: conn + }) + assert {:reply, {:error, error}, new_state} = response assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error assert new_state.conn != state.conn end test "continue to process payload stream", %{request_ref: request_ref, state: state} do - response = ConnectionProcess.handle_call({:stream_body, request_ref, <<1,2,3>>}, self(), state) + response = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, self(), state) + assert {:noreply, new_state, {:continue, :process_request_stream_queue}} = response - assert {[{request_ref, <<1,2,3>>, self()}], []} == new_state.request_stream_queue + assert {[{request_ref, <<1, 2, 3>>, self()}], []} == new_state.request_stream_queue assert new_state.conn == state.conn end end + describe "handle_continue/2 - :process_stream_queue" do + setup :valid_connection + setup :valid_stream_request + + test "do nothing when there is no window_size in the connection", %{ + request_ref: request_ref, + state: state + } do + # hacky to simulate a window size of zero since this is usually updated with the requests interaction + state = %{state | conn: %{state.conn | window_size: 0}} + # enqueue the payload onto the request queue + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, self(), state) + + assert {:noreply, new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert new_state == state + end + + @tag dev: true + test "(body_size > window_size) chunk payload stream what is possible and enqueue the rest at the begining of the queue to give priority to the current request", + %{request_ref: request_ref, state: state} do + # hacky to simulate a window size of 2 bytes since this is usually updated with the requests interaction + state = %{state | conn: %{state.conn | window_size: 2}} + # enqueue the payload onto the request queue. Add to items to the queue, + # this way we can check is the rest is of the payload goes to the first position to the queue + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, self(), state) + + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<4, 5, 6>>}, self(), state) + + assert {:noreply, new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + # mint update window_size for us. + # This is how we check if the body was streamed + assert new_state.conn.window_size == 0 + assert {{:value, head_of_queue}, queue} = :queue.out(state.request_stream_queue) + assert {{:value, rest}, {[], []}} = :queue.out(queue) + + # <<1, 2, 3>> got enqueue first, we streamed 2 bytes, now we have only one left <<3>> + assert {request_ref, <<3>>, self()} == head_of_queue + + # Next to be processed is <<4, 5, 6>> + assert {request_ref, <<4, 5, 6>>, self()} == rest + end + end + defp valid_connection(%{port: port}) do {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) state = :sys.get_state(pid) @@ -170,7 +231,9 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end defp valid_stream_request(%{request: {method, path, headers}, process_pid: pid}) do - {:ok, %{request_ref: request_ref}} = ConnectionProcess.request(pid, method, path, headers, :stream, stream_response_pid: self()) + {:ok, %{request_ref: request_ref}} = + ConnectionProcess.request(pid, method, path, headers, :stream, stream_response_pid: self()) + state = :sys.get_state(pid) %{request_ref: request_ref, state: state} end From 468a7d0fb50793ce29e51c5bb56508465ca8dd5d Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 22 Oct 2022 12:35:21 -0300 Subject: [PATCH 37/82] add error test cases for connection process --- .../adapters/mint/connection_process_test.exs | 94 ++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index 71033061..05e0dc86 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -182,7 +182,6 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert new_state == state end - @tag dev: true test "(body_size > window_size) chunk payload stream what is possible and enqueue the rest at the begining of the queue to give priority to the current request", %{request_ref: request_ref, state: state} do # hacky to simulate a window size of 2 bytes since this is usually updated with the requests interaction @@ -201,7 +200,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do # mint update window_size for us. # This is how we check if the body was streamed assert new_state.conn.window_size == 0 - assert {{:value, head_of_queue}, queue} = :queue.out(state.request_stream_queue) + assert {{:value, head_of_queue}, queue} = :queue.out(new_state.request_stream_queue) assert {{:value, rest}, {[], []}} = :queue.out(queue) # <<1, 2, 3>> got enqueue first, we streamed 2 bytes, now we have only one left <<3>> @@ -210,6 +209,97 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do # Next to be processed is <<4, 5, 6>> assert {request_ref, <<4, 5, 6>>, self()} == rest end + + test "(window_size >= body_size) stream bod, send end_stream message and reply caller process (when process ref is present)", + %{request_ref: request_ref, state: state} do + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert_receive {:tag, :ok}, 500 + end + + test "(window_size >= body_size) stream body, send end_stream message and don't reply caller process (when precess ref is nil)", + %{request_ref: request_ref, state: state} do + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, nil, state) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + refute_receive {:tag, :ok}, 500 + end + + test "(window_size >= body_size) stream body, send end_stream message and check request_queue when queue is not empty", + %{request_ref: request_ref, state: state} do + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<4, 5, 6>>}, + {self(), :tag}, + state + ) + + assert {:noreply, _new_state, {:continue, :process_request_stream_queue}} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert_receive {:tag, :ok}, 500 + end + + test "send error to the caller process when server return an error and there is a process ref", + %{request_ref: request_ref, state: state} do + {:ok, conn} = Mint.HTTP.close(state.conn) + state = %{state | conn: conn} + + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert_receive {:tag, {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: :closed}}}, 500 + end + + test "send error message to stream response process when caller process ref is empty", + %{request_ref: request_ref, state: state} do + # Close connection to simulate an error + {:ok, conn} = Mint.HTTP.close(state.conn) + request_ref_state = state.requests[request_ref] + + # instead of a real process I put test process pid to test that the message is sent + state = %{ + state + | conn: conn, + requests: %{request_ref => %{request_ref_state | stream_response_pid: self()}} + } + + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, nil, state) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert_receive {:"$gen_cast", + {:consume_response, + {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: :closed}}}}, + 500 + end end defp valid_connection(%{port: port}) do From 17aaf2b7ea6c8c85f210d2472a45b1babc96f4d3 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 23 Oct 2022 09:53:09 -0300 Subject: [PATCH 38/82] add mint adapter to interop tests --- interop/lib/interop/client.ex | 40 +++++++---- interop/script/run.exs | 70 ++++++++----------- lib/grpc/client/adapters/mint.ex | 14 ++++ .../connection_process/connection_process.ex | 17 +++++ .../adapters/mint/stream_response_process.ex | 25 ++++++- lib/grpc/message.ex | 24 +++++++ .../mint/stream_response_process_test.exs | 1 + test/grpc/client/adapters/mint_test.exs | 36 ++++++++++ 8 files changed, 170 insertions(+), 57 deletions(-) create mode 100644 test/grpc/client/adapters/mint_test.exs diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index d8661ea9..6c7131c2 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -103,12 +103,9 @@ defmodule Interop.Client do 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) - result = Enum.map([31415, 9, 2653, 58979], &String.duplicate(<<0>>, &1)) + result = Enum.map([31415, 9, 2653, 58979], &String.duplicate(<<0>>, &1)) |> Enum.sort() - ^result = - Enum.map(res_enum, fn {:ok, res} -> - res.payload.body - end) + ^result = res_enum |> Enum.map(fn {:ok, res} -> res.payload.body end) |> Enum.sort() end def server_compressed_streaming!(ch) do @@ -120,12 +117,9 @@ defmodule Interop.Client do size: 92653} ]) {:ok, res_enum} = ch |> Grpc.Testing.TestService.Stub.streaming_output_call(req) - result = Enum.map([31415, 92653], &String.duplicate(<<0>>, &1)) + result = Enum.map([31415, 92653], &String.duplicate(<<0>>, &1)) |> Enum.sort() - ^result = - Enum.map(res_enum, fn {:ok, res} -> - res.payload.body - end) + ^result = res_enum |> Enum.map(fn {:ok, res} -> res.payload.body end) |> Enum.sort() end def ping_pong!(ch) do @@ -191,19 +185,31 @@ defmodule Interop.Client do payload: payload(271_828) ) - {:ok, res_enum, %{headers: new_headers}} = + {headers, data, trailers} = ch - |> Grpc.Testing.TestService.Stub.full_duplex_call(metadata: metadata) + |> Grpc.Testing.TestService.Stub.full_duplex_call(metadata: metadata, return_headers: true) |> GRPC.Stub.send_request(req, end_stream: true) |> GRPC.Stub.recv(return_headers: true) + |> process_full_duplex_response() reply = String.duplicate(<<0>>, 314_159) - {:ok, %{payload: %{body: ^reply}}} = - Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + %{payload: %{body: ^reply}} = data + + validate_headers!(headers, trailers) + end + defp process_full_duplex_response({:ok, res_enum, %{headers: new_headers}}) do + {:ok, data} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() {:trailers, new_trailers} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() - validate_headers!(new_headers, new_trailers) + {new_headers, data, new_trailers} + end + + defp process_full_duplex_response({:ok, res_enum}) do + {:headers, headers} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, data} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:trailers, trailers} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {headers, data, trailers} end def status_code_and_message!(ch) do @@ -226,6 +232,10 @@ defmodule Interop.Client do |> Grpc.Testing.TestService.Stub.full_duplex_call() |> GRPC.Stub.send_request(req, end_stream: true) |> GRPC.Stub.recv() + |> case do + {:ok, stream} -> Stream.take(stream, 1) |> Enum.to_list() |> List.first() + error -> error + end end def unimplemented_service!(ch) do diff --git a/interop/script/run.exs b/interop/script/run.exs index 81cb5d53..7b05f0cc 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -13,49 +13,39 @@ Logger.configure(level: level) Logger.info("Rounds: #{rounds}; concurrency: #{concurrency}; port: #{port}") alias Interop.Client +alias GRPC.Client.Adapters.{Mint, Gun} {:ok, _pid, port} = GRPC.Server.start_endpoint(Interop.Endpoint, port) -1..concurrency -|> Task.async_stream(fn _cli -> - ch = Client.connect("127.0.0.1", port, interceptors: [GRPCPrometheus.ClientInterceptor, GRPC.Logger.Client]) - - for _ <- 1..rounds do - Client.empty_unary!(ch) - Client.cacheable_unary!(ch) - Client.large_unary!(ch) - Client.large_unary2!(ch) - Client.client_compressed_unary!(ch) - Client.server_compressed_unary!(ch) - Client.client_streaming!(ch) - Client.client_compressed_streaming!(ch) - Client.server_streaming!(ch) - Client.server_compressed_streaming!(ch) - Client.ping_pong!(ch) - Client.empty_stream!(ch) - Client.custom_metadata!(ch) - Client.status_code_and_message!(ch) - Client.unimplemented_service!(ch) - Client.cancel_after_begin!(ch) - Client.cancel_after_first_response!(ch) - Client.timeout_on_sleeping_server!(ch) - end - :ok -end, max_concurrency: concurrency, ordered: false, timeout: :infinity) -|> Enum.to_list() - -# defmodule Helper do -# def flush() do -# receive do -# msg -> -# IO.inspect(msg) -# flush() -# after -# 0 -> :ok -# end -# end -# end -# Helper.flush() +for adapter <- [Gun, Mint] do + 1..concurrency + |> Task.async_stream(fn _cli -> + ch = Client.connect("127.0.0.1", port, interceptors: [GRPCPrometheus.ClientInterceptor, GRPC.Logger.Client], adapter: adapter) + + for _ <- 1..rounds do + Client.empty_unary!(ch) + Client.cacheable_unary!(ch) + Client.large_unary!(ch) + Client.large_unary2!(ch) + Client.client_compressed_unary!(ch) + Client.server_compressed_unary!(ch) + Client.client_streaming!(ch) + Client.client_compressed_streaming!(ch) + Client.server_streaming!(ch) + Client.server_compressed_streaming!(ch) + Client.ping_pong!(ch) + Client.empty_stream!(ch) + Client.custom_metadata!(ch) + Client.status_code_and_message!(ch) + Client.unimplemented_service!(ch) + Client.cancel_after_begin!(ch) + Client.cancel_after_first_response!(ch) + Client.timeout_on_sleeping_server!(ch) + end + :ok + end, max_concurrency: concurrency, ordered: false, timeout: :infinity) + |> Enum.to_list() +end Logger.info("Succeed!") :ok = GRPC.Server.stop_endpoint(Interop.Endpoint) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 3b2634cb..599f0c9e 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -89,6 +89,20 @@ defmodule GRPC.Client.Adapters.Mint do stream end + def end_stream( + %{ + channel: %{adapter_payload: %{conn_pid: pid}}, + payload: %{response: {:ok, %{request_ref: request_ref}}} + } = stream + ) do + ConnectionProcess.stream_request_body(pid, request_ref, :eof) + stream + end + + def cancel(%{conn_pid: conn_pid}, %{response: {:ok, %{request_ref: request_ref}}}) do + ConnectionProcess.cancel(conn_pid, request_ref) + end + defp connect_opts(%Channel{scheme: "https"} = channel, opts) do %Credential{ssl: ssl} = Map.get(channel, :cred, %Credential{}) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index c76e77e6..4102c2cb 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -59,6 +59,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.call(pid, {:stream_body, request_ref, body}) end + def cancel(pid, request_ref) do + GenServer.call(pid, {:cancel_request, request_ref}) + end + ## Callbacks @impl true @@ -73,6 +77,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do Logger.error("unable to establish a connection. reason: #{inspect(reason)}") {:stop, :normal} end + catch + :exit, reason -> + Logger.error("unable to establish a connection. reason: #{inspect(reason)}") + {:stop, :normal} end @impl true @@ -143,6 +151,15 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:continue, :process_request_stream_queue}} end + def handle_call({:cancel_request, request_ref}, _from, state) do + state = process_response({:done, request_ref}, state) + + case Mint.HTTP2.cancel_request(state.conn, request_ref) do + {:ok, conn} -> {:reply, :ok, State.update_conn(state, conn)} + {:error, conn, error} -> {:reply, {:error, error}, State.update_conn(state, conn)} + end + end + @impl true def handle_info(message, state) do case Mint.HTTP.stream(state.conn, message) do diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index feeebdfb..2765b035 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -61,7 +61,8 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do buffer: <<>>, responses: [], done: false, - from: nil + from: nil, + compressor: nil } {:ok, state} @@ -78,7 +79,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do responses: responses } = state - case GRPC.Message.get_message(buffer <> data) do + case GRPC.Message.get_message(buffer <> data, state.compressor) do {{_, message}, rest} -> # TODO add code here to handle compressor headers response = codec.decode(message, res_mod) @@ -97,6 +98,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do %{send_headers_or_trailers: true, responses: responses} = state ) when type in @header_types do + state = update_compressor({type, headers}, state) new_responses = [get_headers_response(headers, type) | responses] {:noreply, %{state | responses: new_responses}, {:continue, :produce_response}} end @@ -106,6 +108,8 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do %{send_headers_or_trailers: false, responses: responses} = state ) when type in @header_types do + state = update_compressor({type, headers}, state) + with {:error, _rpc_error} = error <- get_headers_response(headers, type) do {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} else @@ -151,6 +155,23 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do end end + defp update_compressor({:headers, headers}, state) do + decoded_trailers = GRPC.Transport.HTTP2.decode_headers(headers) + + compressor = + get_compressor(decoded_trailers["grpc-encoding"], state.grpc_stream.accepted_compressors) + + %{state | compressor: compressor} + end + + defp update_compressor(_headers, state), do: state + + defp get_compressor(nil = _encoding_name, _accepted_compressors), do: nil + + defp get_compressor(encoding_name, accepted_compressors) do + Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding_name end) + end + def terminate(_reason, _state) do :normal end diff --git a/lib/grpc/message.ex b/lib/grpc/message.ex index cb75a343..2518864a 100644 --- a/lib/grpc/message.ex +++ b/lib/grpc/message.ex @@ -172,4 +172,28 @@ defmodule GRPC.Message do def get_message(_) do false end + + def get_message(data, nil = _compressor) do + case data do + <> -> + {{flag, message}, rest} + + _other -> + data + end + end + + def get_message(data, compressor) do + case data do + <<1, length::unsigned-integer-size(32), message::bytes-size(length), rest::binary>> -> + {{1, compressor.decompress(message)}, rest} + + <<0, length::unsigned-integer-size(32), message::bytes-size(length), rest::binary>> -> + {{0, message}, rest} + + _other -> + data + end + end end diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs index cc649e4b..a1cf8174 100644 --- a/test/grpc/client/adapters/mint/stream_response_process_test.exs +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -11,6 +11,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do from: nil, grpc_stream: build(:client_stream), responses: [], + compressor: nil, send_headers_or_trailers: false } diff --git a/test/grpc/client/adapters/mint_test.exs b/test/grpc/client/adapters/mint_test.exs new file mode 100644 index 00000000..650fdfbc --- /dev/null +++ b/test/grpc/client/adapters/mint_test.exs @@ -0,0 +1,36 @@ +defmodule GRPC.Client.Adapters.MintTest do + use GRPC.DataCase + + alias GRPC.Client.Adapters.Mint + + describe "connect/2" do + setup do + {:ok, _, port} = GRPC.Server.start(FeatureServer, 0) + + on_exit(fn -> + :ok = GRPC.Server.stop(FeatureServer) + end) + + %{port: port} + end + + test "connects insecurely (default options)", %{port: port} do + channel = build(:channel, port: port, host: "localhost") + + assert {:ok, result} = Mint.connect(channel, []) + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + end + + test "connects insecurely (custom options)", %{port: port} do + channel = build(:channel, port: port, host: "localhost") + + assert {:ok, result} = Mint.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_raise RuntimeError, fn -> + Mint.connect(channel, transport_opts: [ip: "256.0.0.0"]) + end + end + end +end From d5bd0c911e3f0dca03abec0ecb36d76034d29ea2 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 23 Oct 2022 11:18:36 -0300 Subject: [PATCH 39/82] add tests to end_stream and cancel callbacks, also improved documentation --- lib/grpc/client/adapter.ex | 22 ++++++++++ lib/grpc/client/adapters/gun.ex | 9 +++- lib/grpc/client/adapters/mint.ex | 9 +++- lib/grpc/stub.ex | 4 +- .../adapters/mint/connection_process_test.exs | 31 ++++++++++--- .../mint/stream_response_process_test.exs | 44 +++++++++++++++++++ test/support/factories/client/stream.ex | 4 +- test/support/test_adapter.exs | 2 + 8 files changed, 113 insertions(+), 12 deletions(-) diff --git a/lib/grpc/client/adapter.ex b/lib/grpc/client/adapter.ex index b2dd3ff6..8d8bc491 100644 --- a/lib/grpc/client/adapter.ex +++ b/lib/grpc/client/adapter.ex @@ -23,7 +23,29 @@ defmodule GRPC.Client.Adapter do @callback receive_data(stream :: Stream.t(), opts :: keyword()) :: GRPC.Stub.receive_data_return() | {:error, any()} + @doc """ + This callback is used to open a stream connection to the server. + Mostly used when the payload for this request is streamed. + To send data using the open stream request, you should use `send_data/3` + """ @callback send_headers(stream :: Stream.t(), opts :: keyword()) :: Stream.t() + @doc """ + This callback will be responsible to send data to the server on a stream + request is open using `send_headers/2` + Opts: + - :send_end_stream (optional) - ends the request stream + """ @callback send_data(stream :: Stream.t(), message :: binary(), opts :: keyword()) :: Stream.t() + + @doc """ + Similarly to the option sent on `send_data/2` - :send_end_stream - + this callback will end request stream + """ + @callback end_stream(stream :: Stream.t()) :: Stream.t() + + @doc """ + Cancel a stream in a streaming client. + """ + @callback cancel(stream :: Stream.t()) :: Stream.t() end diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex index be579c6b..47f12843 100644 --- a/lib/grpc/client/adapters/gun.ex +++ b/lib/grpc/client/adapters/gun.ex @@ -126,13 +126,20 @@ defmodule GRPC.Client.Adapters.Gun do stream end + @impl true def end_stream(%{channel: channel, payload: %{stream_ref: stream_ref}} = stream) do conn_pid = channel.adapter_payload[:conn_pid] :gun.data(conn_pid, stream_ref, :fin, "") stream end - def cancel(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}) do + @impl true + def cancel(stream) do + %{ + channel: %{adapter_payload: %{conn_pid: conn_pid}}, + payload: %{stream_ref: stream_ref} + } = stream + :gun.cancel(conn_pid, stream_ref) end diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 599f0c9e..37c89064 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -89,6 +89,7 @@ defmodule GRPC.Client.Adapters.Mint do stream end + @impl true def end_stream( %{ channel: %{adapter_payload: %{conn_pid: pid}}, @@ -99,7 +100,13 @@ defmodule GRPC.Client.Adapters.Mint do stream end - def cancel(%{conn_pid: conn_pid}, %{response: {:ok, %{request_ref: request_ref}}}) do + @impl true + def cancel(stream) do + %{ + channel: %{adapter_payload: %{conn_pid: conn_pid}}, + payload: %{response: {:ok, %{request_ref: request_ref}}} + } = stream + ConnectionProcess.cancel(conn_pid, request_ref) end diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index db050c0a..8b6c3ff1 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -357,8 +357,8 @@ defmodule GRPC.Stub do After that, callings to `recv/2` will return a CANCEL error. """ - def cancel(%{channel: channel, payload: payload} = stream) do - case channel.adapter.cancel(channel.adapter_payload, payload) do + def cancel(%{channel: channel} = stream) do + case channel.adapter.cancel(stream) do :ok -> %{stream | canceled: true} other -> other end diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index 05e0dc86..9c3b84ac 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -162,6 +162,23 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end end + describe "handle_call/2 - cancel_request" do + setup :valid_connection + setup :valid_stream_request + + test "reply with :ok when canceling the request is successful, also set stream response pid to done and remove request ref from state", + %{ + request_ref: request_ref, + state: state + } do + state = update_stream_response_process_to_test_pid(state, request_ref, self()) + response = ConnectionProcess.handle_call({:cancel_request, request_ref}, nil, state) + assert {:reply, :ok, new_state} = response + assert %{} == new_state.requests + assert_receive {:"$gen_cast", {:consume_response, :done}}, 500 + end + end + describe "handle_continue/2 - :process_stream_queue" do setup :valid_connection setup :valid_stream_request @@ -280,14 +297,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do %{request_ref: request_ref, state: state} do # Close connection to simulate an error {:ok, conn} = Mint.HTTP.close(state.conn) - request_ref_state = state.requests[request_ref] # instead of a real process I put test process pid to test that the message is sent - state = %{ - state - | conn: conn, - requests: %{request_ref => %{request_ref_state | stream_response_pid: self()}} - } + state = + update_stream_response_process_to_test_pid(%{state | conn: conn}, request_ref, self()) {_, state, _} = ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, nil, state) @@ -327,4 +340,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do state = :sys.get_state(pid) %{request_ref: request_ref, state: state} end + + def update_stream_response_process_to_test_pid(state, request_ref, test_pid) do + request_ref_state = state.requests[request_ref] + + %{state | requests: %{request_ref => %{request_ref_state | stream_response_pid: test_pid}}} + end end diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs index a1cf8174..3b14d593 100644 --- a/test/grpc/client/adapters/mint/stream_response_process_test.exs +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -166,6 +166,50 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do end, do: [{:headers}, {:trailers}] ) + + test "add compressor to state when incoming headers match available compressor", %{ + state: state + } do + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"}, + {"grpc-encoding", "gzip"} + ] + + response = + StreamResponseProcess.handle_cast( + {:consume_response, {:headers, headers}}, + state + ) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert GRPC.Compressor.Gzip == new_state.compressor + end + + test "don't update compressor when unsupported compressor is returned by the server", %{ + state: state + } do + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"}, + {"grpc-encoding", "suzana"} + ] + + response = + StreamResponseProcess.handle_cast( + {:consume_response, {:headers, headers}}, + state + ) + + assert {:noreply, new_state, {:continue, :produce_response}} = response + assert nil == new_state.compressor + end end describe "handle_cast/2 - errors" do diff --git a/test/support/factories/client/stream.ex b/test/support/factories/client/stream.ex index 69070a34..216e9a66 100644 --- a/test/support/factories/client/stream.ex +++ b/test/support/factories/client/stream.ex @@ -7,7 +7,6 @@ defmodule GRPC.Factories.Client.Stream do receive_data: &GRPC.Client.Stream.receive_data/2, send_request: &GRPC.Client.Stream.send_request/3 }, - accepted_compressors: [], canceled: false, channel: build(:channel, adapter: GRPC.Client.Adapters.Mint), codec: GRPC.Codec.Proto, @@ -21,7 +20,8 @@ defmodule GRPC.Factories.Client.Stream do response_mod: Helloworld.HelloReply, rpc: {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}}, server_stream: false, - service_name: "helloworld.Greeter" + service_name: "helloworld.Greeter", + accepted_compressors: [GRPC.Compressor.Gzip] } end end diff --git a/test/support/test_adapter.exs b/test/support/test_adapter.exs index 6541bd75..ff603557 100644 --- a/test/support/test_adapter.exs +++ b/test/support/test_adapter.exs @@ -7,6 +7,8 @@ defmodule GRPC.Test.ClientAdapter do def receive_data(_stream, _opts), do: {:ok, nil} def send_data(stream, _message, _opts), do: stream def send_headers(stream, _opts), do: stream + def end_stream(stream), do: stream + def cancel(stream), do: stream end defmodule GRPC.Test.ServerAdapter do From d16ff137daf932fe70b0e49d902e237e08179dd3 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 23 Oct 2022 11:20:08 -0300 Subject: [PATCH 40/82] fix dialyzer issues --- lib/grpc/client/adapter.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grpc/client/adapter.ex b/lib/grpc/client/adapter.ex index 8d8bc491..5160d55f 100644 --- a/lib/grpc/client/adapter.ex +++ b/lib/grpc/client/adapter.ex @@ -47,5 +47,5 @@ defmodule GRPC.Client.Adapter do @doc """ Cancel a stream in a streaming client. """ - @callback cancel(stream :: Stream.t()) :: Stream.t() + @callback cancel(stream :: Stream.t()) :: :ok | {:error, any()} end From e5e6677c126f87637f792e17c69019e74b71ae09 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 23 Oct 2022 12:45:20 -0300 Subject: [PATCH 41/82] improve documentation and error handling --- lib/grpc/client/adapters/mint.ex | 6 ++---- .../adapters/mint/connection_process/connection_process.ex | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 37c89064..36148eed 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -20,7 +20,6 @@ defmodule GRPC.Client.Adapters.Mint do |> ConnectionProcess.start_link(host, port, opts) |> case do {:ok, pid} -> {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} - # TODO add proper error handling error -> raise "An error happened while trying to opening the connection: #{inspect(error)}" end end @@ -84,7 +83,6 @@ defmodule GRPC.Client.Adapters.Mint do ) do {:ok, data, _} = GRPC.Message.to_data(message, opts) :ok = ConnectionProcess.stream_request_body(pid, request_ref, data) - # TODO: check for trailer headers to be sent here if opts[:send_end_stream], do: ConnectionProcess.stream_request_body(pid, request_ref, :eof) stream end @@ -154,8 +152,8 @@ defmodule GRPC.Client.Adapters.Mint do end end - def handle_errors_receive_data(_stream, _opts) do - raise "TODO: Implement" + def handle_errors_receive_data(%GRPC.Client.Stream{payload: %{response: response}}, _opts) do + {:error, "an error occurred while when receiving data: error=#{inspect(response)}"} end defp success_bidi_stream?(%GRPC.Client.Stream{ diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 4102c2cb..aef6f629 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -59,6 +59,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.call(pid, {:stream_body, request_ref, body}) end + @doc """ + cancels an open request request + """ + @spec cancel(pid(), Mint.Types.request_ref()) :: :ok | {:error, Mint.Types.error()} def cancel(pid, request_ref) do GenServer.call(pid, {:cancel_request, request_ref}) end @@ -67,8 +71,6 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do @impl true def init({scheme, host, port, opts}) do - # The current behavior in gun is return error if the connection wasn't successful - # Should we do the same here? case Mint.HTTP.connect(scheme, host, port, opts) do {:ok, conn} -> {:ok, State.new(conn)} From 94b60371f30aaafb0e78efa275687e3b6306edb1 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 23 Oct 2022 12:51:37 -0300 Subject: [PATCH 42/82] remove mint from example projects --- examples/helloworld/mix.lock | 2 -- examples/route_guide/mix.lock | 3 --- 2 files changed, 5 deletions(-) diff --git a/examples/helloworld/mix.lock b/examples/helloworld/mix.lock index 3cd58c87..f96a70d1 100644 --- a/examples/helloworld/mix.lock +++ b/examples/helloworld/mix.lock @@ -5,8 +5,6 @@ "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"}, - "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, - "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "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/mix.lock b/examples/route_guide/mix.lock index cc5b6e12..3d14fba0 100644 --- a/examples/route_guide/mix.lock +++ b/examples/route_guide/mix.lock @@ -1,13 +1,10 @@ %{ - "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, "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"}, - "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, - "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "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"}, } From 9a7c7010bc12ca6101e7abad6d1fa2c1037c2c51 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 23 Oct 2022 12:57:37 -0300 Subject: [PATCH 43/82] remove ex_machina dependency --- mix.exs | 1 - mix.lock | 1 - test/support/factory.ex | 15 +++++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index acbc6bff..f1b565d2 100644 --- a/mix.exs +++ b/mix.exs @@ -48,7 +48,6 @@ defmodule GRPC.Mixfile do {:protobuf, "~> 0.10", only: [:dev, :test]}, {:ex_doc, "~> 0.28.0", only: :dev}, {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, - {:ex_machina, "~> 2.7.0", only: :test}, {:ex_parameterized, "~> 1.3.7", only: :test} ] end diff --git a/mix.lock b/mix.lock index 9ffaf6d0..022e038f 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,6 @@ "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.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"}, - "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, "ex_parameterized": {:hex, :ex_parameterized, "1.3.7", "801f85fc4651cb51f11b9835864c6ed8c5e5d79b1253506b5bb5421e8ab2f050", [:mix], [], "hexpm", "1fb0dc4aa9e8c12ae23806d03bcd64a5a0fc9cd3f4c5602ba72561c9b54a625c"}, "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, diff --git a/test/support/factory.ex b/test/support/factory.ex index e65ec19d..e3ea44e2 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,11 +1,22 @@ defmodule GRPC.Factory do @moduledoc false - use ExMachina - use GRPC.Factories.Channel use GRPC.Factories.Client.Stream # Protobuf factories use GRPC.Factories.Proto.HelloWorld + + 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 end From b6b3e62fd3ed21fb0b51a9cb5fd7e2661dad13af Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sun, 23 Oct 2022 13:06:00 -0300 Subject: [PATCH 44/82] fix: remove ex-machina from test helper --- test/test_helper.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_helper.exs b/test/test_helper.exs index c59f523a..805a2a64 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,5 +1,4 @@ Code.require_file("./support/test_adapter.exs", __DIR__) -{:ok, _} = Application.ensure_all_started(:ex_machina) codecs = [ GRPC.Codec.Erlpack, From 35564a5f3f4f4a20bd113c227089cdb2313ee960 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Tue, 25 Oct 2022 16:32:29 -0300 Subject: [PATCH 45/82] add connection close handling --- lib/grpc/client/adapters/mint.ex | 4 + .../connection_process/connection_process.ex | 53 +++++++++++-- .../adapters/mint/connection_process_test.exs | 76 +++++++++++++------ test/support/feature_server.ex | 6 +- 4 files changed, 107 insertions(+), 32 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 36148eed..8ac7fa92 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -14,6 +14,7 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def connect(%{host: host, port: port} = channel, opts \\ []) do opts = Keyword.merge(@default_connect_opts, connect_opts(channel, opts)) + Process.flag(:trap_exit, true) channel |> mint_scheme() @@ -22,6 +23,9 @@ defmodule GRPC.Client.Adapters.Mint do {:ok, pid} -> {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} error -> raise "An error happened while trying to opening the connection: #{inspect(error)}" end + catch + :exit, reason -> + raise "An error happened while trying to opening the connection: #{inspect(reason)}" end @impl true diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index aef6f629..7ae9bc91 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -13,6 +13,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do require Logger + @connection_closed_error "the connection is closed" + @doc """ Starts and link connection process """ @@ -77,21 +79,25 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:error, reason} -> Logger.error("unable to establish a connection. reason: #{inspect(reason)}") - {:stop, :normal} + {:stop, reason} end catch :exit, reason -> Logger.error("unable to establish a connection. reason: #{inspect(reason)}") - {:stop, :normal} + {:stop, reason} end @impl true def handle_call({:disconnect, :brutal}, _from, state) do - # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests + # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests {:ok, conn} = Mint.HTTP.close(state.conn) {:stop, :normal, :ok, State.update_conn(state, conn)} end + def handle_call(_request, _from, %{conn: %Mint.HTTP2{state: :closed}} = state) do + {:reply, {:error, "the connection is closed"}, state} + end + def handle_call( {:request, method, path, headers, :stream, opts}, _from, @@ -169,14 +175,16 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do Logger.debug(fn -> "Received unknown message: " <> inspect(message) end) {:noreply, state} - {:ok, conn, [] = _responses} -> - check_request_stream_queue(State.update_conn(state, conn)) - {:ok, conn, responses} -> + IO.inspect(conn) state = State.update_conn(state, conn) state = Enum.reduce(responses, state, &process_response/2) - check_request_stream_queue(state) + if(Mint.HTTP.open?(state.conn)) do + check_request_stream_queue(state) + else + finish_all_pending_requests(state) + end end end @@ -323,4 +331,35 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do Mint.HTTP2.get_window_size(conn, :connection) ) end + + defp finish_all_pending_requests(state) do + :queue.fold( + fn request, acc_state -> + case request do + {ref, _body, nil} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + {ref, _body, from} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + GenServer.reply(from, {:error, @connection_closed_error}) + end + + acc_state + end, + state, + state.request_stream_queue + ) + + {:noreply, State.update_request_stream_queue(state, :queue.new())} + end + + defp send_connection_close_and_end_stream_response(pid) do + StreamResponseProcess.consume(pid, :error, @connection_closed_error) + StreamResponseProcess.done(pid) + end end diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index 9c3b84ac..324475fd 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -18,7 +18,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do test "non-successful connection stops the connection process without exit it's caller" do logs = capture_log(fn -> - assert {:error, :normal} == ConnectionProcess.start_link(:http, "localhost", 12345) + assert {:error, %Mint.TransportError{reason: :econnrefused}} == + ConnectionProcess.start_link(:http, "localhost", 12345) end) assert logs =~ "unable to establish a connection" @@ -70,7 +71,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert state.conn != new_state.conn end - test "returns error response when mint returns an error when starting stream request", %{ + test "returns error when connection is closed", %{ request: {method, path, headers}, state: state } do @@ -80,7 +81,22 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert {:reply, {:error, error}, new_state} = response assert state.conn != new_state.conn - assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error + assert "the connection is closed" == error + end + + test "returns error response when mint returns an error when starting stream request", %{ + request: {method, path, headers}, + state: state + } do + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) + + request = {:request, method, path, headers, :stream, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:error, error}, new_state} = response + assert state.conn == new_state.conn + assert %Mint.TransportError{__exception__: true, reason: :closed} == error end end @@ -110,7 +126,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert state.conn != new_state.conn end - test "returns error response when mint returns an error when starting stream request", %{ + test "returns error response when connection is closed", %{ request: {method, path, headers}, state: state } do @@ -121,7 +137,23 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert {:reply, {:error, error}, new_state} = response assert state.conn != new_state.conn - assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error + assert "the connection is closed" == error + end + + test "returns error response when mint returns an error when starting stream request", %{ + request: {method, path, headers}, + state: state + } do + body = <<1, 2, 3>> + request = {:request, method, path, headers, body, [stream_response_pid: self()]} + + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) + + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:error, error}, _new_state} = response + assert %Mint.TransportError{reason: :closed} == error end end @@ -139,17 +171,13 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end test "reply with error when stream :eof is errors", %{request_ref: request_ref, state: state} do - {:ok, conn} = Mint.HTTP.close(state.conn) + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) - response = - ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, %{ - state - | conn: conn - }) + response = ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, state) - assert {:reply, {:error, error}, new_state} = response - assert %Mint.HTTPError{__exception__: true, module: Mint.HTTP2, reason: :closed} == error - assert new_state.conn != state.conn + assert {:reply, {:error, error}, _new_state} = response + assert %Mint.TransportError{__exception__: true, reason: :closed} == error end test "continue to process payload stream", %{request_ref: request_ref, state: state} do @@ -277,8 +305,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do test "send error to the caller process when server return an error and there is a process ref", %{request_ref: request_ref, state: state} do - {:ok, conn} = Mint.HTTP.close(state.conn) - state = %{state | conn: conn} + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) {_, state, _} = ConnectionProcess.handle_call( @@ -290,27 +318,27 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert {:noreply, _new_state} = ConnectionProcess.handle_continue(:process_request_stream_queue, state) - assert_receive {:tag, {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: :closed}}}, 500 + assert_receive {:tag, {:error, %Mint.TransportError{reason: :closed, __exception__: true}}}, + 500 end test "send error message to stream response process when caller process ref is empty", %{request_ref: request_ref, state: state} do - # Close connection to simulate an error - {:ok, conn} = Mint.HTTP.close(state.conn) - # instead of a real process I put test process pid to test that the message is sent - state = - update_stream_response_process_to_test_pid(%{state | conn: conn}, request_ref, self()) + state = update_stream_response_process_to_test_pid(state, request_ref, self()) {_, state, _} = - ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, nil, state) + ConnectionProcess.handle_call({:stream_body, request_ref, <<1>>}, nil, state) + + # Close connection to simulate an error (like the server closing the connection before we update state) + {:ok, _conn} = Mint.HTTP.close(state.conn) assert {:noreply, _new_state} = ConnectionProcess.handle_continue(:process_request_stream_queue, state) assert_receive {:"$gen_cast", {:consume_response, - {:error, %Mint.HTTPError{module: Mint.HTTP2, reason: :closed}}}}, + {:error, %Mint.TransportError{reason: :closed, __exception__: true}}}}, 500 end end diff --git a/test/support/feature_server.ex b/test/support/feature_server.ex index 3951ea8b..a3e70c7e 100644 --- a/test/support/feature_server.ex +++ b/test/support/feature_server.ex @@ -2,6 +2,10 @@ 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}") + if point.latitude != 0 do + Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + else + {:error, "server error"} + end end end From d4f2570b421746c1014a3bab2aa802256ca19c09 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Tue, 25 Oct 2022 18:06:01 -0300 Subject: [PATCH 46/82] add connection close checks and tests --- .../connection_process/connection_process.ex | 80 ++++++++++++------- .../adapters/mint/connection_process/state.ex | 9 ++- .../adapters/mint/connection_process_test.exs | 69 ++++++++++++++++ 3 files changed, 125 insertions(+), 33 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 7ae9bc91..e66dae7a 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -21,6 +21,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do @spec start_link(Mint.Types.scheme(), Mint.Types.address(), :inet.port_number(), keyword()) :: GenServer.on_start() def start_link(scheme, host, port, opts \\ []) do + opts = Keyword.put(opts, :parent, self()) GenServer.start_link(__MODULE__, {scheme, host, port, opts}) end @@ -75,7 +76,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do def init({scheme, host, port, opts}) do case Mint.HTTP.connect(scheme, host, port, opts) do {:ok, conn} -> - {:ok, State.new(conn)} + {:ok, State.new(conn, opts[:parent])} {:error, reason} -> Logger.error("unable to establish a connection. reason: #{inspect(reason)}") @@ -176,15 +177,13 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:noreply, state} {:ok, conn, responses} -> - IO.inspect(conn) state = State.update_conn(state, conn) state = Enum.reduce(responses, state, &process_response/2) + check_connection_status(state) - if(Mint.HTTP.open?(state.conn)) do - check_request_stream_queue(state) - else - finish_all_pending_requests(state) - end + {:error, conn, _error, _responses} -> + state = State.update_conn(state, conn) + check_connection_status(state) end end @@ -333,33 +332,56 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp finish_all_pending_requests(state) do - :queue.fold( - fn request, acc_state -> - case request do - {ref, _body, nil} -> - acc_state - |> State.stream_response_pid(ref) - |> send_connection_close_and_end_stream_response() - - {ref, _body, from} -> - acc_state - |> State.stream_response_pid(ref) - |> send_connection_close_and_end_stream_response() - - GenServer.reply(from, {:error, @connection_closed_error}) - end - - acc_state - end, - state, - state.request_stream_queue - ) + new_state = + :queue.fold( + fn request, acc_state -> + case request do + {ref, _body, nil} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + {ref, _body, from} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + GenServer.reply(from, {:error, @connection_closed_error}) + end + + {ref, _, _} = request + {_ref, new_state} = State.pop_ref(acc_state, ref) + + new_state + end, + state, + state.request_stream_queue + ) + + # Inform the parent that the connection is down + send(new_state.parent, {:elixir_grpc, :connection_down, self()}) + + new_state.requests + |> Map.keys() + |> Enum.each(fn ref -> + new_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + end) - {:noreply, State.update_request_stream_queue(state, :queue.new())} + {:noreply, State.update_request_stream_queue(%{new_state | requests: %{}}, :queue.new())} end defp send_connection_close_and_end_stream_response(pid) do StreamResponseProcess.consume(pid, :error, @connection_closed_error) StreamResponseProcess.done(pid) end + + def check_connection_status(state) do + if(Mint.HTTP.open?(state.conn)) do + check_request_stream_queue(state) + else + finish_all_pending_requests(state) + end + end end diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index 00afda74..8fc02778 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -1,13 +1,14 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do - defstruct [:conn, requests: %{}, request_stream_queue: :queue.new()] + defstruct [:conn, :parent, requests: %{}, request_stream_queue: :queue.new()] @type t :: %__MODULE__{ conn: Mint.HTTP.t(), - requests: map() + requests: map(), + parent: pid() } - def new(conn) do - %__MODULE__{conn: conn, request_stream_queue: :queue.new()} + def new(conn, parent) do + %__MODULE__{conn: conn, request_stream_queue: :queue.new(), parent: parent} end def update_conn(state, conn) do diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index 324475fd..8012ab1f 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -343,6 +343,75 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end end + describe "handle_info - connection_closed - no requests" do + setup :valid_connection + + test "send a message to parent process to inform the connection is down", %{ + state: state + } do + socket = state.conn.socket + # this is a mocked message to inform the connection is closed + tcp_message = {:tcp_closed, socket} + + assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) + assert new_state.conn.state == :closed + assert_receive {:elixir_grpc, :connection_down, pid}, 500 + assert pid == self() + end + end + + describe "handle_info - connection_closed - with request" do + setup :valid_connection + setup :valid_stream_request + + test "send a message to parent process to inform the connection is down and end stream response process", + %{ + state: state, + request_ref: request_ref + } do + socket = state.conn.socket + # this is a mocked message to inform the connection is closed + tcp_message = {:tcp_closed, socket} + + state = update_stream_response_process_to_test_pid(state, request_ref, self()) + assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) + assert new_state.conn.state == :closed + assert_receive {:elixir_grpc, :connection_down, pid}, 500 + + assert_receive {:"$gen_cast", {:consume_response, {:error, "the connection is closed"}}}, + 500 + + assert_receive {:"$gen_cast", {:consume_response, :done}}, 500 + assert pid == self() + end + + test "send a message to parent process to inform the connection is down and reply pending process", + %{ + state: state, + request_ref: request_ref + } do + socket = state.conn.socket + # this is a mocked message to inform the connection is closed + tcp_message = {:tcp_closed, socket} + + response = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + {:noreply, state, {:continue, :process_request_stream_queue}} = response + + state = update_stream_response_process_to_test_pid(state, request_ref, self()) + assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) + assert new_state.conn.state == :closed + assert_receive {:elixir_grpc, :connection_down, pid}, 500 + assert_receive {:tag, {:error, "the connection is closed"}}, 500 + assert pid == self() + end + end + defp valid_connection(%{port: port}) do {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) state = :sys.get_state(pid) From 10e8c471d58479cb396d7f12b1572dbb4c9a32ed Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Tue, 25 Oct 2022 18:17:41 -0300 Subject: [PATCH 47/82] remove :queue.fold reference to avoid break on old versions of erlang --- .../connection_process/connection_process.ex | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index e66dae7a..7d63c9fc 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -333,30 +333,28 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do defp finish_all_pending_requests(state) do new_state = - :queue.fold( - fn request, acc_state -> - case request do - {ref, _body, nil} -> - acc_state - |> State.stream_response_pid(ref) - |> send_connection_close_and_end_stream_response() - - {ref, _body, from} -> - acc_state - |> State.stream_response_pid(ref) - |> send_connection_close_and_end_stream_response() - - GenServer.reply(from, {:error, @connection_closed_error}) - end - - {ref, _, _} = request - {_ref, new_state} = State.pop_ref(acc_state, ref) - - new_state - end, - state, - state.request_stream_queue - ) + state.request_stream_queue + |> :queue.to_list() + |> Enum.reduce(state, fn request, acc_state -> + case request do + {ref, _body, nil} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + {ref, _body, from} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + GenServer.reply(from, {:error, @connection_closed_error}) + end + + {ref, _, _} = request + {_ref, new_state} = State.pop_ref(acc_state, ref) + + new_state + end) # Inform the parent that the connection is down send(new_state.parent, {:elixir_grpc, :connection_down, self()}) From 2238cc4c2d7e2a907e1eb4def7036a6ba7b7e534 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 26 Oct 2022 10:21:40 -0300 Subject: [PATCH 48/82] send error tuple instead of raise for connect --- lib/grpc/client/adapters/mint.ex | 9 ++++++--- .../mint/connection_process/connection_process.ex | 7 ++++++- test/grpc/client/adapters/mint_test.exs | 7 ++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 8ac7fa92..e414f6b6 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -20,12 +20,15 @@ defmodule GRPC.Client.Adapters.Mint do |> mint_scheme() |> ConnectionProcess.start_link(host, port, opts) |> case do - {:ok, pid} -> {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} - error -> raise "An error happened while trying to opening the connection: #{inspect(error)}" + {:ok, pid} -> + {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} + + error -> + {:error, "An error happened while trying to opening the connection: #{inspect(error)}"} end catch :exit, reason -> - raise "An error happened while trying to opening the connection: #{inspect(reason)}" + {:error, "An error happened while trying to opening the connection: #{inspect(reason)}"} end @impl true diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 7d63c9fc..1fcfda47 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -178,7 +178,12 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:ok, conn, responses} -> state = State.update_conn(state, conn) - state = Enum.reduce(responses, state, &process_response/2) + + state = + if state.requests == %{}, + do: state, + else: Enum.reduce(responses, state, &process_response/2) + check_connection_status(state) {:error, conn, _error, _responses} -> diff --git a/test/grpc/client/adapters/mint_test.exs b/test/grpc/client/adapters/mint_test.exs index 650fdfbc..95f33a1d 100644 --- a/test/grpc/client/adapters/mint_test.exs +++ b/test/grpc/client/adapters/mint_test.exs @@ -28,9 +28,10 @@ defmodule GRPC.Client.Adapters.MintTest do assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result # Ensure that changing one of the options breaks things - assert_raise RuntimeError, fn -> - Mint.connect(channel, transport_opts: [ip: "256.0.0.0"]) - end + assert {:error, message} = Mint.connect(channel, transport_opts: [ip: "256.0.0.0"]) + + assert message == + "An error happened while trying to opening the connection: {:error, :badarg}" end end end From d3e026f6bbdb462d044247f94a5a1104866e1b23 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 30 Nov 2022 09:43:00 -0300 Subject: [PATCH 49/82] refact: extract interop test runnet to its module runner --- interop/script/run.exs | 57 ++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/interop/script/run.exs b/interop/script/run.exs index 7b05f0cc..e8a3abe9 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -17,33 +17,40 @@ alias GRPC.Client.Adapters.{Mint, Gun} {:ok, _pid, port} = GRPC.Server.start_endpoint(Interop.Endpoint, port) +defmodule InteropTestRunner do + def run(_cli, adapter, port, rounds) do + opts = [interceptors: [GRPCPrometheus.ClientInterceptor, GRPC.Logger.Client], adapter: adapter] + ch = Client.connect("127.0.0.1", port, opts) + + for _ <- 1..rounds do + Client.empty_unary!(ch) + Client.cacheable_unary!(ch) + Client.large_unary!(ch) + Client.large_unary2!(ch) + Client.client_compressed_unary!(ch) + Client.server_compressed_unary!(ch) + Client.client_streaming!(ch) + Client.client_compressed_streaming!(ch) + Client.server_streaming!(ch) + Client.server_compressed_streaming!(ch) + Client.ping_pong!(ch) + Client.empty_stream!(ch) + Client.custom_metadata!(ch) + Client.status_code_and_message!(ch) + Client.unimplemented_service!(ch) + Client.cancel_after_begin!(ch) + Client.cancel_after_first_response!(ch) + Client.timeout_on_sleeping_server!(ch) + end + :ok + end +end + for adapter <- [Gun, Mint] do + args = [adapter, port, rounds] + stream_opts = [max_concurrency: concurrency, ordered: false, timeout: :infinity] 1..concurrency - |> Task.async_stream(fn _cli -> - ch = Client.connect("127.0.0.1", port, interceptors: [GRPCPrometheus.ClientInterceptor, GRPC.Logger.Client], adapter: adapter) - - for _ <- 1..rounds do - Client.empty_unary!(ch) - Client.cacheable_unary!(ch) - Client.large_unary!(ch) - Client.large_unary2!(ch) - Client.client_compressed_unary!(ch) - Client.server_compressed_unary!(ch) - Client.client_streaming!(ch) - Client.client_compressed_streaming!(ch) - Client.server_streaming!(ch) - Client.server_compressed_streaming!(ch) - Client.ping_pong!(ch) - Client.empty_stream!(ch) - Client.custom_metadata!(ch) - Client.status_code_and_message!(ch) - Client.unimplemented_service!(ch) - Client.cancel_after_begin!(ch) - Client.cancel_after_first_response!(ch) - Client.timeout_on_sleeping_server!(ch) - end - :ok - end, max_concurrency: concurrency, ordered: false, timeout: :infinity) + |> Task.async_stream(InteropTestRunner, :run, args, stream_opts) |> Enum.to_list() end From e704eedaaef7345deddfbe6fe47b5f828fe5406d Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Fri, 2 Dec 2022 17:15:28 +0700 Subject: [PATCH 50/82] fix: fix code review suggestions * Improve doc on `GRPC.Client.Adapters.Mint.ConnectionProcess`. * Remove `Map.keys/1` in `GRPC.Client.Adapters.Mint.ConnectionProcess.finish_all_pending_requests/1` * Remove if parenthesises. --- .../mint/connection_process/connection_process.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 1fcfda47..9eda9d1a 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -27,8 +27,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do @doc """ Sends a request to the connected server. - Opts: - - :stream_response_pid (required) - the process to where send the responses coming from the connection will be sent to be processed + + ## Options + + * :stream_response_pid (required) - the process to where send the responses coming from the connection will be sent to be processed """ @spec request( pid :: pid(), @@ -365,8 +367,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do send(new_state.parent, {:elixir_grpc, :connection_down, self()}) new_state.requests - |> Map.keys() - |> Enum.each(fn ref -> + |> Enum.each(fn {ref, _} -> new_state |> State.stream_response_pid(ref) |> send_connection_close_and_end_stream_response() @@ -381,7 +382,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end def check_connection_status(state) do - if(Mint.HTTP.open?(state.conn)) do + if Mint.HTTP.open?(state.conn) do check_request_stream_queue(state) else finish_all_pending_requests(state) From 96207f9276e14fdc9c825ade9bb665a7e1916e1d Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Fri, 2 Dec 2022 17:20:01 +0700 Subject: [PATCH 51/82] fix: fix code review suggestions --- .../mint/connection_process/connection_process.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 9eda9d1a..5ec978e9 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -1,9 +1,9 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do @moduledoc """ - This module is responsible for manage a connection with a grpc server. - It's also responsible for manage requests, which also includes check for the - connection/request window size, split a given payload into appropriate sized chunks - and stream those to the server using an internal queue. + This module is responsible for managing a connection with a gRPC server. + It's also responsible for managing requests, which also includes checks for the + connection/request window size, splitting a given payload into appropriate sized chunks + and streaming those to the server using an internal queue. """ use GenServer From c67d7d4dc65f29de3e7cafc942da90f5ee200fef Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Fri, 2 Dec 2022 17:21:32 +0700 Subject: [PATCH 52/82] fix: fix code review suggestions --- .../mint/connection_process/connection_process.ex | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 5ec978e9..3edb6fbb 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -286,14 +286,14 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do case stream_body(state.conn, request_ref, body, send_eof?) do {:ok, conn} -> - if from != nil do + if not is_nil(from) do GenServer.reply(from, :ok) end check_request_stream_queue(State.update_conn(state, conn)) {:error, conn, error} -> - if from != nil do + if not is_nil(from) do GenServer.reply(from, {:error, error}) else state @@ -313,9 +313,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp stream_body(conn, request_ref, body, false = _stream_eof?) do - with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, request_ref, body) do - {:ok, conn} - end + Mint.HTTP.stream_request_body(conn, request_ref, body) end def check_request_stream_queue(state) do From 90fad3cf96ae8b2452e337ebfdc60b2ba0ca01ec Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Fri, 2 Dec 2022 21:52:23 +0700 Subject: [PATCH 53/82] fix: fix code review suggestions * Expand alias group in `GRPC.Client.Adapters.Mint`. --- lib/grpc/client/adapters/mint.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index e414f6b6..ec153eb8 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -3,8 +3,10 @@ defmodule GRPC.Client.Adapters.Mint do A client adapter using mint """ - alias GRPC.{Channel, Credential} - alias GRPC.Client.Adapters.Mint.{ConnectionProcess, StreamResponseProcess} + alias GRPC.Channel + alias GRPC.Client.Adapters.Mint.ConnectionProcess + alias GRPC.Client.Adapters.Mint.StreamResponseProcess + alias GRPC.Credential @behaviour GRPC.Client.Adapter From a6f87e9905db877c096a3830970d4187f86e1c9e Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Fri, 2 Dec 2022 21:54:29 +0700 Subject: [PATCH 54/82] fix: fix code review suggestions * Expand alias group in interop test. --- interop/script/run.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interop/script/run.exs b/interop/script/run.exs index e8a3abe9..c99a85f3 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -12,8 +12,9 @@ Logger.configure(level: level) Logger.info("Rounds: #{rounds}; concurrency: #{concurrency}; port: #{port}") +alias GRPC.Client.Adapters.Gun +alias GRPC.Client.Adapters.Mint alias Interop.Client -alias GRPC.Client.Adapters.{Mint, Gun} {:ok, _pid, port} = GRPC.Server.start_endpoint(Interop.Endpoint, port) From 49b0af10d1250c7b77974ea14597ae4c34901f10 Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Fri, 2 Dec 2022 22:31:35 +0700 Subject: [PATCH 55/82] fix: fix code review suggestions --- lib/grpc/client/adapters/mint/stream_response_process.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 2765b035..6f6077fc 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -26,7 +26,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do """ @spec build_stream(pid()) :: Enumerable.t() def build_stream(pid) do - Elixir.Stream.unfold(pid, fn pid -> + Stream.unfold(pid, fn pid -> case GenServer.call(pid, :get_response, :infinity) do nil -> nil response -> {response, pid} From 85f7369275b161506bce4b0edd1ebec513d05049 Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Fri, 2 Dec 2022 22:39:51 +0700 Subject: [PATCH 56/82] refactor: extract ref at the function argument --- .../adapters/mint/connection_process/connection_process.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 3edb6fbb..db32eb79 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -340,7 +340,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do new_state = state.request_stream_queue |> :queue.to_list() - |> Enum.reduce(state, fn request, acc_state -> + |> Enum.reduce(state, fn {ref, _, _} = request, acc_state -> case request do {ref, _body, nil} -> acc_state @@ -355,9 +355,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.reply(from, {:error, @connection_closed_error}) end - {ref, _, _} = request {_ref, new_state} = State.pop_ref(acc_state, ref) - new_state end) From 955a15f3b379c90ec24a3a93cd0c067cf22d9231 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Tue, 13 Dec 2022 15:57:20 -0300 Subject: [PATCH 57/82] remove unecessary sorts --- interop/lib/interop/client.ex | 4 ++-- interop/script/run.exs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index 6c7131c2..952b6b40 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -103,7 +103,7 @@ defmodule Interop.Client do 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) - result = Enum.map([31415, 9, 2653, 58979], &String.duplicate(<<0>>, &1)) |> Enum.sort() + result = Enum.map([9, 2653, 31415, 58979], &String.duplicate(<<0>>, &1)) ^result = res_enum |> Enum.map(fn {:ok, res} -> res.payload.body end) |> Enum.sort() end @@ -117,7 +117,7 @@ defmodule Interop.Client do size: 92653} ]) {:ok, res_enum} = ch |> Grpc.Testing.TestService.Stub.streaming_output_call(req) - result = Enum.map([31415, 92653], &String.duplicate(<<0>>, &1)) |> Enum.sort() + result = Enum.map([31415, 92653], &String.duplicate(<<0>>, &1)) ^result = res_enum |> Enum.map(fn {:ok, res} -> res.payload.body end) |> Enum.sort() end diff --git a/interop/script/run.exs b/interop/script/run.exs index c99a85f3..22f9fae5 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -48,6 +48,7 @@ defmodule InteropTestRunner do end for adapter <- [Gun, Mint] do + Logger.info("Starting run for adapter: #{adapter}") args = [adapter, port, rounds] stream_opts = [max_concurrency: concurrency, ordered: false, timeout: :infinity] 1..concurrency From 5bbb1429765361e09dd3a89d6faefc633290ef57 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Mon, 26 Dec 2022 09:09:36 -0300 Subject: [PATCH 58/82] rename disconnect to remove brutal term --- .../adapters/mint/connection_process/connection_process.ex | 4 ++-- test/grpc/client/adapters/mint/connection_process_test.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index db32eb79..c53d902f 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -49,7 +49,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do """ @spec disconnect(pid :: pid()) :: :ok def disconnect(pid) do - GenServer.call(pid, {:disconnect, :brutal}) + GenServer.call(pid, :disconnect) end @doc """ @@ -91,7 +91,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end @impl true - def handle_call({:disconnect, :brutal}, _from, state) do + def handle_call(:disconnect, _from, state) do # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests {:ok, conn} = Mint.HTTP.close(state.conn) {:stop, :normal, :ok, State.update_conn(state, conn)} diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index 8012ab1f..ce491d03 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -43,7 +43,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do state = :sys.get_state(pid) assert {:stop, :normal, :ok, new_state} = - ConnectionProcess.handle_call({:disconnect, :brutal}, nil, state) + ConnectionProcess.handle_call(:disconnect, nil, state) refute Mint.HTTP.open?(new_state.conn) end From 07b61d3236ef965c5ab6c6e8bc930bce1815811e Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Mon, 26 Dec 2022 13:08:21 -0300 Subject: [PATCH 59/82] rename bidi_stream atom to bidirectional_stream for clarity --- lib/grpc/client/adapters/mint.ex | 4 ++-- lib/grpc/service.ex | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index ec153eb8..ee05ca21 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -118,7 +118,7 @@ defmodule GRPC.Client.Adapters.Mint do end defp connect_opts(%Channel{scheme: "https"} = channel, opts) do - %Credential{ssl: ssl} = Map.get(channel, :cred, %Credential{}) + %Credential{ssl: ssl} = Map.get(channel, :cred) || %Credential{} transport_opts = opts @@ -166,7 +166,7 @@ defmodule GRPC.Client.Adapters.Mint do end defp success_bidi_stream?(%GRPC.Client.Stream{ - grpc_type: :bidi_stream, + grpc_type: :bidirectional_stream, payload: %{response: {:ok, _resp}} }), do: true diff --git a/lib/grpc/service.ex b/lib/grpc/service.ex index 5357b6c2..9eaf717c 100644 --- a/lib/grpc/service.ex +++ b/lib/grpc/service.ex @@ -57,5 +57,5 @@ defmodule GRPC.Service do def grpc_type({_, {_, false}, {_, false}}), do: :unary def grpc_type({_, {_, true}, {_, false}}), do: :client_stream def grpc_type({_, {_, false}, {_, true}}), do: :server_stream - def grpc_type({_, {_, true}, {_, true}}), do: :bidi_stream + def grpc_type({_, {_, true}, {_, true}}), do: :bidirectional_stream end From 0825251368c2d6fbc5b1f67f5f91f68102e8c80b Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Mon, 26 Dec 2022 13:28:41 -0300 Subject: [PATCH 60/82] refactor response checks for mint adapter to have a single funtion --- lib/grpc/client/adapters/mint.ex | 52 +++++--------------------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index ee05ca21..96f94407 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -55,21 +55,10 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def receive_data(stream, opts) do - cond do - success_bidi_stream?(stream) -> - do_receive_data(stream, :bidirectional_stream, opts) - - success_server_stream?(stream) -> - do_receive_data(stream, :unary_request_stream_response, opts) - - success_client_stream?(stream) -> - do_receive_data(stream, :stream_request_unary_response, opts) - - success_unary_request?(stream) -> - do_receive_data(stream, :unary_request_response, opts) - - true -> - handle_errors_receive_data(stream, opts) + if success_response?(stream) do + do_receive_data(stream, stream.grpc_type, opts) + else + handle_errors_receive_data(stream, opts) end end @@ -137,7 +126,7 @@ defmodule GRPC.Client.Adapters.Mint do defp mint_scheme(_channel), do: :http defp do_receive_data(%{payload: %{stream_response_pid: pid}}, request_type, _opts) - when request_type in [:bidirectional_stream, :unary_request_stream_response] do + when request_type in [:bidirectional_stream, :server_stream] do stream = StreamResponseProcess.build_stream(pid) {:ok, stream} end @@ -147,7 +136,7 @@ defmodule GRPC.Client.Adapters.Mint do request_type, opts ) - when request_type in [:stream_request_unary_response, :unary_request_response] do + when request_type in [:client_stream, :unary] do with stream <- StreamResponseProcess.build_stream(pid), responses <- Enum.to_list(stream), :ok <- check_for_error(responses) do @@ -165,37 +154,12 @@ defmodule GRPC.Client.Adapters.Mint do {:error, "an error occurred while when receiving data: error=#{inspect(response)}"} end - defp success_bidi_stream?(%GRPC.Client.Stream{ - grpc_type: :bidirectional_stream, - payload: %{response: {:ok, _resp}} - }), - do: true - - defp success_bidi_stream?(_stream), do: false - - defp success_server_stream?(%GRPC.Client.Stream{ - grpc_type: :server_stream, - payload: %{response: {:ok, _resp}} - }), - do: true - - defp success_server_stream?(_stream), do: false - - defp success_client_stream?(%GRPC.Client.Stream{ - grpc_type: :client_stream, - payload: %{response: {:ok, _resp}} - }), - do: true - - defp success_client_stream?(_stream), do: false - - defp success_unary_request?(%GRPC.Client.Stream{ - grpc_type: :unary, + defp success_response?(%GRPC.Client.Stream{ payload: %{response: {:ok, _resp}} }), do: true - defp success_unary_request?(_stream), do: false + defp success_response?(_stream), do: false defp do_request( %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, From 914d225478106d3687fbb8debc924177b1e0bfc0 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Tue, 27 Dec 2022 15:57:54 -0300 Subject: [PATCH 61/82] refactor response process to use call instead of cast --- .../connection_process/connection_process.ex | 12 ++-- .../adapters/mint/connection_process/state.ex | 2 + .../adapters/mint/stream_response_process.ex | 47 +++++++------ .../adapters/mint/connection_process_test.exs | 49 +++++++------ .../mint/stream_response_process_test.exs | 68 ++++++++++++------- 5 files changed, 106 insertions(+), 72 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index c53d902f..6e5e0cc9 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -1,10 +1,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do - @moduledoc """ - This module is responsible for managing a connection with a gRPC server. - It's also responsible for managing requests, which also includes checks for the - connection/request window size, splitting a given payload into appropriate sized chunks - and streaming those to the server using an internal queue. - """ + @moduledoc false + + # This module is responsible for managing a connection with a gRPC server. + # It's also responsible for managing requests, which also includes checks for the + # connection/request window size, splitting a given payload into appropriate sized chunks + # and streaming those to the server using an internal queue. use GenServer diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex index 8fc02778..bad73233 100644 --- a/lib/grpc/client/adapters/mint/connection_process/state.ex +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -1,4 +1,6 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do + @moduledoc false + defstruct [:conn, :parent, requests: %{}, request_stream_queue: :queue.new()] @type t :: %__MODULE__{ diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 6f6077fc..a6cd66f5 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -1,10 +1,9 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do - @moduledoc """ - This module represents the process responsible for consuming the - incoming messages from a connection. For each request, there will be - a process responsible for consuming its messages. At the end of a stream - this process will automatically be killed. - """ + @moduledoc false + # This module represents the process responsible for consuming the + # incoming messages from a connection. For each request, there will be + # a process responsible for consuming its messages. At the end of a stream + # this process will automatically be killed. @typep accepted_types :: :data | :trailers | :headers | :error @typep data_types :: binary() | Mint.Types.headers() | Mint.Types.error() @@ -41,7 +40,8 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do """ @spec done(pid()) :: :ok def done(pid) do - GenServer.cast(pid, {:consume_response, :done}) + :ok = GenServer.call(pid, {:consume_response, :done}) + :ok end @doc """ @@ -49,7 +49,8 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do """ @spec consume(pid(), type :: accepted_types, data :: data_types) :: :ok def consume(pid, type, data) when type in @accepted_types do - GenServer.cast(pid, {:consume_response, {type, data}}) + :ok = GenServer.call(pid, {:consume_response, {type, data}}) + :ok end # Callbacks @@ -72,7 +73,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:noreply, put_in(state[:from], from), {:continue, :produce_response}} end - def handle_cast({:consume_response, {:data, data}}, state) do + def handle_call({:consume_response, {:data, data}}, _from, state) do %{ buffer: buffer, grpc_stream: %{response_mod: res_mod, codec: codec}, @@ -85,44 +86,50 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do response = codec.decode(message, res_mod) new_responses = [{:ok, response} | responses] new_state = %{state | buffer: rest, responses: new_responses} - {:noreply, new_state, {:continue, :produce_response}} + {:reply, :ok, new_state, {:continue, :produce_response}} _ -> new_state = %{state | buffer: buffer <> data} - {:noreply, new_state, {:continue, :produce_response}} + {:reply, :ok, new_state, {:continue, :produce_response}} end end - def handle_cast( + def handle_call( {:consume_response, {type, headers}}, + _from, %{send_headers_or_trailers: true, responses: responses} = state ) when type in @header_types do state = update_compressor({type, headers}, state) new_responses = [get_headers_response(headers, type) | responses] - {:noreply, %{state | responses: new_responses}, {:continue, :produce_response}} + {:reply, :ok, %{state | responses: new_responses}, {:continue, :produce_response}} end - def handle_cast( + def handle_call( {:consume_response, {type, headers}}, + _from, %{send_headers_or_trailers: false, responses: responses} = state ) when type in @header_types do state = update_compressor({type, headers}, state) with {:error, _rpc_error} = error <- get_headers_response(headers, type) do - {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} + {:reply, :ok, %{state | responses: [error | responses]}, {:continue, :produce_response}} else - _any -> {:noreply, state, {:continue, :produce_response}} + _any -> {:reply, :ok, state, {:continue, :produce_response}} end end - def handle_cast({:consume_response, {:error, _error} = error}, %{responses: responses} = state) do - {:noreply, %{state | responses: [error | responses]}, {:continue, :produce_response}} + def handle_call( + {:consume_response, {:error, _error} = error}, + _from, + %{responses: responses} = state + ) do + {:reply, :ok, %{state | responses: [error | responses]}, {:continue, :produce_response}} end - def handle_cast({:consume_response, :done}, state) do - {:noreply, %{state | done: true}, {:continue, :produce_response}} + def handle_call({:consume_response, :done}, _from, state) do + {:reply, :ok, %{state | done: true}, {:continue, :produce_response}} end def handle_continue(:produce_response, state) do diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs index ce491d03..8989a6ae 100644 --- a/test/grpc/client/adapters/mint/connection_process_test.exs +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -1,6 +1,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do use GRPC.DataCase alias GRPC.Client.Adapters.Mint.ConnectionProcess + alias GRPC.Client.Adapters.Mint.StreamResponseProcess import ExUnit.CaptureLog @@ -193,23 +194,27 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do describe "handle_call/2 - cancel_request" do setup :valid_connection setup :valid_stream_request + setup :valid_stream_response test "reply with :ok when canceling the request is successful, also set stream response pid to done and remove request ref from state", %{ request_ref: request_ref, + stream_response_pid: response_pid, state: state } do - state = update_stream_response_process_to_test_pid(state, request_ref, self()) response = ConnectionProcess.handle_call({:cancel_request, request_ref}, nil, state) assert {:reply, :ok, new_state} = response assert %{} == new_state.requests - assert_receive {:"$gen_cast", {:consume_response, :done}}, 500 + response_state = :sys.get_state(response_pid) + assert [] == response_state.responses + assert true == response_state.done end end describe "handle_continue/2 - :process_stream_queue" do setup :valid_connection setup :valid_stream_request + setup :valid_stream_response test "do nothing when there is no window_size in the connection", %{ request_ref: request_ref, @@ -323,10 +328,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end test "send error message to stream response process when caller process ref is empty", - %{request_ref: request_ref, state: state} do - # instead of a real process I put test process pid to test that the message is sent - state = update_stream_response_process_to_test_pid(state, request_ref, self()) - + %{request_ref: request_ref, state: state, stream_response_pid: response_pid} do {_, state, _} = ConnectionProcess.handle_call({:stream_body, request_ref, <<1>>}, nil, state) @@ -336,10 +338,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do assert {:noreply, _new_state} = ConnectionProcess.handle_continue(:process_request_stream_queue, state) - assert_receive {:"$gen_cast", - {:consume_response, - {:error, %Mint.TransportError{reason: :closed, __exception__: true}}}}, - 500 + response_state = :sys.get_state(response_pid) + + assert [error: %Mint.TransportError{reason: :closed, __exception__: true}] == + response_state.responses end end @@ -363,32 +365,31 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do describe "handle_info - connection_closed - with request" do setup :valid_connection setup :valid_stream_request + setup :valid_stream_response test "send a message to parent process to inform the connection is down and end stream response process", %{ state: state, - request_ref: request_ref + stream_response_pid: response_pid } do socket = state.conn.socket # this is a mocked message to inform the connection is closed tcp_message = {:tcp_closed, socket} - state = update_stream_response_process_to_test_pid(state, request_ref, self()) assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) assert new_state.conn.state == :closed assert_receive {:elixir_grpc, :connection_down, pid}, 500 - - assert_receive {:"$gen_cast", {:consume_response, {:error, "the connection is closed"}}}, - 500 - - assert_receive {:"$gen_cast", {:consume_response, :done}}, 500 + response_state = :sys.get_state(response_pid) + assert [error: "the connection is closed"] == response_state.responses + assert true == response_state.done assert pid == self() end test "send a message to parent process to inform the connection is down and reply pending process", %{ state: state, - request_ref: request_ref + request_ref: request_ref, + stream_response_pid: response_pid } do socket = state.conn.socket # this is a mocked message to inform the connection is closed @@ -403,11 +404,12 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do {:noreply, state, {:continue, :process_request_stream_queue}} = response - state = update_stream_response_process_to_test_pid(state, request_ref, self()) assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) assert new_state.conn.state == :closed assert_receive {:elixir_grpc, :connection_down, pid}, 500 - assert_receive {:tag, {:error, "the connection is closed"}}, 500 + response_state = :sys.get_state(response_pid) + assert [error: "the connection is closed"] == response_state.responses + assert true == response_state.done assert pid == self() end end @@ -438,6 +440,13 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do %{request_ref: request_ref, state: state} end + defp valid_stream_response(%{request_ref: request_ref, state: state} = ctx) do + stream = build(:client_stream) + {:ok, pid} = StreamResponseProcess.start_link(stream, true) + state = update_stream_response_process_to_test_pid(state, request_ref, pid) + Map.merge(ctx, %{state: state, stream_response_pid: pid}) + end + def update_stream_response_process_to_test_pid(state, request_ref, test_pid) do request_ref_state = state.requests[request_ref] diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs index 3b14d593..b7baad16 100644 --- a/test/grpc/client/adapters/mint/stream_response_process_test.exs +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -18,7 +18,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do %{state: state} end - describe "handle_cast/2 - data" do + describe "handle_call/3 - data" do setup do part_1 = <<0, 0, 0, 0, 12, 10, 10, 72, 101, 108>> part_2 = <<108, 111, 32, 76, 117, 105, 115>> @@ -30,9 +30,10 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do state: state, data: {part1, _, _} } do - response = StreamResponseProcess.handle_cast({:consume_response, {:data, part1}}, state) + response = + StreamResponseProcess.handle_call({:consume_response, {:data, part1}}, self(), state) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert new_state.buffer == part1 end @@ -43,9 +44,13 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do expected_response_message = build(:hello_reply_rpc) response = - StreamResponseProcess.handle_cast({:consume_response, {:data, full_message}}, state) + StreamResponseProcess.handle_call( + {:consume_response, {:data, full_message}}, + self(), + state + ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert new_state.buffer == <<>> assert [{:ok, response_message}] = new_state.responses assert expected_response_message == response_message @@ -54,9 +59,11 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do test "append incoming message to existing buffer", %{state: state, data: {part1, part2, _}} do state = %{state | buffer: part1} expected_response_message = build(:hello_reply_rpc) - response = StreamResponseProcess.handle_cast({:consume_response, {:data, part2}}, state) - assert {:noreply, new_state, {:continue, :produce_response}} = response + response = + StreamResponseProcess.handle_call({:consume_response, {:data, part2}}, self(), state) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert new_state.buffer == <<>> assert [{:ok, response_message}] = new_state.responses assert expected_response_message == response_message @@ -66,16 +73,18 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do extra_data = <<0, 1, 2>> data = full <> extra_data expected_response_message = build(:hello_reply_rpc) - response = StreamResponseProcess.handle_cast({:consume_response, {:data, data}}, state) - assert {:noreply, new_state, {:continue, :produce_response}} = response + response = + StreamResponseProcess.handle_call({:consume_response, {:data, data}}, self(), state) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert new_state.buffer == extra_data assert [{:ok, response_message}] = new_state.responses assert expected_response_message == response_message end end - describe "handle_cast/2 - headers/trailers" do + describe "handle_call/3 - headers/trailers" do test_with_params( "put error in responses when incoming headers has error status", %{state: state}, @@ -91,12 +100,13 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do ] response = - StreamResponseProcess.handle_cast( + StreamResponseProcess.handle_call( {:consume_response, {type, headers}}, + self(), state ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert [{:error, error}] = new_state.responses assert %GRPC.RPCError{message: "Internal Server Error", status: 2} == error end, @@ -123,12 +133,13 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do ] response = - StreamResponseProcess.handle_cast( + StreamResponseProcess.handle_call( {:consume_response, {type, headers}}, + self(), state ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert [{type_response, response_headers}] = new_state.responses assert type == type_response @@ -156,12 +167,13 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do ] response = - StreamResponseProcess.handle_cast( + StreamResponseProcess.handle_call( {:consume_response, {type, headers}}, + self(), state ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert [] == new_state.responses end, do: [{:headers}, {:trailers}] @@ -180,12 +192,13 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do ] response = - StreamResponseProcess.handle_cast( + StreamResponseProcess.handle_call( {:consume_response, {:headers, headers}}, + self(), state ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert GRPC.Compressor.Gzip == new_state.compressor end @@ -202,41 +215,44 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do ] response = - StreamResponseProcess.handle_cast( + StreamResponseProcess.handle_call( {:consume_response, {:headers, headers}}, + self(), state ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert nil == new_state.compressor end end - describe "handle_cast/2 - errors" do + describe "handle_call/3 - errors" do test "add error tuple to responses", %{state: state} do error = {:error, "howdy"} response = - StreamResponseProcess.handle_cast( + StreamResponseProcess.handle_call( {:consume_response, error}, + self(), state ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert [response_error] = new_state.responses assert response_error == error end end - describe "handle_cast/2 - done" do + describe "handle_call/3 - done" do test "set state to done", %{state: state} do response = - StreamResponseProcess.handle_cast( + StreamResponseProcess.handle_call( {:consume_response, :done}, + self(), state ) - assert {:noreply, new_state, {:continue, :produce_response}} = response + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response assert true == new_state.done end end From a2b4be336614f292e853aecd9157c15f6461525f Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Tue, 27 Dec 2022 16:04:07 -0300 Subject: [PATCH 62/82] add pattern match for response calls --- .../connection_process/connection_process.ex | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 6e5e0cc9..4e10c912 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -223,32 +223,36 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do if State.empty_headers?(state, request_ref) do new_state = State.update_response_headers(state, request_ref, headers) - new_state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:headers, headers) + :ok = + new_state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:headers, headers) new_state else - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:trailers, headers) + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:trailers, headers) state end end defp process_response({:data, request_ref, new_data}, state) do - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:data, new_data) + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:data, new_data) state end defp process_response({:done, request_ref}, state) do - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.done() + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.done() {_ref, new_state} = State.pop_ref(state, request_ref) new_state @@ -272,9 +276,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do if from != nil do GenServer.reply(from, {:error, error}) else - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:error, error) + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:error, error) end {:noreply, State.update_conn(state, conn)} @@ -296,9 +301,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do if not is_nil(from) do GenServer.reply(from, {:error, error}) else - state - |> State.stream_response_pid(request_ref) - |> StreamResponseProcess.consume(:error, error) + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:error, error) end check_request_stream_queue(State.update_conn(state, conn)) @@ -373,8 +379,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp send_connection_close_and_end_stream_response(pid) do - StreamResponseProcess.consume(pid, :error, @connection_closed_error) - StreamResponseProcess.done(pid) + :ok = StreamResponseProcess.consume(pid, :error, @connection_closed_error) + :ok = StreamResponseProcess.done(pid) end def check_connection_status(state) do From 37c9a11cc5ea27b703ce1bfbad2c8fc1fe1a3bf6 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 11:33:39 -0300 Subject: [PATCH 63/82] unify return headers behavior to match gun behavior --- interop/lib/interop/client.ex | 2 +- lib/grpc/client/adapters/mint.ex | 33 +++++++++++++++---- .../adapters/mint/stream_response_process.ex | 23 +++++++++---- lib/grpc/stub.ex | 4 +-- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index 952b6b40..92f7f09d 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -187,7 +187,7 @@ defmodule Interop.Client do {headers, data, trailers} = ch - |> Grpc.Testing.TestService.Stub.full_duplex_call(metadata: metadata, return_headers: true) + |> Grpc.Testing.TestService.Stub.full_duplex_call(metadata: metadata) |> GRPC.Stub.send_request(req, end_stream: true) |> GRPC.Stub.recv(return_headers: true) |> process_full_duplex_response() diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 96f94407..7cbd986b 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -125,10 +125,19 @@ defmodule GRPC.Client.Adapters.Mint do defp mint_scheme(%Channel{scheme: "https"} = _channel), do: :https defp mint_scheme(_channel), do: :http - defp do_receive_data(%{payload: %{stream_response_pid: pid}}, request_type, _opts) + defp do_receive_data(%{payload: %{stream_response_pid: pid}}, request_type, opts) when request_type in [:bidirectional_stream, :server_stream] do - stream = StreamResponseProcess.build_stream(pid) - {:ok, stream} + produce_trailers? = opts[:return_headers] == true + stream = StreamResponseProcess.build_stream(pid, produce_trailers?) + headers_or_error = stream |> Enum.take(1) |> List.first() + # if this check fails then an error tuple will be returned + with {:headers, headers} <- headers_or_error do + if opts[:return_headers] do + {:ok, stream, %{headers: headers}} + else + {:ok, stream} + end + end end defp do_receive_data( @@ -137,9 +146,9 @@ defmodule GRPC.Client.Adapters.Mint do opts ) when request_type in [:client_stream, :unary] do - with stream <- StreamResponseProcess.build_stream(pid), - responses <- Enum.to_list(stream), - :ok <- check_for_error(responses) do + responses = pid |> StreamResponseProcess.build_stream() |> Enum.to_list() + + with :ok <- check_for_error(responses) do data = Keyword.fetch!(responses, :ok) if opts[:return_headers] do @@ -169,7 +178,7 @@ defmodule GRPC.Client.Adapters.Mint do headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) {:ok, stream_response_pid} = - StreamResponseProcess.start_link(stream, opts[:return_headers] || false) + StreamResponseProcess.start_link(stream, return_headers_for_request?(stream, opts)) response = ConnectionProcess.request(pid, "POST", path, headers, body, @@ -190,4 +199,14 @@ defmodule GRPC.Client.Adapters.Mint do if error, do: {:error, error}, else: :ok end + + defp return_headers_for_request?(%GRPC.Client.Stream{grpc_type: type}, _opts) + when type in [:bidirectional_stream, :server_stream] do + true + end + + defp return_headers_for_request?(_stream, opts) do + # Explicitly check for true to ensure the boolean type here + opts[:return_headers] == true or false + end end diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index a6cd66f5..1436afc8 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -23,16 +23,27 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do Given a pid from this process, build an Elixir.Stream that will consume the accumulated data inside this process """ - @spec build_stream(pid()) :: Enumerable.t() - def build_stream(pid) do + @spec build_stream(pid(), produce_trailers? :: boolean) :: Enumerable.t() + def build_stream(pid, produce_trailers? \\ true) do Stream.unfold(pid, fn pid -> - case GenServer.call(pid, :get_response, :infinity) do - nil -> nil - response -> {response, pid} - end + pid + |> GenServer.call(:get_response, :infinity) + |> process_response(produce_trailers?, pid) end) end + defp process_response(nil = _response, _produce_trailers, _pid), do: nil + + defp process_response({:trailers, _trailers}, false = produce_trailers?, pid) do + pid + |> GenServer.call(:get_response, :infinity) + |> process_response(produce_trailers?, pid) + end + + defp process_response(response, _produce_trailers, pid) do + {response, pid} + end + @doc """ Cast a message to process to inform that the stream has finished once all messages are produced. This process will automatically diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 5517a397..07841674 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -373,8 +373,8 @@ defmodule GRPC.Stub do {:ok, reply} = GRPC.Stub.recv(stream) # Reply is streaming - {:ok, enum} = GRPC.Stub.recv(stream) - replies = Enum.map(enum, fn({:ok, reply}) -> reply end) + {:ok, ex_stream} = GRPC.Stub.recv(stream) + replies = Enum.map(ex_stream, fn({:ok, reply}) -> reply end) ## Options From fe380234597b700f03560b31245455dff3a367fe Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 11:53:49 -0300 Subject: [PATCH 64/82] simplify code in favor of use enum module --- interop/lib/interop/client.ex | 40 +++++++++++++++++++++++--------- lib/grpc/client/adapters/mint.ex | 2 +- lib/grpc/stub.ex | 24 +++++++++++++++++++ 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index 92f7f09d..1a48c442 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -137,15 +137,13 @@ defmodule Interop.Client do {:ok, res_enum} = GRPC.Stub.recv(stream) reply = String.duplicate(<<0>>, 31415) - {:ok, %{payload: %{body: ^reply}}} = - Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, %{payload: %{body: ^reply}}} = Enum.at(res_enum, 0) Enum.each([{9, 8}, {2653, 1828}, {58979, 45904}], fn {res, payload} -> GRPC.Stub.send_request(stream, req.(res, payload)) reply = String.duplicate(<<0>>, res) - {:ok, %{payload: %{body: ^reply}}} = - Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, %{payload: %{body: ^reply}}} = Enum.at(res_enum, 0) end) GRPC.Stub.end_stream(stream) @@ -163,6 +161,25 @@ defmodule Interop.Client do [] = Enum.to_list(res_enum) end + @doc """ + We build the Stream struct (res_enum) using Stream.unfold/2 + the unfold function is built in such a way - for both adapters - that the acc is map used to find a + connection_stream process and the next_fun arg is a function that reads directly from the connection_stream + that is producing data. + Every time we execute the next_fun we read a chunk of data and remove that from the buffer. + That action by itself will generate a side effect that will update the state of the connection_stream by removing the chunk of data we just read. + An easier way to visualize that is. Take the code and the execution bellow as an example + + + ``` + iex(4)> ex_stream |> Stream.take(1) |> Enum.to_list() + [1] + iex(5)> ex_stream |> Enum.to_list() + [2, 3] + iex(6)> ex_stream |> Enum.to_list() + [] + ``` + """ def custom_metadata!(ch) do Logger.info("Run custom_metadata!") # UnaryCall @@ -200,15 +217,16 @@ defmodule Interop.Client do end defp process_full_duplex_response({:ok, res_enum, %{headers: new_headers}}) do - {:ok, data} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() - {:trailers, new_trailers} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, data} = Enum.at(res_enum, 0) + {:trailers, new_trailers} = Enum.at(res_enum, 0) {new_headers, data, new_trailers} end + defp process_full_duplex_response({:ok, res_enum}) do - {:headers, headers} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() - {:ok, data} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() - {:trailers, trailers} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:headers, headers} = Enum.at(res_enum, 0) + {:ok, data} = Enum.at(res_enum, 0) + {:trailers, trailers} = Enum.at(res_enum, 0) {headers, data, trailers} end @@ -233,7 +251,7 @@ defmodule Interop.Client do |> GRPC.Stub.send_request(req, end_stream: true) |> GRPC.Stub.recv() |> case do - {:ok, stream} -> Stream.take(stream, 1) |> Enum.to_list() |> List.first() + {:ok, stream} -> Enum.at(stream, 0) error -> error end end @@ -270,7 +288,7 @@ defmodule Interop.Client do |> GRPC.Stub.send_request(req) |> GRPC.Stub.recv() - {:ok, _} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, _} = Enum.at(res_enum, 0) stream = GRPC.Stub.cancel(stream) {:error, %GRPC.RPCError{status: 1}} = GRPC.Stub.recv(stream) end diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 7cbd986b..d34d097e 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -129,7 +129,7 @@ defmodule GRPC.Client.Adapters.Mint do when request_type in [:bidirectional_stream, :server_stream] do produce_trailers? = opts[:return_headers] == true stream = StreamResponseProcess.build_stream(pid, produce_trailers?) - headers_or_error = stream |> Enum.take(1) |> List.first() + headers_or_error = Enum.at(stream, 0) # if this check fails then an error tuple will be returned with {:headers, headers} <- headers_or_error do if opts[:return_headers] do diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 07841674..8aad738e 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -381,6 +381,30 @@ defmodule GRPC.Stub do * `:timeout` - request timeout * `:deadline` - when the request is timeout, will override timeout * `:return_headers` - when true, headers will be returned. + + ## Stream behavior + The action of consuming data from the replied stream will generate a side effect. + Unlikely the usual stream behavior you can see bellow. + ``` + iex(1)> s = Stream.cycle([1, 2, 3, 4]) + #Function<63.6935098/2 in Stream.unfold/2> + iex(2)> s |> Stream.take(1) |> Enum.to_list() + [1] + iex(3)> s |> Stream.take(1) |> Enum.to_list() + [1] + iex(4)> s |> Stream.take(3) |> Enum.to_list() + [1, 2, 3] + ``` + + when you try something similar with the stream returned by this function, you'll see a similar behavior as bellow + ``` + iex(4)> ex_stream |> Stream.take(1) |> Enum.to_list() + [1] + iex(5)> ex_stream |> Enum.to_list() + [2, 3] + iex(6)> ex_stream |> Enum.to_list() + [] + ``` """ @spec recv(GRPC.Client.Stream.t(), keyword()) :: {:ok, struct()} From 17a8e116b7e88c7394faf0d92db64c1257794ff0 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 12:10:36 -0300 Subject: [PATCH 65/82] refactor variable names for clarity --- .../adapters/mint/connection_process/connection_process.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 4e10c912..e016d09e 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -346,7 +346,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do new_state = state.request_stream_queue |> :queue.to_list() - |> Enum.reduce(state, fn {ref, _, _} = request, acc_state -> + |> Enum.reduce(state, fn {request_ref, _, _} = request, acc_state -> case request do {ref, _body, nil} -> acc_state @@ -361,7 +361,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do GenServer.reply(from, {:error, @connection_closed_error}) end - {_ref, new_state} = State.pop_ref(acc_state, ref) + {_request_data, new_state} = State.pop_ref(acc_state, request_ref) new_state end) From 71d1d10455f7e38c2efa51fe93ce351a18740b27 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 12:13:29 -0300 Subject: [PATCH 66/82] add todo for error handling on response process --- lib/grpc/client/adapters/mint/stream_response_process.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 1436afc8..72df816a 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -5,6 +5,9 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do # a process responsible for consuming its messages. At the end of a stream # this process will automatically be killed. + # TODO: Refactor the GenServer.call/3 occurrences on this module to produce + # telemetry errors in case of failures + @typep accepted_types :: :data | :trailers | :headers | :error @typep data_types :: binary() | Mint.Types.headers() | Mint.Types.error() From 6f38f12f1c9091c41340479fa11dfd55df2dbee2 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 12:16:04 -0300 Subject: [PATCH 67/82] add bit lengh for binary manipulation for clarity --- lib/grpc/message.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/grpc/message.ex b/lib/grpc/message.ex index 7f462615..03ac2e33 100644 --- a/lib/grpc/message.ex +++ b/lib/grpc/message.ex @@ -186,10 +186,10 @@ defmodule GRPC.Message do def get_message(data, compressor) do case data do - <<1, length::unsigned-integer-size(32), message::bytes-size(length), rest::binary>> -> + <<1::8, length::unsigned-integer-32, message::bytes-size(length), rest::binary>> -> {{1, compressor.decompress(message)}, rest} - <<0, length::unsigned-integer-size(32), message::bytes-size(length), rest::binary>> -> + <<0::8, length::unsigned-integer-32, message::bytes-size(length), rest::binary>> -> {{0, message}, rest} _other -> From fbdc8790fc348485fe3a1e74b4cf4aca57759f8a Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 12:17:50 -0300 Subject: [PATCH 68/82] rollback cowboy config --- lib/grpc/server/adapters/cowboy.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/grpc/server/adapters/cowboy.ex b/lib/grpc/server/adapters/cowboy.ex index ede18b3f..51caa7f0 100644 --- a/lib/grpc/server/adapters/cowboy.ex +++ b/lib/grpc/server/adapters/cowboy.ex @@ -203,8 +203,7 @@ defmodule GRPC.Server.Adapters.Cowboy do # https://github.com/ninenines/cowboy/issues/1398 # If there are 1000 streams in one connection, then 1000/s frames per stream. max_received_frame_rate: {10_000_000, 10_000}, - max_reset_stream_rate: {10_000, 10_000}, - stream_window_update_threshold: 65_500 + max_reset_stream_rate: {10_000, 10_000} }, Enum.into(opts, %{}) ) From 9e2436d5973cf49034f39d227a907ac5436417a0 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 12:24:30 -0300 Subject: [PATCH 69/82] unify factories to a single module --- test/support/factories/channel.ex | 41 ------------- test/support/factories/client/stream.ex | 29 --------- test/support/factories/proto/hello_world.ex | 9 --- test/support/factory.ex | 68 +++++++++++++++++++-- 4 files changed, 63 insertions(+), 84 deletions(-) delete mode 100644 test/support/factories/channel.ex delete mode 100644 test/support/factories/client/stream.ex delete mode 100644 test/support/factories/proto/hello_world.ex diff --git a/test/support/factories/channel.ex b/test/support/factories/channel.ex deleted file mode 100644 index 0e479137..00000000 --- a/test/support/factories/channel.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule GRPC.Factories.Channel do - alias GRPC.Channel - alias GRPC.Credential - - defmacro __using__(_opts) do - quote do - 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 - 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)) - - %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 - end -end diff --git a/test/support/factories/client/stream.ex b/test/support/factories/client/stream.ex deleted file mode 100644 index 216e9a66..00000000 --- a/test/support/factories/client/stream.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule GRPC.Factories.Client.Stream do - defmacro __using__(_opts) do - quote do - def client_stream_factory do - %GRPC.Client.Stream{ - __interface__: %{ - receive_data: &GRPC.Client.Stream.receive_data/2, - send_request: &GRPC.Client.Stream.send_request/3 - }, - canceled: false, - channel: build(:channel, adapter: GRPC.Client.Adapters.Mint), - codec: GRPC.Codec.Proto, - compressor: nil, - grpc_type: :unary, - headers: %{}, - method_name: "SayHello", - path: "/helloworld.Greeter/SayHello", - payload: %{}, - request_mod: Helloworld.HelloRequest, - response_mod: Helloworld.HelloReply, - rpc: {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}}, - server_stream: false, - service_name: "helloworld.Greeter", - accepted_compressors: [GRPC.Compressor.Gzip] - } - end - end - end -end diff --git a/test/support/factories/proto/hello_world.ex b/test/support/factories/proto/hello_world.ex deleted file mode 100644 index a1a8aa59..00000000 --- a/test/support/factories/proto/hello_world.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule GRPC.Factories.Proto.HelloWorld do - defmacro __using__(_opts) do - quote do - def hello_reply_rpc_factory do - %Helloworld.HelloReply{message: "Hello Luis"} - end - end - end -end diff --git a/test/support/factory.ex b/test/support/factory.ex index e3ea44e2..d38f10a2 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,11 +1,8 @@ defmodule GRPC.Factory do @moduledoc false - use GRPC.Factories.Channel - use GRPC.Factories.Client.Stream - - # Protobuf factories - use GRPC.Factories.Proto.HelloWorld + alias GRPC.Channel + alias GRPC.Credential def build(resource, attrs \\ %{}) do name = :"#{resource}_factory" @@ -19,4 +16,65 @@ defmodule GRPC.Factory do 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 + 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)) + + %Credential{ + ssl: [ + certfile: cert_path, + cacertfile: ca_path, + keyfile: key_path, + verify: :verify_peer, + fail_if_no_peer_cert: true, + versions: [:"tlsv1.2"] + ] + } + end + + def client_stream_factory do + %GRPC.Client.Stream{ + __interface__: %{ + receive_data: &GRPC.Client.Stream.receive_data/2, + send_request: &GRPC.Client.Stream.send_request/3 + }, + canceled: false, + channel: build(:channel, adapter: GRPC.Client.Adapters.Mint), + codec: GRPC.Codec.Proto, + compressor: nil, + grpc_type: :unary, + headers: %{}, + method_name: "SayHello", + path: "/helloworld.Greeter/SayHello", + payload: %{}, + request_mod: Helloworld.HelloRequest, + response_mod: Helloworld.HelloReply, + rpc: {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}}, + server_stream: false, + service_name: "helloworld.Greeter", + accepted_compressors: [GRPC.Compressor.Gzip] + } + end + + def hello_reply_rpc_factory do + %Helloworld.HelloReply{message: "Hello Luis"} + end end From ea46a41a52ab1b0a57991fd6b96412c5fe8d1dc1 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 29 Dec 2022 13:47:18 -0300 Subject: [PATCH 70/82] add case for chunk_body to avoid raise --- .../adapters/mint/connection_process/connection_process.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index e016d09e..3f713482 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -331,8 +331,10 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp chunk_body(body, bytes_length) do - <> = body - {head, tail} + case body do + <> -> {head, tail} + _other -> {body, <<>>} + end end def get_window_size(conn, ref) do From db5a200a54d6e48dccd30e32cc75737d994276ec Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Fri, 30 Dec 2022 16:21:40 -0300 Subject: [PATCH 71/82] Update interop/lib/interop/client.ex Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com> --- interop/lib/interop/client.ex | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index 1a48c442..11aaadeb 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -162,15 +162,12 @@ defmodule Interop.Client do end @doc """ - We build the Stream struct (res_enum) using Stream.unfold/2 - the unfold function is built in such a way - for both adapters - that the acc is map used to find a - connection_stream process and the next_fun arg is a function that reads directly from the connection_stream + We build the Stream struct (`res_enum` in the code) using `Stream.unfold/2`. + + The unfold function is built in such a way that - for both adapters - the accumulator is a map used to find the + `connection_stream`process and the `next_fun` argument is a function that reads directly from the `connection_stream` that is producing data. - Every time we execute the next_fun we read a chunk of data and remove that from the buffer. - That action by itself will generate a side effect that will update the state of the connection_stream by removing the chunk of data we just read. - An easier way to visualize that is. Take the code and the execution bellow as an example - - + Every time we execute `next_fun` we read a chunk of data. This means that `next_fun` will have the side effect of updating the state of the `connection_stream` process, removing the chunk of data that's being read from the underlying `GenServer`'s state. ``` iex(4)> ex_stream |> Stream.take(1) |> Enum.to_list() [1] From 1e86681d3d0f19eb964631298368ab1906d71dc8 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Fri, 30 Dec 2022 16:21:53 -0300 Subject: [PATCH 72/82] Update interop/lib/interop/client.ex Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com> --- interop/lib/interop/client.ex | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index 11aaadeb..cb8db1cc 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -168,14 +168,14 @@ defmodule Interop.Client do `connection_stream`process and the `next_fun` argument is a function that reads directly from the `connection_stream` that is producing data. Every time we execute `next_fun` we read a chunk of data. This means that `next_fun` will have the side effect of updating the state of the `connection_stream` process, removing the chunk of data that's being read from the underlying `GenServer`'s state. - ``` - iex(4)> ex_stream |> Stream.take(1) |> Enum.to_list() - [1] - iex(5)> ex_stream |> Enum.to_list() - [2, 3] - iex(6)> ex_stream |> Enum.to_list() - [] - ``` + ## Examples + + iex> ex_stream |> Stream.take(1) |> Enum.to_list() + [1] + iex> ex_stream |> Enum.to_list() + [2, 3] + iex> ex_stream |> Enum.to_list() + [] """ def custom_metadata!(ch) do Logger.info("Run custom_metadata!") From 55cbc28e3a9903ecd313e7dbb3919359c9a29ca0 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Fri, 30 Dec 2022 16:29:36 -0300 Subject: [PATCH 73/82] improve documentation for GRPC.Stub.recv --- interop/lib/interop/client.ex | 21 +++++---------------- lib/grpc/stub.ex | 27 ++++++++++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index cb8db1cc..76013736 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -1,8 +1,13 @@ defmodule Interop.Client do + import ExUnit.Assertions, only: [refute: 1] require Logger + # To better understand the behavior of streams used in this module + # we suggest you to check the documentation for `GRPC.Stub.recv/2` + # there is some unusual behavior that can be observed. + def connect(host, port, opts \\ []) do {:ok, ch} = GRPC.Stub.connect(host, port, opts) ch @@ -161,22 +166,6 @@ defmodule Interop.Client do [] = Enum.to_list(res_enum) end - @doc """ - We build the Stream struct (`res_enum` in the code) using `Stream.unfold/2`. - - The unfold function is built in such a way that - for both adapters - the accumulator is a map used to find the - `connection_stream`process and the `next_fun` argument is a function that reads directly from the `connection_stream` - that is producing data. - Every time we execute `next_fun` we read a chunk of data. This means that `next_fun` will have the side effect of updating the state of the `connection_stream` process, removing the chunk of data that's being read from the underlying `GenServer`'s state. - ## Examples - - iex> ex_stream |> Stream.take(1) |> Enum.to_list() - [1] - iex> ex_stream |> Enum.to_list() - [2, 3] - iex> ex_stream |> Enum.to_list() - [] - """ def custom_metadata!(ch) do Logger.info("Run custom_metadata!") # UnaryCall diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 8aad738e..5a03d373 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -383,8 +383,23 @@ defmodule GRPC.Stub do * `:return_headers` - when true, headers will be returned. ## Stream behavior - The action of consuming data from the replied stream will generate a side effect. + We build the Stream struct using `Stream.unfold/2`. + + The unfold function is built in such a way that - for both adapters - the accumulator is a map used to find the + `connection_stream`process and the `next_fun` argument is a function that reads directly from the `connection_stream` + that is producing data. + Every time we execute `next_fun` we read a chunk of data. This means that `next_fun` will have the side effect of updating the state of the `connection_stream` process, removing the chunk of data that's being read from the underlying `GenServer`'s state. + ## Examples + + iex> ex_stream |> Stream.take(1) |> Enum.to_list() + [1] + iex> ex_stream |> Enum.to_list() + [2, 3] + iex> ex_stream |> Enum.to_list() + [] + Unlikely the usual stream behavior you can see bellow. + ``` iex(1)> s = Stream.cycle([1, 2, 3, 4]) #Function<63.6935098/2 in Stream.unfold/2> @@ -395,16 +410,6 @@ defmodule GRPC.Stub do iex(4)> s |> Stream.take(3) |> Enum.to_list() [1, 2, 3] ``` - - when you try something similar with the stream returned by this function, you'll see a similar behavior as bellow - ``` - iex(4)> ex_stream |> Stream.take(1) |> Enum.to_list() - [1] - iex(5)> ex_stream |> Enum.to_list() - [2, 3] - iex(6)> ex_stream |> Enum.to_list() - [] - ``` """ @spec recv(GRPC.Client.Stream.t(), keyword()) :: {:ok, struct()} From f9fdfff4c84078aa5613d23a71970d9885a7fbc1 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Fri, 30 Dec 2022 16:36:04 -0300 Subject: [PATCH 74/82] remove doc for GRPC.Stub.call/5 --- lib/grpc/stub.ex | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 5a03d373..f5d2f66b 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -218,25 +218,24 @@ defmodule GRPC.Stub do adapter.disconnect(channel) end - @doc """ - The actual function invoked when invoking a rpc function. - - ## Returns - - * Unary calls. `{:ok, reply} | {:ok, headers_map} | {:error, error}` - * Client streaming. A `GRPC.Client.Stream` - * Server streaming. `{:ok, Enumerable.t} | {:ok, Enumerable.t, trailers_map} | {:error, error}` - - ## Options - - * `:timeout` - request timeout. Default is 10s for unary calls and `:infinity` for - client or server streaming calls - * `:deadline` - when the request is timeout, will override timeout - * `:metadata` - a map, your custom metadata - * `:return_headers` - default is false. When it's true, a three elem tuple will be returned - with the last elem being a map of headers `%{headers: headers, trailers: trailers}`(unary) or - `%{headers: headers}`(server streaming) - """ + @doc false + # The actual function invoked when invoking a rpc function. + # + # Returns + # + # * Unary calls. `{:ok, reply} | {:ok, headers_map} | {:error, error}` + # * Client streaming. A `GRPC.Client.Stream` + # * Server streaming. `{:ok, Enumerable.t} | {:ok, Enumerable.t, trailers_map} | {:error, error}` + # + # Options + # + # * `:timeout` - request timeout. Default is 10s for unary calls and `:infinity` for + # client or server streaming calls + # * `:deadline` - when the request is timeout, will override timeout + # * `:metadata` - a map, your custom metadata + # * `:return_headers` - default is false. When it's true, a three elem tuple will be returned + # 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 def call(_service_mod, rpc, %{channel: channel} = stream, request, opts) do {_, {req_mod, req_stream}, {res_mod, response_stream}} = rpc From 7edc0639afd63026f4c2fc71cdaf806b9f5c2ade Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Tue, 3 Jan 2023 08:34:28 -0300 Subject: [PATCH 75/82] make connection status check a private function --- .../adapters/mint/connection_process/connection_process.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 3f713482..6aeb14ee 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -385,7 +385,7 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do :ok = StreamResponseProcess.done(pid) end - def check_connection_status(state) do + defp check_connection_status(state) do if Mint.HTTP.open?(state.conn) do check_request_stream_queue(state) else From 736e203d2a012f8b57f4179ba889e7eed5ab393a Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 4 Jan 2023 18:52:05 -0300 Subject: [PATCH 76/82] apply PR comments --- lib/grpc/client/adapters/mint.ex | 4 ++-- .../connection_process/connection_process.ex | 9 ++++----- .../adapters/mint/stream_response_process.ex | 14 ++++++++------ lib/grpc/stub.ex | 16 +--------------- 4 files changed, 15 insertions(+), 28 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index d34d097e..426f6cc8 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -46,7 +46,7 @@ defmodule GRPC.Client.Adapters.Mint do @impl true def send_request(%{channel: %{adapter_payload: nil}}, _message, _opts), - do: raise("Can't perform a request without a connection process") + do: raise(ArgumentError, "Can't perform a request without a connection process") def send_request(stream, message, opts) do {:ok, data, _} = GRPC.Message.to_data(message, opts) @@ -207,6 +207,6 @@ defmodule GRPC.Client.Adapters.Mint do defp return_headers_for_request?(_stream, opts) do # Explicitly check for true to ensure the boolean type here - opts[:return_headers] == true or false + opts[:return_headers] == true end end diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 6aeb14ee..b99dd5fc 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -199,13 +199,12 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {{:value, request}, queue} = :queue.out(state.request_stream_queue) {ref, body, _from} = request window_size = get_window_size(state.conn, ref) - body_size = IO.iodata_length(body) dequeued_state = State.update_request_stream_queue(state, queue) cond do # Do nothing, wait for server (on stream/2) to give us more window size window_size == 0 -> {:noreply, state} - body_size > window_size -> chunk_body_and_enqueue_rest(request, dequeued_state) + IO.iodata_length(body) > window_size -> chunk_body_and_enqueue_rest(request, dequeued_state) true -> stream_body_and_reply(request, dequeued_state) end end @@ -312,9 +311,9 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp stream_body(conn, request_ref, body, true = _stream_eof?) do - with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, request_ref, body), - {:ok, conn} <- Mint.HTTP.stream_request_body(conn, request_ref, :eof) do - {:ok, conn} + case Mint.HTTP.stream_request_body(conn, request_ref, body) do + {:ok, conn} -> Mint.HTTP.stream_request_body(conn, request_ref, :eof) + error -> error end end diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index 72df816a..fa78b077 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -6,7 +6,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do # this process will automatically be killed. # TODO: Refactor the GenServer.call/3 occurrences on this module to produce - # telemetry errors in case of failures + # telemetry events and log entries in case of failures @typep accepted_types :: :data | :trailers | :headers | :error @typep data_types :: binary() | Mint.Types.headers() | Mint.Types.error() @@ -24,7 +24,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do @doc """ Given a pid from this process, build an Elixir.Stream that will consume the accumulated - data inside this process + data inside this process """ @spec build_stream(pid(), produce_trailers? :: boolean) :: Enumerable.t() def build_stream(pid, produce_trailers? \\ true) do @@ -127,10 +127,12 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do when type in @header_types do state = update_compressor({type, headers}, state) - with {:error, _rpc_error} = error <- get_headers_response(headers, type) do - {:reply, :ok, %{state | responses: [error | responses]}, {:continue, :produce_response}} - else - _any -> {:reply, :ok, state, {:continue, :produce_response}} + case get_headers_response(headers, type) do + {:error, _rpc_error} = error -> + {:reply, :ok, %{state | responses: [error | responses]}, {:continue, :produce_response}} + + _any -> + {:reply, :ok, state, {:continue, :produce_response}} end end diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index f5d2f66b..520fce00 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -219,7 +219,7 @@ defmodule GRPC.Stub do end @doc false - # The actual function invoked when invoking a rpc function. + # # The actual function invoked when invoking an RPC function. # # Returns # @@ -236,7 +236,6 @@ defmodule GRPC.Stub do # * `:return_headers` - default is false. When it's true, a three elem tuple will be returned # 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 def call(_service_mod, rpc, %{channel: channel} = stream, request, opts) do {_, {req_mod, req_stream}, {res_mod, response_stream}} = rpc @@ -396,19 +395,6 @@ defmodule GRPC.Stub do [2, 3] iex> ex_stream |> Enum.to_list() [] - - Unlikely the usual stream behavior you can see bellow. - - ``` - iex(1)> s = Stream.cycle([1, 2, 3, 4]) - #Function<63.6935098/2 in Stream.unfold/2> - iex(2)> s |> Stream.take(1) |> Enum.to_list() - [1] - iex(3)> s |> Stream.take(1) |> Enum.to_list() - [1] - iex(4)> s |> Stream.take(3) |> Enum.to_list() - [1, 2, 3] - ``` """ @spec recv(GRPC.Client.Stream.t(), keyword()) :: {:ok, struct()} From b38ec4260a582a3c2336bb4b08182bf0054011f1 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 4 Jan 2023 19:03:45 -0300 Subject: [PATCH 77/82] apply missing PR comments --- .../connection_process/connection_process.ex | 16 ++++++++++------ lib/grpc/stub.ex | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index b99dd5fc..4fcafa55 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -182,9 +182,13 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do state = State.update_conn(state, conn) state = - if state.requests == %{}, - do: state, - else: Enum.reduce(responses, state, &process_response/2) + case state.requests do + requests when map_size(requests) == 0 -> + state + + _ -> + Enum.reduce(responses, state, &process_response/2) + end check_connection_status(state) @@ -286,18 +290,18 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do end defp stream_body_and_reply({request_ref, body, from}, state) do - send_eof? = from == nil + send_eof? = is_nil(from) case stream_body(state.conn, request_ref, body, send_eof?) do {:ok, conn} -> - if not is_nil(from) do + if not send_eof? do GenServer.reply(from, :ok) end check_request_stream_queue(State.update_conn(state, conn)) {:error, conn, error} -> - if not is_nil(from) do + if not send_eof? do GenServer.reply(from, {:error, error}) else :ok = diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 520fce00..8ae75e02 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -387,6 +387,7 @@ defmodule GRPC.Stub do `connection_stream`process and the `next_fun` argument is a function that reads directly from the `connection_stream` that is producing data. Every time we execute `next_fun` we read a chunk of data. This means that `next_fun` will have the side effect of updating the state of the `connection_stream` process, removing the chunk of data that's being read from the underlying `GenServer`'s state. + ## Examples iex> ex_stream |> Stream.take(1) |> Enum.to_list() From 7ae3036f1ad7e9661be7af1d595b93267b6b621d Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 4 Jan 2023 19:05:33 -0300 Subject: [PATCH 78/82] Update lib/grpc/stub.ex Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com> --- lib/grpc/stub.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index 8ae75e02..0efec677 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -388,6 +388,7 @@ defmodule GRPC.Stub do that is producing data. Every time we execute `next_fun` we read a chunk of data. This means that `next_fun` will have the side effect of updating the state of the `connection_stream` process, removing the chunk of data that's being read from the underlying `GenServer`'s state. + ## Examples iex> ex_stream |> Stream.take(1) |> Enum.to_list() From c99f547548076be4f1c5ce086a30f4d310b6a5cc Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Wed, 4 Jan 2023 19:08:19 -0300 Subject: [PATCH 79/82] anotate stream response process for genserver callback functions --- lib/grpc/client/adapters/mint/stream_response_process.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex index fa78b077..249bd280 100644 --- a/lib/grpc/client/adapters/mint/stream_response_process.ex +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -69,6 +69,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do # Callbacks + @impl true def init({stream, send_headers_or_trailers?}) do state = %{ grpc_stream: stream, @@ -83,6 +84,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:ok, state} end + @impl true def handle_call(:get_response, from, state) do {:noreply, put_in(state[:from], from), {:continue, :produce_response}} end @@ -148,6 +150,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do {:reply, :ok, %{state | done: true}, {:continue, :produce_response}} end + @impl true def handle_continue(:produce_response, state) do case state do %{from: nil} -> @@ -195,6 +198,7 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding_name end) end + @impl true def terminate(_reason, _state) do :normal end From c3950555e8656689786e665a2902e1baf5b44c01 Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Thu, 5 Jan 2023 05:46:01 -0300 Subject: [PATCH 80/82] Update lib/grpc/client/adapters/mint/connection_process/connection_process.ex Co-authored-by: Paulo Valente <16843419+polvalente@users.noreply.github.com> --- .../adapters/mint/connection_process/connection_process.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 4fcafa55..0fab29d9 100644 --- a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -277,6 +277,8 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do {:error, conn, error} -> if from != nil do + # We need an explicit reply here because the process that called this GenServer + # isn't the same one that's expecting the reply. GenServer.reply(from, {:error, error}) else :ok = From b278d053789f8193b7a574596519069a811692ba Mon Sep 17 00:00:00 2001 From: Luis Gustavo Beligante Date: Sat, 7 Jan 2023 14:14:25 -0300 Subject: [PATCH 81/82] improve error messages --- lib/grpc/client/adapters/mint.ex | 6 +++--- test/grpc/client/adapters/mint_test.exs | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 426f6cc8..1e45a95d 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -26,11 +26,11 @@ defmodule GRPC.Client.Adapters.Mint do {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} error -> - {:error, "An error happened while trying to opening the connection: #{inspect(error)}"} + {:error, "Error when opening connection: #{inspect(error)}"} end catch :exit, reason -> - {:error, "An error happened while trying to opening the connection: #{inspect(reason)}"} + {:error, "Error when opening connection: #{inspect(reason)}"} end @impl true @@ -160,7 +160,7 @@ defmodule GRPC.Client.Adapters.Mint do end def handle_errors_receive_data(%GRPC.Client.Stream{payload: %{response: response}}, _opts) do - {:error, "an error occurred while when receiving data: error=#{inspect(response)}"} + {:error, "Error occurred when receiving data: #{inspect(response)}"} end defp success_response?(%GRPC.Client.Stream{ diff --git a/test/grpc/client/adapters/mint_test.exs b/test/grpc/client/adapters/mint_test.exs index 95f33a1d..b9930243 100644 --- a/test/grpc/client/adapters/mint_test.exs +++ b/test/grpc/client/adapters/mint_test.exs @@ -30,8 +30,7 @@ defmodule GRPC.Client.Adapters.MintTest do # Ensure that changing one of the options breaks things assert {:error, message} = Mint.connect(channel, transport_opts: [ip: "256.0.0.0"]) - assert message == - "An error happened while trying to opening the connection: {:error, :badarg}" + assert message == "Error when opening connection: {:error, :badarg}" end end end From 00b0b6ab9df229c4cb9f845030adcc08e57b6d41 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:26:20 -0300 Subject: [PATCH 82/82] Apply suggestions from code review --- lib/grpc/client/adapters/mint.ex | 6 +++--- test/grpc/client/adapters/mint_test.exs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex index 1e45a95d..eea1c8a2 100644 --- a/lib/grpc/client/adapters/mint.ex +++ b/lib/grpc/client/adapters/mint.ex @@ -26,11 +26,11 @@ defmodule GRPC.Client.Adapters.Mint do {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} error -> - {:error, "Error when opening connection: #{inspect(error)}"} + {:error, "Error while opening connection: #{inspect(error)}"} end catch :exit, reason -> - {:error, "Error when opening connection: #{inspect(reason)}"} + {:error, "Error while opening connection: #{inspect(reason)}"} end @impl true @@ -160,7 +160,7 @@ defmodule GRPC.Client.Adapters.Mint do end def handle_errors_receive_data(%GRPC.Client.Stream{payload: %{response: response}}, _opts) do - {:error, "Error occurred when receiving data: #{inspect(response)}"} + {:error, "Error occurred while receiving data: #{inspect(response)}"} end defp success_response?(%GRPC.Client.Stream{ diff --git a/test/grpc/client/adapters/mint_test.exs b/test/grpc/client/adapters/mint_test.exs index b9930243..6b849be0 100644 --- a/test/grpc/client/adapters/mint_test.exs +++ b/test/grpc/client/adapters/mint_test.exs @@ -30,7 +30,7 @@ defmodule GRPC.Client.Adapters.MintTest do # Ensure that changing one of the options breaks things assert {:error, message} = Mint.connect(channel, transport_opts: [ip: "256.0.0.0"]) - assert message == "Error when opening connection: {:error, :badarg}" + assert message == "Error while opening connection: {:error, :badarg}" end end end