From fd0672fcffd519b306810e0562b96b9aabd11624 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 19 Jul 2026 12:57:49 +0200 Subject: [PATCH] Reject checkouts after deadline --- integration_test/cases/queue_test.exs | 23 +++++++++++++++ integration_test/ownership/owner_test.exs | 2 +- lib/db_connection/holder.ex | 36 ++++++++++++++++------- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/integration_test/cases/queue_test.exs b/integration_test/cases/queue_test.exs index 3e79b4f3..688c0b99 100644 --- a/integration_test/cases/queue_test.exs +++ b/integration_test/cases/queue_test.exs @@ -86,6 +86,29 @@ defmodule QueueTest do assert P.run(pool, fn _ -> :result end) == :result end + test "does not checkout a connection after its deadline" do + stack = [ + {:ok, :state}, + :ok, + fn opts -> + send(opts[:parent], :reconnected) + {:ok, :state} + end + ] + + {:ok, agent} = A.start_link(stack) + + opts = [agent: agent, parent: self()] + {:ok, pool} = P.start_link(opts) + deadline = System.monotonic_time(:millisecond) - 1 + + assert_raise DBConnection.ConnectionError, ~r/deadline reached/, fn -> + P.run(pool, fn _ -> flunk("checkout succeeded") end, deadline: deadline) + end + + assert_receive :reconnected + end + test "queue many async exits" do stack = [{:ok, :state}] ++ List.duplicate({:idle, :state}, 20) {:ok, agent} = A.start_link(stack) diff --git a/integration_test/ownership/owner_test.exs b/integration_test/ownership/owner_test.exs index 69bc0d6f..f0b86d79 100644 --- a/integration_test/ownership/owner_test.exs +++ b/integration_test/ownership/owner_test.exs @@ -132,7 +132,7 @@ defmodule OwnerTest do fn _ -> assert_receive :reconnected end, - timeout: 0 + timeout: 10 ) end) =~ ~r"timed out" diff --git a/lib/db_connection/holder.ex b/lib/db_connection/holder.ex index 3b9916d8..0d69a51d 100644 --- a/lib/db_connection/holder.ex +++ b/lib/db_connection/holder.ex @@ -321,7 +321,7 @@ defmodule DBConnection.Holder do pool_ref = pool_ref(pool: pool, reference: ref, deadline: deadline, holder: holder, lock: lock) - checkout_result(holder, pool_ref, checkin_time) + checkout_result(holder, pool_ref, checkin_time, timeout) {^lock, reply} -> Process.demonitor(lock, [:flush]) @@ -332,20 +332,34 @@ defmodule DBConnection.Holder do end end - defp checkout_result(holder, pool_ref, checkin_time) do - try do - :ets.lookup(holder, :conn) - rescue - ArgumentError -> - # Deadline could hit and be handled pool before using connection - msg = "connection not available because deadline reached while in queue" - {:error, DBConnection.ConnectionError.exception(msg)} + defp checkout_result(holder, pool_ref, checkin_time, timeout) do + if deadline_reached?(timeout) do + err = checkout_deadline_error() + disconnect(pool_ref, err) + {:error, err} else - [conn(module: mod, state: state)] -> - {:ok, pool_ref, mod, checkin_time, state} + try do + :ets.lookup(holder, :conn) + rescue + ArgumentError -> + # Deadline could hit and be handled by the pool before using the connection. + {:error, checkout_deadline_error()} + else + [conn(module: mod, state: state)] -> + {:ok, pool_ref, mod, checkin_time, state} + end end end + defp checkout_deadline_error do + DBConnection.ConnectionError.exception( + "connection not available because deadline reached while in queue" + ) + end + + defp deadline_reached?(nil), do: false + defp deadline_reached?(timeout), do: timeout <= System.monotonic_time(@time_unit) + defp no_holder(holder, maybe_pid) do reason = case :ets.info(holder, :owner) do