Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ _build/
deps/
.elixir_ls/
mix.lock
# antd-elixir is a library but commits its lock: CI and dev resolve the same tree
!antd-elixir/mix.lock

# Zig
.zig-cache/
Expand Down
110 changes: 57 additions & 53 deletions antd-elixir/lib/antd/grpc_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,28 @@ defmodule Antd.GrpcAdapter do
# there (`x-antd-error-code`) for the errors that share a gRPC status with
# older ones. `GRPC.RPCError` has no field for metadata.
#
# How: the call's gun messages are taken from this process's mailbox up to
# the last one of the response stream, their headers and trailers noted,
# and then sent back to this process in their original order for Gun's own
# `receive_data/2` to read, so decoding and every success and error result
# stay Gun's. No message of the stream can arrive after its last one, so
# re-sending them cannot reorder anything.
# How: grpc 1.0's Gun adapter reads a call's response from a per-stream
# `GRPC.Client.Adapters.Gun.StreamResponseProcess`, which queues the
# stream's messages and hands them out one `await/2` at a time. This adapter
# takes the stream's messages out of that process up to the last one of the
# response, notes their headers and trailers, then plays them into a fresh
# `StreamResponseProcess` (as the gun messages it was built to receive) and
# lets Gun's own `receive_data/2` read that one. So decoding and every
# success and error result stay Gun's.
#
# Calls without `return_headers`, and every server-streaming call, go to
# Gun untouched. The SDK only sets `return_headers` on unary calls for its
# pointer functions.
#
# Written against grpc 1.0.5 with gun 2.4.1. It leans on
# `StreamResponseProcess.start_link/0`, `await/2` and the gun messages that
# process accepts, and on the `:response_pid` key of the stream payload.
# Re-run test/antd/grpc_pointers_test.exs on any grpc upgrade.

@behaviour GRPC.Client.Adapter

alias GRPC.Client.Adapters.Gun
alias GRPC.Client.Adapters.Gun.StreamResponseProcess

@impl true
defdelegate connect(channel, opts), to: Gun
Expand Down Expand Up @@ -62,15 +70,21 @@ defmodule Antd.GrpcAdapter do
def receive_data(stream, opts), do: Gun.receive_data(stream, opts)

defp receive_with_metadata(stream, opts) do
%{channel: %{adapter_payload: %{conn_pid: conn_pid}}, payload: %{stream_ref: ref}} = stream
%{payload: %{response_pid: response_pid} = payload} = stream

case collect(conn_pid, ref, await_timeout(opts[:timeout])) do
case collect(response_pid, await_timeout(opts[:timeout]), []) do
{:ok, messages} ->
Enum.each(messages, &send(self(), &1))

case Gun.receive_data(stream, opts) do
{:error, error} -> {:error, error, metadata(messages)}
result -> result
{:ok, replay} = StreamResponseProcess.start_link()
Enum.each(messages, &send(replay, &1))
replayed = %{stream | payload: %{payload | response_pid: replay}}

try do
case Gun.receive_data(replayed, opts) do
{:error, error} -> {:error, error, metadata(messages)}
result -> result
end
after
stop(replay)
end

{:error, _} = error ->
Expand All @@ -82,59 +96,49 @@ defmodule Antd.GrpcAdapter do
defp await_timeout(timeout) when is_integer(timeout), do: timeout * 2
defp await_timeout(timeout), do: timeout

defp collect(conn_pid, ref, timeout) do
mref = Process.monitor(conn_pid)

try do
collect(conn_pid, ref, timeout, mref, [])
after
Process.demonitor(mref, [:flush])
end
end

# Takes the stream's messages until its last one: a response or data frame
# that ends the stream, its trailers, or an error. An error message is kept
# for Gun to read and map as it would have. If the connection goes down,
# what was taken goes back and Gun reports the dead connection.
defp collect(conn_pid, ref, timeout, mref, acc) do
receive do
{:gun_response, ^conn_pid, ^ref, :nofin, _status, _headers} = msg ->
collect(conn_pid, ref, timeout, mref, [msg | acc])

{:gun_data, ^conn_pid, ^ref, :nofin, _data} = msg ->
collect(conn_pid, ref, timeout, mref, [msg | acc])

