Skip to content

Commit 828debd

Browse files
committed
use a per-stack request pool for the sandbox client
so all requests are killed when the stack exits
1 parent 11c5df2 commit 828debd

File tree

3 files changed

+72
-1
lines changed

3 files changed

+72
-1
lines changed

lib/phoenix/sync/sandbox.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ if Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) do
266266
{:ok, client} =
267267
Phoenix.Sync.Electric.client(:test, Keyword.put(api_config, :mode, :embedded))
268268

269+
client = Map.put(client, :pool, {Phoenix.Sync.Sandbox.Fetch, stack_id: stack_id})
270+
269271
# we link the sandbox to the current (test) process not the connection
270272
# owner because that's the ownership route that works. The owner
271273
# is a convenience to link the repo connection to a process who's lifetime

lib/phoenix/sync/sandbox/fetch.ex

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
defmodule Phoenix.Sync.Sandbox.Fetch do
2+
@moduledoc false
3+
4+
alias Electric.Client
5+
alias Electric.Client.Fetch
6+
7+
require Logger
8+
9+
@callback request(Client.t(), Fetch.Request.t(), opts :: Keyword.t()) ::
10+
Fetch.Response.t() | {:error, Fetch.Response.t() | term()}
11+
12+
@behaviour Electric.Client.Fetch.Pool
13+
14+
def name(stack_id) do
15+
Phoenix.Sync.Sandbox.name({__MODULE__, stack_id})
16+
end
17+
18+
@impl Electric.Client.Fetch.Pool
19+
def request(%Client{} = client, %Fetch.Request{} = request, opts) do
20+
{:ok, stack_id} = Keyword.fetch(opts, :stack_id)
21+
22+
request_id = request_id(client, request, stack_id)
23+
24+
# The monitor process is unique to the request and launches the actual
25+
# request as a linked process.
26+
#
27+
# This coalesces requests, so no matter how many simultaneous
28+
# clients we have, we only ever make one request to the backend.
29+
{:ok, monitor_pid} = start_monitor(stack_id, request_id, request, client)
30+
31+
try do
32+
ref = Fetch.Monitor.register(monitor_pid, self())
33+
34+
Fetch.Monitor.wait(ref)
35+
catch
36+
:exit, {reason, _} ->
37+
Logger.debug(fn ->
38+
"Request process ended with reason #{inspect(reason)} before we could register. Re-attempting."
39+
end)
40+
41+
request(client, request, opts)
42+
end
43+
end
44+
45+
defp start_monitor(stack_id, request_id, request, client) do
46+
DynamicSupervisor.start_child(
47+
name(stack_id),
48+
{Electric.Client.Fetch.Monitor, {request_id, request, client}}
49+
)
50+
|> return_existing()
51+
end
52+
53+
defp return_existing({:ok, pid}), do: {:ok, pid}
54+
defp return_existing({:error, {:already_started, pid}}), do: {:ok, pid}
55+
defp return_existing(error), do: error
56+
57+
defp request_id(%Client{fetch: {fetch_impl, _}}, %Fetch.Request{} = request, stack_id) do
58+
{
59+
fetch_impl,
60+
stack_id,
61+
URI.to_string(request.endpoint),
62+
request.headers,
63+
Fetch.Request.params(request)
64+
}
65+
end
66+
end
67+

lib/phoenix/sync/sandbox/stack.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ if Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) do
140140
restart: :temporary
141141
),
142142
{Sandbox.Inspector, stack_id: stack_id, repo: repo},
143-
{Sandbox.Producer, stack_id: stack_id}
143+
{Sandbox.Producer, stack_id: stack_id},
144+
{DynamicSupervisor,
145+
name: Phoenix.Sync.Sandbox.Fetch.name(stack_id), strategy: :one_for_one}
144146
]
145147

146148
Supervisor.init(children, strategy: :one_for_one)

0 commit comments

Comments
 (0)