Skip to content

fix: stop leaking client supervisor children on failed and closed connections#554

Open
mvanhorn wants to merge 2 commits into
elixir-grpc:masterfrom
mvanhorn:fix/531-grpc-client-supervisor-memory-leak
Open

fix: stop leaking client supervisor children on failed and closed connections#554
mvanhorn wants to merge 2 commits into
elixir-grpc:masterfrom
mvanhorn:fix/531-grpc-client-supervisor-memory-leak

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Summary

Short-lived client connections no longer leak GRPC.Client.Supervisor children. Orchestrator processes that fail to finalize a connection are terminated instead of being left under the DynamicSupervisor, and disconnected connections are fully cleaned up, so the supervisor's binary memory stays flat under connect/disconnect churn.

Why this matters

Reported in #531: a user running hundreds of short-lived connections (GRPC.Stub.connect/2 then GRPC.Stub.disconnect/1) on 1.0.0-rc.1 sees GRPC.Client.Supervisor top :recon.proc_count(:memory, 10) and :recon.bin_leak(10) with persistent growth; downgrading to 0.10.2 (no DynamicSupervisor in the client path) eliminates it, and a collaborator confirmed the diagnosis and labeled it a bug. The regression is in the 0.11+/1.0 client connection architecture: GRPC.Client.Connection.connect/2 leaves the orchestrator child running when finalize_connection/2 returns {:error, :no_connection}, and the disconnect path leaves per-connection state behind.

Changes

  • connect/2 now terminates the orchestrator child when connection finalization fails, so error paths cannot accumulate supervised processes.
  • The disconnect path releases the connection's supervised resources instead of leaving the child alive under GRPC.Client.Supervisor.

Testing

Added regression tests that drive repeated connect/disconnect cycles and connect-failure cycles, asserting DynamicSupervisor.count_children/1 on GRPC.Client.Supervisor returns to baseline after each cycle. Ran the client connection test files with mix test.

Fixes #531

@sleipnir

Copy link
Copy Markdown
Collaborator

@mvanhorn Some tests are failing, could you please check them?

@mvanhorn

Copy link
Copy Markdown
Contributor Author

The OTP-matrix failures were the connection tests asserting active == 0 on the shared GRPC.Client.Supervisor — under the full suite other tests leave children behind (CI showed active: 44), so the assertions failed even though the lifecycle fix works. 5a6f752 captures the supervisor's pre-existing child count in setup and asserts the count returns to that baseline instead. All 14 connection tests pass locally.

@polvalente

Copy link
Copy Markdown
Contributor

@mvanhorn could you resolve the merge conflicts?

mvanhorn added 2 commits July 24, 2026 21:38
A connection that reports ready but cannot hand out a channel stays under
GRPC.Client.Supervisor forever: connect/2 returns {:error, :no_connection}
to the caller and nothing terminates the child. Terminate it on the
finalize-failure path, mirroring the existing terminate_child on the
ready_status error path.

Also give GRPC.Client.Supervisor hibernate_after and a fullsweep_after
spawn_opt so long-lived connection processes release binary-heavy heaps
instead of accumulating them.

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Adds a resolver that establishes and then drops its LB table so connect/2
takes the finalize-failure path, and asserts the child is gone afterwards.

Supervisor assertions capture the pre-existing child count in setup rather
than asserting zero on the shared GRPC.Client.Supervisor, which fails under
the full suite where other tests leave children behind.

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@mvanhorn
mvanhorn force-pushed the fix/531-grpc-client-supervisor-memory-leak branch from 5a6f752 to e4fdaae Compare July 25, 2026 04:41
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Rebased onto master — conflicts resolved, mix test is green (366 passed, 2 skipped) and mix format --check-formatted is clean.

Worth flagging what the rebase surfaced: #557 already added terminate_child on the ready_status error path, so half of this PR landed independently. What's left is the finalize_connection failure path — a connection that reports ready but can't hand out a channel still returns {:error, :no_connection} to the caller while staying under GRPC.Client.Supervisor. That's the leak this now fixes, in the shape connect/2 has after #557.

I also dropped my restart: :temporary change and the test that asserted a crashed connection is not restarted. #557's supervised connections deliberately restart on crash (and terminate/2 documents relying on it), so :transient stays as you have it.

Remaining diff is the finalize-path cleanup plus a hibernate_after / fullsweep_after spawn_opt on the client supervisor so long-lived connection processes release binary-heavy heaps.

Comment on lines +9 to 14
[
name: GRPC.Client.Supervisor,
hibernate_after: 15_000,
spawn_opt: [fullsweep_after: 20]
]}
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if these should be configurable via Application env

# A connection that reported ready but cannot hand out a
# channel is unusable; leaving it under the supervisor is the
# leak this fixes.
_ = DynamicSupervisor.terminate_child(GRPC.Client.Supervisor, pid)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we hard-match on the result?

end
end

defp assert_eventually(fun, 0), do: assert(fun.())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not add asssert_eventually in favor of more robust strategies. I believe waiting for the DOWN message should be enough with other assertions happening after that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GRPC.Client.Supervisor persistent memory growth

3 participants