Skip to content

Commit a9ea94f

Browse files
committed
Add label (repo name) to ownership and connection errors
A label like MyApp.Repo can be passed via the :label option. It will be used in the process label for the DBConnection.Ownership.Manager process and will appear in error messages from both DBConnection.Ownership.Manager and DBConnection.Holder. This is helpful when applications use multiple repos (e.g., read replicas). Before: (DBConnection.OwnershipError) cannot find ownership process for #PID<...> (DBConnection.ConnectionError) connection is closed because of an error... After: (DBConnection.OwnershipError) cannot find ownership process for #PID<...> (MyApp.Repo) using mode :manual. (DBConnection.ConnectionError) MyApp.Repo connection is closed because of an error, disconnect or timeout Tests verify labels appear correctly in both error types, and that errors work correctly if no label is given. In elixir-ecto/ecto_sql#698 the adapter is updated to pass the repo name as a label.
1 parent 79143bb commit a9ea94f

File tree

4 files changed

+125
-17
lines changed

4 files changed

+125
-17
lines changed

integration_test/ownership/manager_test.exs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,59 @@ defmodule ManagerTest do
681681
assert log =~ "** (RuntimeError) oops"
682682
end
683683

684+
test "includes label in ownership error when label is provided" do
685+
{:ok, pool, opts} = start_pool(label: MyApp.Repo)
686+
687+
# Try to use the pool without checking out - should raise OwnershipError with label
688+
error =
689+
assert_raise DBConnection.OwnershipError, fn ->
690+
P.run(pool, fn _ -> :ok end, opts)
691+
end
692+
693+
# Verify the error message includes the label
694+
assert error.message =~ "cannot find ownership process"
695+
assert error.message =~ "(MyApp.Repo)"
696+
assert error.message =~ "using mode :manual"
697+
end
698+
699+
test "doesn't require a label to produce an ownership error" do
700+
{:ok, pool, opts} = start_pool()
701+
702+
# Try to use the pool without checking out - should raise OwnershipError with label
703+
error =
704+
assert_raise DBConnection.OwnershipError, fn ->
705+
P.run(pool, fn _ -> :ok end, opts)
706+
end
707+
708+
assert error.message =~ "cannot find ownership process"
709+
end
710+
711+
test "includes label in connection closed error when label is provided" do
712+
alias TestQuery, as: Q
713+
{pool, _agent} = start_pool_with_disconnect_on_execute(label: MyApp.Repo)
714+
Ownership.ownership_checkout(pool, [])
715+
716+
P.transaction(pool, fn conn ->
717+
P.execute(conn, %Q{}, [:param])
718+
error = assert_raise DBConnection.ConnectionError, fn -> P.execute!(conn, %Q{}, [:param]) end
719+
assert String.starts_with?(error.message, "MyApp.Repo connection is closed")
720+
:closed
721+
end)
722+
end
723+
724+
test "doesn't require a label to produce a connection closed error" do
725+
alias TestQuery, as: Q
726+
{pool, _agent} = start_pool_with_disconnect_on_execute()
727+
Ownership.ownership_checkout(pool, [])
728+
729+
P.transaction(pool, fn conn ->
730+
P.execute(conn, %Q{}, [:param])
731+
error = assert_raise DBConnection.ConnectionError, fn -> P.execute!(conn, %Q{}, [:param]) end
732+
assert String.starts_with?(error.message, "connection is closed")
733+
:closed
734+
end)
735+
end
736+
684737
defp start_pool(opts \\ []) do
685738
stack = [{:ok, :state}] ++ List.duplicate({:idle, :state}, 10)
686739
{:ok, agent} = A.start_link(stack)
@@ -690,6 +743,21 @@ defmodule ManagerTest do
690743
{:ok, pid, opts}
691744
end
692745

