Skip to content

Commit 3e2fc85

Browse files
committed
Ensure pool connections terminate when the pool manager shuts down
Closes elixir-ecto/postgrex#763.
1 parent 6c4e5c2 commit 3e2fc85

4 files changed

Lines changed: 65 additions & 15 deletions

File tree

‎lib/db_connection/connection_pool.ex‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ defmodule DBConnection.ConnectionPool do
1818
@queue_interval 2000
1919
@idle_interval 1000
2020
@time_unit 1000
21+
@watcher_ref {__MODULE__, :watcher_ref}
2122

2223
@doc false
2324
def start_link({mod, opts}) do
@@ -46,6 +47,7 @@ defmodule DBConnection.ConnectionPool do
4647

4748
@impl GenServer
4849
def init({mod, opts}) do
50+
Process.flag(:trap_exit, true)
4951
DBConnection.register_as_pool(mod)
5052

5153
queue = :ets.new(__MODULE__.Queue, [:protected, :ordered_set, decentralized_counters: true])
@@ -64,7 +66,8 @@ defmodule DBConnection.ConnectionPool do
6466
end
6567

6668
ts = {nil, max_lifetime}
67-
{:ok, _} = DBConnection.ConnectionPool.Pool.start_supervised(queue, mod, opts)
69+
{:ok, watcher_ref} = DBConnection.ConnectionPool.Pool.start_supervised(queue, mod, opts)
70+
Process.put(@watcher_ref, watcher_ref)
6871
target = Keyword.get(opts, :queue_target, @queue_target)
6972
interval = Keyword.get(opts, :queue_interval, @queue_interval)
7073
idle_interval = Keyword.get(opts, :idle_interval, @idle_interval)
@@ -229,6 +232,19 @@ defmodule DBConnection.ConnectionPool do
229232
drop_idle(past_in_native, limit, status, queue, codel, ts)
230233
end
231234

235+
def handle_info({:EXIT, _pid, reason}, state) do
236+
{:stop, reason, state}
237+
end
238+
239+
@impl GenServer
240+
def terminate(_reason, _state) do
241+
if watcher_ref = Process.get(@watcher_ref) do
242+
DBConnection.ConnectionPool.Pool.stop_supervised(watcher_ref)
243+
end
244+
245+
:ok
246+
end
247+
232248
defp drop_idle(past_in_native, limit, status, queue, codel, ts) do
233249
with true <- status == :ready and limit > 0,
234250
{queued_in_native, holder} = key when queued_in_native <= past_in_native <-

‎lib/db_connection/connection_pool/pool.ex‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ defmodule DBConnection.ConnectionPool.Pool do
99
)
1010
end
1111

12+
def stop_supervised(ref) do
13+
case DBConnection.Watcher.unwatch(ref) do
14+
pid when is_pid(pid) -> wait_until_down(pid)
15+
nil -> :ok
16+
end
17+
end
18+
1219
def start_link(arg) do
1320
Supervisor.start_link(__MODULE__, arg)
1421
end
@@ -30,4 +37,12 @@ defmodule DBConnection.ConnectionPool.Pool do
3037
child_opts = [id: {mod, owner, id}] ++ Keyword.take(opts, [:shutdown])
3138
DBConnection.Connection.child_spec(mod, [pool_index: id] ++ opts, owner, tag, child_opts)
3239
end
40+
41+
defp wait_until_down(pid) do
42+
ref = Process.monitor(pid)
43+
44+
receive do
45+
{:DOWN, ^ref, _, _, _} -> :ok
46+
end
47+
end
3348
end

‎lib/db_connection/watcher.ex‎

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ defmodule DBConnection.Watcher do
1212
GenServer.call(@name, {:watch, supervisor, args}, :infinity)
1313
end
1414

15+
def unwatch(ref) do
16+
GenServer.call(@name, {:unwatch, ref}, :infinity)
17+
end
18+
1519
@impl true
1620
def init(:ok) do
1721
Process.flag(:trap_exit, true)
@@ -27,33 +31,39 @@ defmodule DBConnection.Watcher do
2731
started_ref = Process.monitor(started_pid)
2832
caller_refs = Map.put(caller_refs, caller_ref, {supervisor, started_pid, started_ref})
2933
started_refs = Map.put(started_refs, started_ref, {caller_pid, caller_ref})
30-
{:reply, {:ok, started_pid}, {caller_refs, started_refs}}
34+
{:reply, {:ok, caller_ref}, {caller_refs, started_refs}}
3135

3236
other ->
3337
{:reply, other, {caller_refs, started_refs}}
3438
end
3539
end
3640

41+
@impl true
42+
def handle_call({:unwatch, ref}, _from, {caller_refs, started_refs}) do
43+
case Map.pop(caller_refs, ref) do
44+
{{_supervisor, started_pid, started_ref}, caller_refs} ->
45+
terminate_started(started_pid)
46+
Process.demonitor(ref, [:flush])
47+
Process.demonitor(started_ref, [:flush])
48+
{:reply, started_pid, {caller_refs, Map.delete(started_refs, started_ref)}}
49+
50+
{nil, caller_refs} ->
51+
{:reply, nil, {caller_refs, started_refs}}
52+
end
53+
end
54+
3755
@impl true
3856
def handle_info({:DOWN, ref, _, _, _}, {caller_refs, started_refs}) do
3957
case caller_refs do
4058
%{^ref => {_supervisor, started_pid, started_ref}} ->
41-
try do
42-
:sys.terminate(started_pid, :shutdown, :infinity)
43-
catch
44-
:exit, {:noproc, {:sys, :terminate, _}} -> :ok
45-
:exit, {:shutdown, {:sys, :terminate, _}} -> :ok
46-
end
47-
59+
terminate_started(started_pid)
60+
Process.demonitor(started_ref, [:flush])
4861
caller_refs = Map.delete(caller_refs, ref)
49-
started_refs = Map.put(started_refs, started_ref, {nil, nil})
62+
started_refs = Map.delete(started_refs, started_ref)
5063
{:noreply, {caller_refs, started_refs}}
5164

5265
%{} ->
5366
case started_refs do
54-
%{^ref => {nil, nil}} ->
55-
{:noreply, {caller_refs, Map.delete(started_refs, ref)}}
56-
5767
%{^ref => {caller_pid, caller_ref}} ->
5868
Process.demonitor(caller_ref, [:flush])
5969
Process.exit(caller_pid, :kill)
@@ -66,9 +76,18 @@ defmodule DBConnection.Watcher do
6676
{:noreply, state}
6777
end
6878

79+
defp terminate_started(started_pid) do
80+
try do
81+
:sys.terminate(started_pid, :shutdown, :infinity)
82+
catch
83+
:exit, {:noproc, {:sys, :terminate, _}} -> :ok
84+
:exit, {:shutdown, {:sys, :terminate, _}} -> :ok
85+
end
86+
end
87+
6988
@impl true
7089
def terminate(_, {_, started_refs}) do
71-
for {_, {caller_pid, _}} when caller_pid != nil <- started_refs do
90+
for {_, {caller_pid, _}} <- started_refs do
7291
Process.exit(caller_pid, :kill)
7392
end
7493

‎test/db_connection/watcher_test.exs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ defmodule DBConnection.WatcherTest do
4747
# Trigger pool termination
4848
stop_supervised!(:pool1)
4949

50-
{:ok, agent2} = A.start_link([{:ok, :state}])
50+
{:ok, agent2} = A.start_link([{:ok, :state}, {:ok, :state}])
5151

5252
task =
5353
Task.async(fn ->

0 commit comments

Comments
 (0)