Skip to content

Commit 449bb8b

Browse files
authored
Refactor: Split CallbackTracingServer to separate gen servers (#323)
* Split gen server * Add tests for EtsTableServer * Make EtsTableServer mockable * Add more tests
1 parent c661aff commit 449bb8b

File tree

6 files changed

+251
-75
lines changed

6 files changed

+251
-75
lines changed

lib/live_debugger.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ defmodule LiveDebugger do
3434
else
3535
children ++
3636
[
37-
{LiveDebugger.GenServers.CallbackTracingServer, []}
37+
{LiveDebugger.GenServers.CallbackTracingServer, []},
38+
{LiveDebugger.GenServers.EtsTableServer, []}
3839
]
3940
end
4041

lib/live_debugger/gen_servers/callback_tracing_server.ex

Lines changed: 3 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule LiveDebugger.GenServers.CallbackTracingServer do
22
@moduledoc """
3-
This gen_server is responsible for tracing callbacks and managing ETS tables.
3+
This gen_server is responsible for tracing callbacks.
44
"""
55

66
use GenServer
@@ -15,30 +15,10 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
1515
alias LiveDebugger.Utils.Callbacks, as: CallbackUtils
1616
alias LiveDebugger.Utils.PubSub, as: PubSubUtils
1717

18-
@ets_table_name :lvdbg_traces
1918
@callback_functions CallbackUtils.callbacks_functions()
2019

21-
@type table_refs() :: %{pid() => :ets.table()}
22-
2320
## API
2421

25-
@doc """
26-
Returns ETS table reference.
27-
It creates table if none is associated with given pid
28-
"""
29-
@spec table!(pid :: pid()) :: :ets.table()
30-
def table!(pid) when is_pid(pid) do
31-
GenServer.call(__MODULE__, {:get_or_create_table, pid}, 1000)
32-
end
33-
34-
@doc """
35-
If table for given `pid` exists it deletes it from ETS.
36-
"""
37-
@spec delete_table!(pid :: pid()) :: :ok
38-
def delete_table!(pid) when is_pid(pid) do
39-
GenServer.call(__MODULE__, {:delete_table, pid}, 1000)
40-
end
41-
4222
@doc """
4323
Checks if GenServer has been loaded
4424
"""
@@ -63,7 +43,7 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
6343
end
6444

6545
@impl true
66-
def handle_info(:setup_tracing, table_refs) do
46+
def handle_info(:setup_tracing, state) do
6747
Dbg.tracer(:process, {&handle_trace/2, 0})
6848
Dbg.p(:all, :c)
6949

@@ -84,61 +64,14 @@ defmodule LiveDebugger.GenServers.CallbackTracingServer do
8464
# We trace it to refresh the components tree
8565
Dbg.tp({Phoenix.LiveView.Diff, :delete_component, 2}, [])
8666

87-
{:noreply, table_refs}
88-
end
89-
90-
@impl true
91-
def handle_info({:DOWN, _, :process, closed_pid, _}, table_refs) do
92-
{_, table_refs} = delete_ets_table(closed_pid, table_refs)
93-
94-
closed_pid
95-
|> PubSubUtils.process_status_topic()
96-
|> PubSubUtils.broadcast({:process_status, :dead})
97-
98-
{:noreply, table_refs}
99-
end
100-
101-
@impl true
102-
def handle_call({:get_or_create_table, pid}, _from, table_refs) do
103-
case Map.get(table_refs, pid) do
104-
nil ->
105-
ref = create_ets_table()
106-
Process.monitor(pid)
107-
{:reply, ref, Map.put(table_refs, pid, ref)}
108-
109-
ref ->
110-
{:reply, ref, table_refs}
111-
end
112-
end
113-
114-
@impl true
115-
def handle_call({:delete_table, pid}, _from, table_refs) do
116-
{_, table_refs} = delete_ets_table(pid, table_refs)
117-
{:reply, :ok, table_refs}
67+
{:noreply, state}
11868
end
11969

12070
@impl true
12171
def handle_call(:ping, _from, state) do
12272
{:reply, :ok, state}
12373
end
12474

125-
@spec create_ets_table() :: :ets.table()
126-
defp create_ets_table() do
127-
:ets.new(@ets_table_name, [:ordered_set, :public])
128-
end
129-
130-
@spec delete_ets_table(pid(), table_refs()) :: {boolean(), table_refs()}
131-
defp delete_ets_table(pid, table_refs) do
132-
case Map.pop(table_refs, pid) do
133-
{nil, table_refs} ->
134-
{false, table_refs}
135-
136-
{ref, updated_table_refs} ->
137-
:ets.delete(ref)
138-
{true, updated_table_refs}
139-
end
140-
end
141-
14275
# This handler is heavy because of fetching state and we do not care for order because it is not displayed to user
14376
# Because of that we do it asynchronously to speed up tracer a bit
14477
# We do not persist this trace because it is not displayed to user
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
defmodule LiveDebugger.GenServers.EtsTableServer do
2+
@moduledoc """
3+
This gen_server is responsible for managing ETS tables.
4+
"""
5+
6+
use GenServer
7+
8+
alias LiveDebugger.Utils.PubSub, as: PubSubUtils
9+
10+
@ets_table_name :lvdbg_traces
11+
12+
@type table_refs() :: %{pid() => :ets.table()}
13+
14+
## API
15+
16+
@callback table!(pid :: pid()) :: :ets.table()
17+
@callback delete_table!(pid :: pid()) :: :ok
18+
19+
@doc """
20+
Returns ETS table reference.
21+
It creates table if none is associated with given pid
22+
"""
23+
@spec table!(pid :: pid()) :: :ets.table()
24+
def table!(pid) when is_pid(pid) do
25+
impl().table!(pid)
26+
end
27+
28+
@doc """
29+
If table for given `pid` exists it deletes it from ETS.
30+
"""
31+
@spec delete_table!(pid :: pid()) :: :ok
32+
def delete_table!(pid) when is_pid(pid) do
33+
impl().delete_table!(pid)
34+
end
35+
36+
def impl() do
37+
Application.get_env(:live_debugger, :ets_table_server, __MODULE__.Impl)
38+
end
39+
40+
defmodule Impl do
41+
@moduledoc false
42+
@behaviour LiveDebugger.GenServers.EtsTableServer
43+
@server_module LiveDebugger.GenServers.EtsTableServer
44+
45+
@impl true
46+
def table!(pid) do
47+
GenServer.call(@server_module, {:get_or_create_table, pid}, 1000)
48+
end
49+
50+
@impl true
51+
def delete_table!(pid) do
52+
GenServer.call(@server_module, {:delete_table, pid}, 1000)
53+
end
54+
end
55+
56+
## GenServer
57+
58+
@doc false
59+
def start_link(args \\ []) do
60+
GenServer.start_link(__MODULE__, args, name: __MODULE__)
61+
end
62+
63+
@impl true
64+
def init(_args) do
65+
{:ok, %{}}
66+
end
67+
68+
@impl true
69+
def handle_info({:DOWN, _, :process, closed_pid, _}, table_refs) do
70+
{_, table_refs} = delete_ets_table(closed_pid, table_refs)
71+
72+
closed_pid
73+
|> PubSubUtils.process_status_topic()
74+
|> PubSubUtils.broadcast({:process_status, :dead})
75+
76+
{:noreply, table_refs}
77+
end
78+
79+
@impl true
80+
def handle_call({:get_or_create_table, pid}, _from, table_refs) do
81+
case Map.get(table_refs, pid) do
82+
nil ->
83+
ref = create_ets_table()
84+
Process.monitor(pid)
85+
{:reply, ref, Map.put(table_refs, pid, ref)}
86+
87+
ref ->
88+
{:reply, ref, table_refs}
89+
end
90+
end
91+
92+
@impl true
93+
def handle_call({:delete_table, pid}, _from, table_refs) do
94+
{_, table_refs} = delete_ets_table(pid, table_refs)
95+
{:reply, :ok, table_refs}
96+
end
97+
98+
@spec create_ets_table() :: :ets.table()
99+
defp create_ets_table() do
100+
:ets.new(@ets_table_name, [:ordered_set, :public])
101+
end
102+
103+
@spec delete_ets_table(pid(), table_refs()) :: {boolean(), table_refs()}
104+
defp delete_ets_table(pid, table_refs) do
105+
case Map.pop(table_refs, pid) do
106+
{nil, table_refs} ->
107+
{false, table_refs}
108+
109+
{ref, updated_table_refs} ->
110+
:ets.delete(ref)
111+
{true, updated_table_refs}
112+
end
113+
end
114+
end

lib/live_debugger/services/trace_service.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
defmodule LiveDebugger.Services.TraceService do
22
@moduledoc """
33
This module is responsible for accessing traces from ETS.
4-
It uses calls to `CallbackTracingServer` to get proper table reference.
4+
It uses calls to `EtsTableServer` to get proper table reference.
55
"""
66

77
alias LiveDebugger.Structs.Trace
88
alias LiveDebugger.CommonTypes
9-
alias LiveDebugger.GenServers.CallbackTracingServer
9+
alias LiveDebugger.GenServers.EtsTableServer
1010
alias Phoenix.LiveComponent.CID
1111

1212
@default_limit 100
@@ -15,7 +15,7 @@ defmodule LiveDebugger.Services.TraceService do
1515
@type ets_continuation :: term()
1616
@typedoc """
1717
Pid is used to store mapping to table references.
18-
It identifies ETS tables managed by CallbackTracingServer
18+
It identifies ETS tables managed by EtsTableServer.
1919
"""
2020
@type ets_table_id() :: pid()
2121

@@ -149,6 +149,6 @@ defmodule LiveDebugger.Services.TraceService do
149149

150150
@spec ets_table!(pid :: ets_table_id()) :: :ets.table()
151151
defp ets_table!(pid) when is_pid(pid) do
152-
CallbackTracingServer.table!(pid)
152+
EtsTableServer.table!(pid)
153153
end
154154
end
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
defmodule LiveDebugger.GenServers.EtsTableServerTest do
2+
@moduledoc false
3+
use ExUnit.Case, async: true
4+
5+
alias LiveDebugger.Utils.PubSub, as: PubSubUtils
6+
alias LiveDebugger.GenServers.EtsTableServer
7+
8+
test "start_link/1" do
9+
assert {:ok, _pid} = EtsTableServer.start_link()
10+
GenServer.stop(EtsTableServer)
11+
end
12+
13+
test "init/1" do
14+
assert {:ok, %{}} = EtsTableServer.init([])
15+
end
16+
17+
describe "gen server api" do
18+
test "table!/1" do
19+
pid = :c.pid(0, 0, 1)
20+
21+
LiveDebugger.MockEtsTableServer
22+
|> Mox.expect(:table!, fn ^pid -> :some_ref end)
23+
24+
assert :some_ref = EtsTableServer.table!(pid)
25+
end
26+
27+
test "delete_table!/1" do
28+
pid = :c.pid(0, 0, 1)
29+
30+
LiveDebugger.MockEtsTableServer
31+
|> Mox.expect(:delete_table!, fn ^pid -> :ok end)
32+
33+
assert :ok = EtsTableServer.delete_table!(pid)
34+
end
35+
end
36+
37+
describe "handle_info/2" do
38+
test "deletes table ref after process down" do
39+
pid = :c.pid(0, 0, 1)
40+
ref = :ets.new(:test_table, [])
41+
42+
other_pid = :c.pid(0, 0, 2)
43+
other_ref = :ets.new(:test_table, [])
44+
45+
table_refs = %{pid => ref, other_pid => other_ref}
46+
47+
topic = PubSubUtils.process_status_topic(pid)
48+
49+
LiveDebugger.MockPubSubUtils
50+
|> Mox.expect(:broadcast, fn ^topic, {:process_status, :dead} -> :ok end)
51+
52+
assert {:noreply, new_table_refs} =
53+
EtsTableServer.handle_info({:DOWN, :_, :process, pid, :_}, table_refs)
54+
55+
assert :undefined == :ets.info(ref)
56+
assert nil == Map.get(new_table_refs, pid)
57+
58+
assert [{:id, ^other_ref} | _] = :ets.info(other_ref)
59+
assert other_ref == Map.get(new_table_refs, other_pid)
60+
end
61+
end
62+
63+
describe "handle_call/3" do
64+
test "deletes table on event {:delete_table, pid}" do
65+
pid = :c.pid(0, 0, 1)
66+
ref = :ets.new(:test_table, [])
67+
68+
other_pid = :c.pid(0, 0, 2)
69+
other_ref = :ets.new(:test_table, [])
70+
71+
table_refs = %{pid => ref, other_pid => other_ref}
72+
73+
assert {:reply, :ok, new_table_refs} =
74+
EtsTableServer.handle_call({:delete_table, pid}, self(), table_refs)
75+
76+
assert :undefined == :ets.info(ref)
77+
assert nil == Map.get(new_table_refs, pid)
78+
79+
assert [{:id, ^other_ref} | _] = :ets.info(other_ref)
80+
assert other_ref == Map.get(new_table_refs, other_pid)
81+
end
82+
83+
test "ignores delete table on event {:delete_table, pid} when table does not exist" do
84+
pid = :c.pid(0, 0, 1)
85+
ref = :ets.new(:test_table, [])
86+
87+
other_pid = :c.pid(0, 0, 2)
88+
89+
table_refs = %{pid => ref}
90+
91+
assert {:reply, :ok, new_table_refs} =
92+
EtsTableServer.handle_call({:delete_table, other_pid}, self(), table_refs)
93+
94+
assert [{:id, ^ref} | _] = :ets.info(ref)
95+
assert ref == Map.get(new_table_refs, pid)
96+
97+
assert nil == Map.get(new_table_refs, other_pid)
98+
end
99+
100+
test "creates table on event {:get_or_create_table, pid}" do
101+
pid = :c.pid(0, 0, 1)
102+
table_refs = %{}
103+
104+
assert {:reply, ref, new_table_refs} =
105+
EtsTableServer.handle_call({:get_or_create_table, pid}, self(), table_refs)
106+
107+
assert [{:id, ^ref} | _] = :ets.info(ref)
108+
assert ref == Map.get(new_table_refs, pid)
109+
end
110+
111+
test "returns existing table on event {:get_or_create_table, pid}" do
112+
pid = :c.pid(0, 0, 1)
113+
ref = :ets.new(:test_table, [])
114+
table_refs = %{pid => ref}
115+
116+
assert {:reply, ^ref, new_table_refs} =
117+
EtsTableServer.handle_call({:get_or_create_table, pid}, self(), table_refs)
118+
119+
assert ref == Map.get(new_table_refs, pid)
120+
end
121+
end
122+
end

0 commit comments

Comments
 (0)