{:gun_response, ^conn_pid, ^ref, :fin, _status, _headers} = msg ->
{:ok, Enum.reverse([msg | acc])}

{:gun_data, ^conn_pid, ^ref, :fin, _data} = msg ->
{:ok, Enum.reverse([msg | acc])}
# that ends the stream, its trailers, or an error. Each comes back as the
# gun message that produced it, for a StreamResponseProcess to queue again.
# A dead stream process answers with a connection error, which Gun reports.
defp collect(response_pid, timeout, acc) do
case StreamResponseProcess.await(response_pid, timeout) do
{:response, fin, status, headers} ->
next(response_pid, timeout, acc, fin, {:gun_response, nil, nil, fin, status, headers})

{:gun_trailers, ^conn_pid, ^ref, _trailers} = msg ->
{:ok, Enum.reverse([msg | acc])}
{:data, fin, data} ->
next(response_pid, timeout, acc, fin, {:gun_data, nil, nil, fin, data})

{:gun_inform, ^conn_pid, ^ref, _status, _headers} = msg ->
{:ok, Enum.reverse([msg | acc])}
{:trailers, trailers} ->
{:ok, Enum.reverse([{:gun_trailers, nil, nil, trailers} | acc])}

{:gun_error, ^conn_pid, ^ref, _reason} = msg ->
{:ok, Enum.reverse([msg | acc])}

{:gun_error, ^conn_pid, _reason} = msg ->
{:ok, Enum.reverse([msg | acc])}

{:DOWN, ^mref, :process, ^conn_pid, _reason} ->
{:ok, Enum.reverse(acc)}
after
timeout ->
{:error, :timeout} ->
# What Gun's adapter answers when the server does not reply in time.
{:error,
GRPC.RPCError.exception(
GRPC.Status.deadline_exceeded(),
"timeout when waiting for server"
)}

{:error, {:stream_error, reason}} ->
{:ok, Enum.reverse([{:gun_error, nil, nil, reason} | acc])}

{:error, {:connection_error, reason}} ->
{:ok, Enum.reverse([{:gun_error, nil, reason} | acc])}

{:error, {:unexpected_message, _}} = unexpected ->
{:ok, Enum.reverse([unexpected | acc])}
end
end

defp next(_response_pid, _timeout, acc, :fin, message), do: {:ok, Enum.reverse([message | acc])}
defp next(response_pid, timeout, acc, :nofin, message), do: collect(response_pid, timeout, [message | acc])

defp stop(pid) do
GenServer.stop(pid)
catch
:exit, _ -> :ok
end