746+
# Helper to set up a pool that will disconnect during a transaction.
747+
# This is needed to trigger Holder's "connection is closed" error.
748+
defp start_pool_with_disconnect_on_execute(opts \\ []) do
749+
err = %DBConnection.ConnectionError{message: "test error"}
750+
# The stack represents TestAgent responses: initial connect, begin transaction,
751+
# disconnect on first execute, then allow reconnect.
752+
stack = [{:ok, :state}, {:ok, :began, :new_state}, {:disconnect, err, :newer_state}, :ok, {:ok, :state}]
753+
754+
{:ok, agent} = A.start_link(stack)
755+
pool_opts = [agent: agent, parent: self(), ownership_mode: :manual] ++ opts
756+
{:ok, pool} = P.start_link(pool_opts)
757+
758+
{pool, agent}
759+
end
760+
693761
defp async_no_callers(fun) do
694762
Task.async(fn ->
695763
Process.delete(:"$callers")

lib/db_connection/holder.ex

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule DBConnection.Holder do
99
@time_unit 1000
1010

1111
Record.defrecord(:conn, [:connection, :module, :state, :lock, :ts, deadline: nil, status: :ok])
12-
Record.defrecord(:pool_ref, [:pool, :reference, :deadline, :holder, :lock])
12+
Record.defrecord(:pool_ref, [:pool, :reference, :deadline, :holder, :lock, :label])
1313

1414
@type t :: :ets.tid()
1515
@type checkin_time :: non_neg_integer() | nil
@@ -131,31 +131,49 @@ defmodule DBConnection.Holder do
131131
end
132132

133133
defp handle_or_cleanup(type, pool_ref, fun, args, opts) do
134-
pool_ref(holder: holder, lock: lock) = pool_ref
134+
pool_ref(holder: holder, lock: lock, label: label) = pool_ref
135135

136136
try do
137137
:ets.lookup(holder, :conn)
138138
rescue
139139
ArgumentError ->
140-
msg = "connection is closed because of an error, disconnect or timeout"
140+
msg =
141+
maybe_prefix_label(
142+
"connection is closed because of an error, disconnect or timeout",
143+
label
144+
)
145+
141146
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
142147
else
143148
[conn(lock: conn_lock)] when conn_lock != lock ->
144-
raise "an outdated connection has been given to DBConnection on #{fun}/#{length(args) + 2}"
149+
raise maybe_prefix_label(
150+
"an outdated connection has been given to DBConnection on #{fun}/#{length(args) + 2}",
151+
label,
152+
":"
153+
)
145154

146155
[conn(status: :error)] ->
147-
msg = "connection is closed because of an error, disconnect or timeout"
156+
msg =
157+
maybe_prefix_label(
158+
"connection is closed because of an error, disconnect or timeout",
159+
label
160+
)
161+
148162
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
149163

150164
[conn(status: :aborted)] when type != :cleanup ->
151-
msg = "transaction rolling back"
165+
msg = maybe_prefix_label("transaction rolling back", label)
152166
{:disconnect, DBConnection.ConnectionError.exception(msg), _state = :unused}
153167

154168
[conn(module: module, state: state)] ->
155169
holder_apply(holder, module, fun, args ++ [opts, state])
156170
end
157171
end
158172

173+
defp maybe_prefix_label(msg, label, separator \\ "") do
174+
if label, do: "#{inspect(label)} " <> separator <> msg, else: msg
175+
end
176+
159177
## Pool state helpers API (invoked by callers)
160178

161179
@spec put_state(pool_ref :: any, term) :: :ok
@@ -196,9 +214,9 @@ defmodule DBConnection.Holder do
196214
:ok
197215
end
198216

199-
@spec handle_checkout(t, {pid, reference}, reference, checkin_time) :: boolean
200-
def handle_checkout(holder, {pid, mref}, ref, checkin_time) do
201-
:ets.give_away(holder, pid, {mref, ref, checkin_time})
217+
@spec handle_checkout(t, {pid, reference}, reference, checkin_time, label :: any) :: boolean
218+
def handle_checkout(holder, {pid, mref}, ref, checkin_time, label \\ nil) do
219+
:ets.give_away(holder, pid, {mref, ref, checkin_time, label})
202220
rescue
203221
ArgumentError ->
204222
if Process.alive?(pid) or :ets.info(holder, :owner) != self() do
@@ -290,13 +308,20 @@ defmodule DBConnection.Holder do
290308
send(pid, {:db_connection, {self(), lock}, {:checkout, callers, start, queue?}})
291309

292310
receive do
293-
{:"ETS-TRANSFER", holder, pool, {^lock, ref, checkin_time}} ->
311+
{:"ETS-TRANSFER", holder, pool, {^lock, ref, checkin_time, label}} ->
294312
Process.demonitor(lock, [:flush])
295313
{deadline, ops} = start_deadline(timeout, pool, ref, holder, start)
296314
:ets.update_element(holder, :conn, [{conn(:lock) + 1, lock} | ops])
297315

298316
pool_ref =
299-
pool_ref(pool: pool, reference: ref, deadline: deadline, holder: holder, lock: lock)
317+
pool_ref(
318+
pool: pool,
319+
reference: ref,
320+
deadline: deadline,
321+
holder: holder,
322+
lock: lock,
323+
label: label
324+
)
300325

301326
checkout_result(holder, pool_ref, checkin_time)
302327

lib/db_connection/ownership/manager.ex

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ defmodule DBConnection.Ownership.Manager do
9898
mode = Keyword.get(pool_opts, :ownership_mode, :auto)
9999
checkout_opts = Keyword.take(pool_opts, [:ownership_timeout, :queue_target, :queue_interval])
100100

101+
Util.set_label({__MODULE__, pool_opts[:label]})
102+
101103
{:ok,
102104
%{
103105
pool: pool,
@@ -107,7 +109,8 @@ defmodule DBConnection.Ownership.Manager do
107109
mode: mode,
108110
mode_ref: nil,
109111
ets: ets,
110-
log: log
112+
log: log,
113+
label: pool_opts[:label]
111114
}}
112115
end
113116

@@ -223,7 +226,7 @@ defmodule DBConnection.Ownership.Manager do
223226
{:noreply, state}
224227

225228
:not_found when mode == :manual ->
226-
not_found(from, mode)
229+
not_found(from, mode, state.label)
227230
{:noreply, state}
228231

229232
:not_found ->
@@ -253,6 +256,9 @@ defmodule DBConnection.Ownership.Manager do
253256
defp proxy_checkout(state, caller, opts) do
254257
%{pool: pool, checkouts: checkouts, owners: owners, ets: ets, log: log, mode: mode} = state
255258

259+
label = state.label
260+
opts = if label, do: Keyword.put(opts, :label, label), else: opts
261+
256262
{:ok, proxy} =
257263
DynamicSupervisor.start_child(
258264
DBConnection.Ownership.Supervisor,
@@ -405,10 +411,12 @@ defmodule DBConnection.Ownership.Manager do
405411
caller
406412
end
407413

408-
defp not_found({pid, _} = from, mode) do
414+
defp not_found({pid, _} = from, mode, label) do
409415
msg = """
410416
cannot find ownership process for #{Util.inspect_pid(pid)}
411-
using mode #{inspect(mode)}.
417+
#{if label, do: "(#{inspect(label)})"} using mode #{inspect(mode)}.
418+
(Note that a connection's mode reverts to :manual if its owner
419+
terminates.)
412420
413421
When using ownership, you must manage connections in one
414422
of the four ways:

lib/db_connection/ownership/proxy.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ defmodule DBConnection.Ownership.Proxy do
3434

3535
pre_checkin = Keyword.get(pool_opts, :pre_checkin, fn _, mod, state -> {:ok, mod, state} end)
3636
post_checkout = Keyword.get(pool_opts, :post_checkout, &{:ok, &1, &2})
37+
label = pool_opts[:label]
38+
39+
if label do
40+
Process.set_label({__MODULE__, label})
41+
end
3742

3843
state = %{
3944
client: nil,
@@ -193,8 +198,10 @@ defmodule DBConnection.Ownership.Proxy do
193198
{:reply, connection_metrics, state}
194199
end
195200

196-
defp checkout({pid, ref} = from, %{holder: holder} = state) do
197-
if Holder.handle_checkout(holder, from, ref, nil) do
201+
defp checkout({pid, ref} = from, %{holder: holder, pool_opts: pool_opts} = state) do
202+
label = pool_opts[:label]
203+
204+
if Holder.handle_checkout(holder, from, ref, nil, label) do
198205
{:noreply, %{state | client: {pid, ref, pruned_stacktrace(pid)}}}
199206
else
200207
next(state)

0 commit comments

Comments
 (0)