From 4df07b2b2f13306e65058804358eec6f556498ea Mon Sep 17 00:00:00 2001 From: Burmaja Milan Date: Tue, 16 Jun 2026 17:40:29 +0100 Subject: [PATCH] Add configurable conn_max_idle_time to Finch pool Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 +++ lib/graph_conn/supervisor.ex | 16 +++++- mix.exs | 2 +- test/graph_conn/supervisor_test.exs | 86 +++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 test/graph_conn/supervisor_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5f122..7574033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 1.9.12 + +## Enhancement + +- Add configurable `conn_max_idle_time` Finch pool option. Set + `config :graph_conn, conn_max_idle_time: ms` to proactively drop idle + HTTP connections before the upstream gateway closes them. Defaults to + `nil` (Finch default: `:infinity`, preserving existing behaviour). + # 1.9.11 ## Update diff --git a/lib/graph_conn/supervisor.ex b/lib/graph_conn/supervisor.ex index a1a81fe..11c97ed 100644 --- a/lib/graph_conn/supervisor.ex +++ b/lib/graph_conn/supervisor.ex @@ -35,7 +35,7 @@ defmodule GraphConn.Supervisor do {Finch, name: Module.concat(base_name, Finch), pools: %{ - default: [{:size, 50}, {:count, 1} | _conn_opts()] + default: _pool_opts() }}, {GraphConn.ConnectionManager, [base_name, config]} ] @@ -44,6 +44,17 @@ defmodule GraphConn.Supervisor do defp _name(base_name), do: Module.concat(base_name, Supervisor) + defp _pool_opts do + base = [{:size, 50}, {:count, 1} | _conn_opts()] + + :graph_conn + |> Application.get_env(:conn_max_idle_time) + |> case do + nil -> base + timeout -> [{:conn_max_idle_time, timeout} | base] + end + end + defp _conn_opts do insecure? = Application.get_env(:graph_conn, :insecure) == true proxy = Application.get_env(:graph_conn, :proxy, false) @@ -51,7 +62,8 @@ defmodule GraphConn.Supervisor do if ca_cert_file = Application.get_env(:graph_conn, :ca_cert), do: :public_key.cacerts_load(ca_cert_file) - case {insecure?, proxy} do + {insecure?, proxy} + |> case do {true, false} -> transport_opts = [verify: :verify_none] [conn_opts: [transport_opts: transport_opts]] diff --git a/mix.exs b/mix.exs index a6c7a0a..074d1eb 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule GraphConn.MixProject do def project do [ app: :graph_conn, - version: "1.9.9", + version: "1.9.12", elixir: "~> 1.17", start_permanent: true, test_coverage: [tool: ExCoveralls], diff --git a/test/graph_conn/supervisor_test.exs b/test/graph_conn/supervisor_test.exs new file mode 100644 index 0000000..59f8cdf --- /dev/null +++ b/test/graph_conn/supervisor_test.exs @@ -0,0 +1,86 @@ +defmodule GraphConn.SupervisorTest do + use ExUnit.Case, async: false + + alias GraphConn.{Request, TestConn} + + describe "Finch conn_max_idle_time" do + setup do + original = Application.get_env(:graph_conn, :conn_max_idle_time) + test_pid = self() + + "supervisor-test-idle-#{inspect(self())}" + |> :telemetry.attach( + [:finch, :conn_max_idle_time_exceeded], + fn _, _, _, _ -> send(test_pid, :idle_exceeded) end, + nil + ) + + fn -> + :telemetry.detach("supervisor-test-idle-#{inspect(test_pid)}") + + original + |> case do + nil -> Application.delete_env(:graph_conn, :conn_max_idle_time) + val -> Application.put_env(:graph_conn, :conn_max_idle_time, val) + end + + TestConn + |> Module.concat(Supervisor) + |> Process.whereis() + |> case do + nil -> :ok + pid -> Process.exit(pid, :test_cleanup) + end + end + |> on_exit() + + :ok + end + + test "idle connection is discarded when conn_max_idle_time is configured" do + Application.put_env(:graph_conn, :conn_max_idle_time, 50) + + config = Application.get_env(:graph_conn, TestConn) + assert {:ok, _pid} = _start_connection(config) + assert_receive {:conn_status_changed, :ready} + + # Wait longer than the configured idle time so the connection ages + Process.sleep(100) + + # Trigger a checkout — Finch discards the stale connection and emits the event + assert {:ok, _} = TestConn.execute(:action, %Request{path: "capabilities"}) + + assert_receive :idle_exceeded, 500 + assert :ok = TestConn.stop() + end + + test "idle connection is kept when conn_max_idle_time is not configured" do + Application.delete_env(:graph_conn, :conn_max_idle_time) + + config = Application.get_env(:graph_conn, TestConn) + assert {:ok, _pid} = _start_connection(config) + assert_receive {:conn_status_changed, :ready} + + Process.sleep(100) + + assert {:ok, _} = TestConn.execute(:action, %Request{path: "capabilities"}) + + refute_receive :idle_exceeded, 200 + assert :ok = TestConn.stop() + end + end + + defp _start_connection(config) do + config + |> TestConn.start_supervisor(%{forward_to: self()}) + |> case do + {:ok, pid} -> + assert Process.alive?(pid) + {:ok, pid} + + {:error, {:already_started, pid}} -> + Process.exit(pid, :we_need_new_connection) + _start_connection(config) + end + end +end