defp metadata(messages) do
Enum.reduce(messages, %{headers: %{}, trailers: %{}}, fn
{:gun_response, _, _, _, _, headers}, acc ->
Expand Down
2 changes: 1 addition & 1 deletion antd-elixir/lib/antd/grpc_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1379,7 +1379,7 @@ defmodule Antd.GrpcClient do
#
# Message size: a manifest can be 64 MiB, and a Build or Decode response
# carries it, so antd-go raises its client's 4 MiB receive default per
# call. grpc-elixir 0.11 has no receive limit to raise: its Gun adapter
# call. grpc-elixir 0.11 and 1.0 have no receive limit to raise: the Gun adapter
# reads a unary response whole, whatever its size; GRPC.Stub.connect has no
# option for one, and a call rejects any option it does not know; and
# sends are capped only by GRPC.Message's 2 GiB default. So no limit is
Expand Down
11 changes: 10 additions & 1 deletion antd-elixir/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ defmodule Antd.MixProject do
[
{:req, "~> 0.4"},
{:jason, "~> 1.4"},
{:grpc, "~> 0.9"},
# grpc < 1.0 carries the advisories GHSA-grp7-v8xh-rj7h (critical) and
# three more, all fixed in 1.0.0. From 1.0 the client is its own package
# and gun is an optional dependency of it, so it is listed here.
# Antd.GrpcAdapter copies from grpc's Gun adapter internals, so keep the
# range to one minor line and re-run test/antd/grpc_pointers_test.exs
# when moving it.
{:grpc, "~> 1.0.5"},
{:gun, "~> 2.4.0"},
{:protobuf, "~> 0.12"},
# Only the test suite's mock servers use the server half.
{:grpc_server, "~> 1.0.5", only: :test},
{:bypass, "~> 2.1", only: :test}
]
end
Expand Down
27 changes: 27 additions & 0 deletions antd-elixir/mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
%{
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
"cowboy": {:hex, :cowboy, "2.20.0", "63f6e3afa7e14201498142a10d00379f5e11761f69e69782807c713627f6bc73", [:make, :rebar3], [{:cowlib, ">= 2.21.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "a0be47464a2887f71c061ce90723dc17441c9b81ba9569cd9b596eb0367d1edd"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.21.0", "3ea757c79784bdbdebac7857ce3adf2290af6b8680d4248c75e40ced2e98c89e", [:make, :rebar3], [], "hexpm", "0960248773762a5319b3c17285028bc76f54f58ef0f9451367401bdf67b46274"},
"finch": {:hex, :finch, "0.24.0", "4022b6194e907b6d9b597c168001cac38e488a35fe36cef9d8297e3159782510", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "33ba40069c3587c2f99f9125b766e19dad87d6d54be3c6961db2304df04cef00"},
"flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
"gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"},
"googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"},
"grpc": {:hex, :grpc, "1.0.5", "71082f1a2d0ff6d2a4198efaf514c33e71485167d1e278022bc0641cd788cea2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:grpc_core, "~> 1.0.5", [hex: :grpc_core, repo: "hexpm", optional: false]}, {:gun, "~> 2.4.0", [hex: :gun, repo: "hexpm", optional: true]}, {:mint, "~> 1.9", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "5e892e542a2e0f05ed17a4ad3814483e3980eaa93906f993dac9e08369b178ea"},
"grpc_core": {:hex, :grpc_core, "1.0.5", "add39a2ca38604f73c8b2feed30c0fd2d2291daedac8060ee1a75b5658f0a69d", [:mix], [{:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.17", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a18ae7380cfebc20271aad36393e8aa041e1faa926fa25f40328e4350f83d8f0"},
"grpc_server": {:hex, :grpc_server, "1.0.5", "7cad10f412de85d5a7329f09302f17644142702ca7d5cbe47cf68a6cce463ad9", [:mix], [{:cowboy, "~> 2.14", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowlib, "~> 2.14", [hex: :cowlib, repo: "hexpm", optional: false]}, {:flow, "~> 1.2", [hex: :flow, repo: "hexpm", optional: false]}, {:grpc_core, "~> 1.0.5", [hex: :grpc_core, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.17", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1a7df870e92dad4d7c12ec1bd264a06c94cbab2f12b054064636ee2b486607b0"},
"gun": {:hex, :gun, "2.4.1", "c3a8bff8e155e1fc8e499216599bb7effbd27bc1985662a8b25016090dc18428", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "1450575b4d393aa0811322727b90ad1dd94b5c10026f54a1641785bcbd250384"},
"hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.11.0", "a713551624815c0435237b93d90ea8b9b14254690c66f732d0ef8930f76ff1d9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "c6279ba2d6aa3a383a1d4cfbe7b59f42e6efd400f58d8e2acfeac48a438693ab"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"},
"plug_cowboy": {:hex, :plug_cowboy, "2.9.0", "87e21e0d9054ced99c36d128f49e3ea2cd8b745fffb97de50bff99706087af4f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2002bafba4f3a45b55a58e68d70211b153a7ed18d37edb1ceb6e96e7a92c422e"},
"plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"},
"protobuf": {:hex, :protobuf, "0.17.1", "5c058a4abbdf3c094bbdab142022b22950dd25214fcfdc624ae9bf0c60f9346f", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "76770ecda0ef057e540932f15b5a143ba8877875e88727f53429a7dc4dbf8b6c"},
"ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"},
"req": {:hex, :req, "0.7.5", "0c3293cbf7717aba20b1b1b3ea13fe8d3fdf6f1557490f390c3f1ef6dd9db955", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "576853c4bf403faa3c1fa5d093e0759227083b78f8a6864c67d62c0a9c45514d"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
}