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
5 changes: 5 additions & 0 deletions grpc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
### Behavior Changes

* The Mint adapter now enforces the requested `:timeout`/`:deadline` on unary receives. A unary call that never receives a response fails with `DEADLINE_EXCEEDED` after the documented 10s default instead of blocking indefinitely, and an explicit `:deadline` now takes precedence over `:timeout`.

### Bug Fixes

* The Gun adapter no longer shares a named channel's connection process across Erlang nodes. It was registered in `:global`, so a node connecting with a `:name` already used on another node adopted the remote connection process; `connect/2` returned `{:ok, channel}`, but every RPC on it then raised `ArgumentError` because `GRPC.Stub.call/5` calls `Process.alive?/1` on the connection pid and that raises for remote pids. Connection processes are now registered in the node-local `GRPC.Client.Registry`, so reuse is per node.

## v1.0.4 (2026-0-15)

### Bug Fixes
Expand Down
5 changes: 4 additions & 1 deletion grpc/lib/grpc/client/adapters/gun/connection_process.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ defmodule GRPC.Client.Adapters.Gun.ConnectionProcess do
Request-specific Gun messages are routed to per-stream response processes, so
this process only needs to manage connection-level lifecycle and stream
bookkeeping.

Named channels are registered in the node-local `GRPC.Client.Registry` under
`{ref, host, port}`, so a connection is reused by every caller on the node.
"""

use GenServer
Expand Down Expand Up @@ -177,7 +180,7 @@ defmodule GRPC.Client.Adapters.Gun.ConnectionProcess do
end

defp via(channel) do
{:global, {__MODULE__, owner_key(channel)}}
{:via, Registry, {GRPC.Client.Registry, {__MODULE__, owner_key(channel)}}}
end

defp start_response_process do
Expand Down
41 changes: 41 additions & 0 deletions grpc/test/grpc/adapters/gun_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule GRPC.Client.Adapters.GunTest do
use GRPC.Client.DataCase, async: true

alias GRPC.Client.Adapters.Gun
alias GRPC.Client.Adapters.Gun.ConnectionProcess

defmodule Endpoint do
use GRPC.Endpoint
Expand Down Expand Up @@ -97,6 +98,46 @@ defmodule GRPC.Client.Adapters.GunTest do
]
)
end

test "reuses the connection process for a named channel", %{
port: port,
credential: credential
} do
channel =
build(:channel, port: port, host: "localhost", cred: credential, ref: make_ref())

assert {:ok, %{adapter_payload: %{conn_pid: conn_pid}} = connected} =
Gun.connect(channel, [])

on_exit(fn -> Gun.disconnect(connected) end)

assert {:ok, %{adapter_payload: %{conn_pid: ^conn_pid}}} = Gun.connect(channel, [])

assert [{^conn_pid, _}] =
Registry.lookup(
GRPC.Client.Registry,
{ConnectionProcess, {channel.ref, channel.host, channel.port}}
)
end

test "does not reuse the connection process for an unnamed channel", %{
port: port,
credential: credential
} do
channel = build(:channel, port: port, host: "localhost", cred: credential)

assert {:ok, %{adapter_payload: %{conn_pid: first_pid}} = first} = Gun.connect(channel, [])

assert {:ok, %{adapter_payload: %{conn_pid: second_pid}} = second} =
Gun.connect(channel, [])

on_exit(fn ->
Gun.disconnect(first)
Gun.disconnect(second)
end)

refute first_pid == second_pid
end
end

describe "disconnect/1" do
Expand Down
21 changes: 19 additions & 2 deletions grpc/test/grpc/client/connection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,28 @@ defmodule GRPC.Client.ConnectionTest do
target = "ipv4:127.0.0.1:#{port}"
ref = :shared_channel

assert {:ok, %Channel{ref: ^ref}} =
assert {:ok, %Channel{ref: ^ref} = channel1} =
@peer.call(peer1, Connection, :connect, [target, [name: ref]])

assert {:ok, %Channel{ref: ^ref}} =
assert {:ok, %Channel{ref: ^ref} = channel2} =
@peer.call(peer2, Connection, :connect, [target, [name: ref]])

# Each node must own its own adapter connection. Adopting another node's
# connection process leaves the channel unusable: `GRPC.Stub.call/5`
# checks `Process.alive?(conn_pid)` on every RPC, and `Process.alive?/1`
# raises ArgumentError on a remote pid.
%{adapter_payload: %{conn_pid: conn_pid1}} = channel1
%{adapter_payload: %{conn_pid: conn_pid2}} = channel2

assert node(conn_pid1) == node1
assert node(conn_pid2) == node2

point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906}

for {peer, channel} <- [{peer1, channel1}, {peer2, channel2}] do
assert {:ok, %Routeguide.Feature{}} =
@peer.call(peer, Routeguide.RouteGuide.Stub, :get_feature, [channel, point])
end
end
end

Expand Down
Loading