fix(gun): scope named connection processes to the local node - #580
Merged
sleipnir merged 1 commit intoAug 27, 2026
Merged
Conversation
kamilkowalski
force-pushed
the
fix/gun-connection-process-local-registry
branch
from
August 27, 2026 13:24
0ce84a2 to
6b963c0
Compare
`GRPC.Client.Adapters.Gun.ConnectionProcess` registered itself under
`{:global, {ConnectionProcess, {ref, host, port}}}`. `:global` is
cluster-wide, so on a connected cluster the second node to call
`GRPC.Stub.connect(addr, name: Foo)` got `{:error, {:already_started,
remote_pid}}` and adopted the *other* node's connection process as its
`conn_pid`.
`connect/2` reported success, but the channel was unusable: `GRPC.Stub.call/5`
checks `Process.alive?(conn_pid)` on every RPC, and `Process.alive?/1` raises
`ArgumentError` on a remote pid, so every call failed before a request was
sent. `GRPC.Client.Connection.channel_alive?/1` has the same problem on
DNS re-resolve, but the hot-path check fires first.
Registering globally also meant every connect paid a cluster-wide
`:global.register_name/3` — a global lock plus a multi-call to every
connected node — even for the unique `make_ref/0` names that `connect/2`
generates when no `:name` is given, which nothing else ever looks up.
`GRPC.Client.Connection` is already registered in the node-local
`GRPC.Client.Registry`; register the Gun connection process there too so
reuse is per node, matching the intent of the original change (keeping
named channels alive after the calling process exits).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kamilkowalski
force-pushed
the
fix/gun-connection-process-local-registry
branch
from
August 27, 2026 13:28
6b963c0 to
9a79352
Compare
perzanko
approved these changes
Aug 27, 2026
sleipnir
approved these changes
Aug 27, 2026
Collaborator
|
Thank you @kamilkowalski |
Contributor
Author
|
@sleipnir thanks! Do you know when this would be released? We don't want to upgrade all of our services to v1 without this fix because the bug caused an outage, but also versions <=1.0.0 have the Erlpack vulnerability... |
Collaborator
Hi @kamilkowalski https://hex.pm/packages/grpc |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
GRPC.Client.Adapters.Gun.ConnectionProcessregisters itself under{:global, {ConnectionProcess, {ref, host, port}}}(connection_process.ex:180).:globalis cluster-wide, which has two distinct consequences on a connected Erlang cluster.1. A node can adopt another node's connection, and the channel is then unusable. Two nodes calling
GRPC.Stub.connect(addr, name: :foo)collide on the same global name. The second gets{:error, {:already_started, remote_pid}}, whichconnect/2treats as successful reuse — so it returns{:ok, channel}with aconn_pidthat lives on the other node.Nothing is actually sent over that channel.
GRPC.Stub.call/5checksProcess.alive?(conn_pid)on every RPC (stub.ex:298), andProcess.alive?/1raises on a remote pid, so every call fails before a request leaves the node:GRPC.Client.Connection.channel_alive?/1(connection.ex:939) has the same problem and would raise on the next DNS re-resolve, but the hot-path check fires first. The failure mode is therefore:connect/2succeeds, then every RPC on that channel raises.2. Every connect pays a cluster-wide registration, even when no name is ever shared. Callers that don't pass
:namestill get one —connect/2fills inmake_ref/0— sochannel.refis non-nil and the process registers globally under a name nothing else will ever look up.:global.register_name/3takes a global lock and multi-calls every connected node, and:globalthen keeps that name in a table it must reconcile on every node join and leave.Production measurement
Deployed on an Elixir service whose pods are clustered via libcluster. Because that service uses unique per-connect names, it only ever hit consequence 2, never 1.
The measurement comes from an endpoint that should be very fast but opens a gRPC connection on every call, so it pays the connect penalty often — meaning it over-represents the connect path relative to steady-state RPC traffic.
Request volume flat at ~40/interval throughout. As the patched version takes over traffic, p95 goes from a spiky 100–215 ms to a flat line under ~5 ms, with p50/p75/p99 dropping together, sustained for 30+ minutes.
Traffic that reuses an established channel should see little or nothing from this change, since it doesn't touch
:globalafter the initial connect.Fix
Register in the node-local
GRPC.Client.Registry, whichGRPC.Client.Connectionalready uses:Reuse becomes per node, which matches the intent of #519 (keeping named channels alive after the calling process exits) — that change needed the process to outlive its caller, not to be shared across the cluster.
Tests
test/grpc/adapters/gun_test.exs— a named channel connected twice returns the sameconn_pidand is registered inGRPC.Client.Registry; an unnamed channel gets a fresh process each time.test/grpc/client/connection_test.exs— the existing two-node "named channels do not conflict across connected nodes" test asserted only that both connects returned{:ok, _}, which held while the two nodes shared one process. Extended to assert each node'sconn_pidis local to that node, and to issue a real RPC from each node so the unusable-channel case is covered rather than just the pid location.Both fail on master and pass with the fix. Full suite green;
mix format --check-formattedclean. CHANGELOG entry added under Unreleased.