Skip to content

Commit a86b8eb

Browse files
authored
Fix owner_unallow using wrong ref when switching owners (#342)
When ownership_allow is called with unallow_existing: true to move a process from one owner to another, owner_unallow was removing the process from the caller's owner ref rather than the process's current owner ref. This left the process in the old owner's allowed list. When the old owner later exits, owner_down removes all processes in its allowed list from checkouts — including the process that was already re-allowed on a different owner. This causes an OwnershipError on the next query because the process is no longer in checkouts. The fix looks up the process's current checkout entry to find its actual ref and proxy, then cleans up the correct owner's allowed list. Also fixes the ETS delete in owner_unallow which was passing {pid, proxy} as the key instead of just pid — a no-op on a :set table keyed by the first element.
1 parent 9512c28 commit a86b8eb

2 files changed

Lines changed: 81 additions & 19 deletions

File tree

integration_test/ownership/manager_test.exs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,62 @@ defmodule ManagerTest do
697697
end
698698
end
699699

700+
test "unallow_existing cleans up old owner so its exit doesn't revoke the new allowance" do
701+
stack = List.duplicate({:ok, :state}, 2) ++ List.duplicate({:idle, :state}, 20)
702+
{:ok, agent} = A.start_link(stack)
703+
opts = [agent: agent, parent: self(), ownership_mode: :manual, pool_size: 2]
704+
{:ok, pool} = P.start_link(opts)
705+
parent = self()
706+
707+
# A long-lived worker process that gets re-allowed across owners.
708+
worker =
709+
spawn(fn ->
710+
receive do
711+
:done -> :ok
712+
end
713+
end)
714+
715+
# owner_a checks out and allows the worker
716+
owner_a =
717+
async_no_callers(fn ->
718+
:ok = Ownership.ownership_checkout(pool, [])
719+
:ok = Ownership.ownership_allow(pool, self(), worker, [])
720+
send(parent, {:owner_ready, :a})
721+
assert_receive :exit
722+
end)
723+
724+
assert_receive {:owner_ready, :a}
725+
726+
# owner_b checks out and re-allows the worker via unallow_existing
727+
owner_b =
728+
async_no_callers(fn ->
729+
:ok = Ownership.ownership_checkout(pool, [])
730+
:ok = Ownership.ownership_allow(pool, self(), worker, unallow_existing: true)
731+
send(parent, {:owner_ready, :b})
732+
assert_receive :exit
733+
end)
734+
735+
assert_receive {:owner_ready, :b}
736+
737+
# Verify worker has access via owner_b
738+
assert P.run(pool, fn _ -> :ok end, [caller: worker] ++ opts)
739+
740+
# Kill owner_a. The worker was moved to owner_b via unallow_existing,
741+
# so owner_a's exit must not revoke the worker's access.
742+
send(owner_a.pid, :exit)
743+
ref = Process.monitor(owner_a.pid)
744+
assert_receive {:DOWN, ^ref, _, _, _}
745+
746+
# Let the pool manager process the :DOWN
747+
Process.sleep(50)
748+
749+
# Worker should still have access through owner_b
750+
assert P.run(pool, fn _ -> :ok end, [caller: worker] ++ opts)
751+
752+
send(owner_b.pid, :exit)
753+
send(worker, :done)
754+
end
755+
700756
test "doesn't require a label to produce an ownership error" do
701757
{:ok, pool, opts} = start_pool()
702758

lib/db_connection/ownership/manager.ex

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -331,28 +331,34 @@ defmodule DBConnection.Ownership.Manager do
331331
state
332332
end
333333

334-
defp owner_unallow(%{ets: ets, log: log} = state, caller, unallow, ref, proxy) do
335-
if log do
336-
Logger.log(log, fn ->
337-
[
338-
Util.inspect_pid(unallow),
339-
" was unallowed by ",
340-
Util.inspect_pid(caller),
341-
" on proxy ",
342-
Util.inspect_pid(proxy)
343-
]
344-
end)
345-
end
334+
defp owner_unallow(%{ets: ets, log: log} = state, caller, unallow, _ref, _proxy) do
335+
case Map.get(state.checkouts, unallow, :not_found) do
336+
{_status, old_ref, old_proxy} ->
337+
if log do
338+
Logger.log(log, fn ->
339+
[
340+
Util.inspect_pid(unallow),
341+
" was unallowed by ",
342+
Util.inspect_pid(caller),
343+
" on proxy ",
344+
Util.inspect_pid(old_proxy)
345+
]
346+
end)
347+
end
346348

347-
state = update_in(state.checkouts, &Map.delete(&1, unallow))
349+
state = update_in(state.checkouts, &Map.delete(&1, unallow))
348350

349-
state =
350-
update_in(state.owners[ref], fn {proxy, caller, allowed} ->
351-
{proxy, caller, List.delete(allowed, unallow)}
352-
end)
351+
state =
352+
update_in(state.owners[old_ref], fn {proxy, caller, allowed} ->
353+
{proxy, caller, List.delete(allowed, unallow)}
354+
end)
353355

354-
ets && :ets.delete(ets, {unallow, proxy})
355-
state
356+
ets && :ets.delete(ets, unallow)
357+
state
358+
359+
:not_found ->
360+
state
361+
end
356362
end
357363

358364
defp owner_down(%{ets: ets, log: log} = state, ref) do

0 commit comments

Comments
 (0)