From 45c34d984fbaf9a28388ba442282e340a326d260 Mon Sep 17 00:00:00 2001 From: Som Samantray <92726151+SomSamantray@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:24:53 +0530 Subject: [PATCH] fix(mint): prevent duplicate reconnect retries --- .../connection_process/connection_process.ex | 7 ++++- .../adapters/mint/connection_process_test.exs | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex index 0e865d65..456d9543 100644 --- a/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex +++ b/grpc/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -445,7 +445,12 @@ if Code.ensure_loaded?(Mint.HTTP) do clean_state = State.update_request_stream_queue(%{new_state | requests: %{}}, :queue.new()) if clean_state.retry > 0 do - attempt_reconnect(clean_state) + # A failed attempt already scheduled the next retry; ignore duplicate close events. + if clean_state.retry_attempt > 0 do + {:noreply, clean_state} + else + attempt_reconnect(clean_state) + end else send(clean_state.parent, {:elixir_grpc, :connection_down, self()}) {:stop, :normal, clean_state} diff --git a/grpc/test/grpc/adapters/mint/connection_process_test.exs b/grpc/test/grpc/adapters/mint/connection_process_test.exs index d097d561..8ddc1232 100644 --- a/grpc/test/grpc/adapters/mint/connection_process_test.exs +++ b/grpc/test/grpc/adapters/mint/connection_process_test.exs @@ -439,6 +439,35 @@ defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do end end + describe "handle_info - duplicate connection close notifications" do + @describetag private_endpoint: ReconnectUnavailableEndpoint + setup :start_private_endpoint + setup :valid_connection_with_retry + + test "does not start another retry chain after GOAWAY and tcp_closed", %{ + process_pid: pid + } do + wait_until_open(pid) + state = :sys.get_state(pid) + :ok = GRPC.Server.stop_endpoint(ReconnectUnavailableEndpoint) + + goaway_frame = <<8::24, 0x07, 0, 0::32, 0::32, 0::32>> + goaway = {:tcp, state.conn.socket, goaway_frame} + closed = {:tcp_closed, state.conn.socket} + unavailable_state = %{state | retry_timeout_ms: 60_000} + + assert {:noreply, retrying_state} = + ConnectionProcess.handle_info(goaway, unavailable_state) + + assert retrying_state.retry_attempt == 1 + + assert {:noreply, still_retrying_state} = + ConnectionProcess.handle_info(closed, retrying_state) + + assert still_retrying_state.retry_attempt == 1 + end + end + describe "handle_info - connection_closed - with retry - exhausted" do @describetag private_endpoint: ReconnectExhaustedEndpoint setup :start_private_endpoint