11defmodule LiveDebugger.GenServers.EtsTableServer do
22 @ moduledoc """
33 This gen_server is responsible for managing ETS tables.
4+
5+ It sends `{:process_status, {:dead, pid}}` to the process status topic.
6+
7+ ## Dead View Mode
8+ When in dead view mode, the gen_server will send `{:process_status, {:dead, pid}}` to the process status topic when a process dies.
9+ It will wait for all watchers to be removed and then delete the ETS table and sends `{:process_status, {:dead, pid}}` to the process status topic.
410 """
511
12+ defmodule TableInfo do
13+ @ moduledoc """
14+ - `table`: ETS table reference.
15+ - `alive?`: Indicates if the process is alive.
16+ - `watchers`: Set of pids that are watching this table.
17+ """
18+ defstruct [ :table , alive?: true , watchers: MapSet . new ( ) ]
19+
20+ @ type t ( ) :: % __MODULE__ {
21+ alive?: boolean ( ) ,
22+ table: :ets . table ( ) ,
23+ watchers: MapSet . t ( )
24+ }
25+ end
26+
627 use GenServer
728
29+ alias __MODULE__ . TableInfo
830 alias LiveDebugger.Utils.PubSub , as: PubSubUtils
931
10- @ ets_table_name :lvdbg_traces
32+ @ type state ( ) :: % { pid ( ) => TableInfo . t ( ) }
1133
12- @ type table_refs ( ) :: % { pid ( ) => :ets . table ( ) }
34+ @ ets_table_name :lvdbg_traces
1335
1436 ## API
1537
16- @ callback table! ( pid :: pid ( ) ) :: :ets . table ( )
17- @ callback delete_table! ( pid :: pid ( ) ) :: :ok
38+ @ callback table ( pid :: pid ( ) ) :: :ets . table ( )
39+ @ callback watch ( pid :: pid ( ) ) :: :ok | { :error , term ( ) }
1840
1941 @ doc """
2042 Returns ETS table reference.
2143 It creates table if none is associated with given pid
2244 """
23- @ spec table! ( pid :: pid ( ) ) :: :ets . table ( )
24- def table! ( pid ) when is_pid ( pid ) , do: impl ( ) . table! ( pid )
45+ @ spec table ( pid :: pid ( ) ) :: :ets . table ( )
46+ def table ( pid ) when is_pid ( pid ) , do: impl ( ) . table ( pid )
2547
2648 @ doc """
27- If table for given `pid` exists it deletes it from ETS.
49+ Adds watcher to indicate when to delete table from ETS.
50+ It uses pid of process which the function was called.
2851 """
29- @ spec delete_table! ( pid :: pid ( ) ) :: :ok
30- def delete_table! ( pid ) when is_pid ( pid ) , do: impl ( ) . delete_table! ( pid )
52+ @ spec watch ( pid :: pid ( ) ) :: :ok | { :error , term ( ) }
53+ def watch ( pid ) when is_pid ( pid ) do
54+ impl ( ) . watch ( pid )
55+ end
3156
3257 ## GenServer
3358
@@ -41,33 +66,63 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
4166 { :ok , % { } }
4267 end
4368
69+ # This is for debugged processes monitored
70+ @ impl true
71+ def handle_info ( { :DOWN , _ , :process , closed_pid , _ } , state )
72+ when is_map_key ( state , closed_pid ) do
73+ if LiveDebugger.Feature . enabled? ( :dead_view_mode ) do
74+ PubSubUtils . process_status_topic ( )
75+ |> PubSubUtils . broadcast ( { :process_status , { :died , closed_pid } } )
76+ end
77+
78+ state =
79+ state
80+ |> Map . update! ( closed_pid , fn table_info -> % { table_info | alive?: false } end )
81+ |> maybe_delete_ets_table ( closed_pid )
82+
83+ { :noreply , state }
84+ end
85+
86+ # This is for watchers processes
4487 @ impl true
45- def handle_info ( { :DOWN , _ , :process , closed_pid , _ } , table_refs ) do
46- { _ , table_refs } = delete_ets_table ( closed_pid , table_refs )
88+ def handle_info ( { :DOWN , _ , :process , closed_pid , _ } , state ) do
89+ { updated_state , touched_pids } = remove_watcher ( state , closed_pid )
4790
48- PubSubUtils . process_status_topic ( )
49- |> PubSubUtils . broadcast ( { :process_status , { :dead , closed_pid } } )
91+ updated_state =
92+ touched_pids
93+ |> Enum . reduce ( updated_state , fn pid , state_acc ->
94+ maybe_delete_ets_table ( state_acc , pid )
95+ end )
5096
51- { :noreply , table_refs }
97+ { :noreply , updated_state }
5298 end
5399
54100 @ impl true
55- def handle_call ( { :get_or_create_table , pid } , _from , table_refs ) do
56- case Map . get ( table_refs , pid ) do
101+ def handle_call ( { :get_or_create_table , pid } , _from , state ) do
102+ case Map . get ( state , pid ) do
57103 nil ->
58104 ref = create_ets_table ( )
59105 Process . monitor ( pid )
60- { :reply , ref , Map . put ( table_refs , pid , ref ) }
106+ { :reply , ref , Map . put ( state , pid , % TableInfo { table: ref } ) }
61107
62- ref ->
63- { :reply , ref , table_refs }
108+ % TableInfo { table: ref } ->
109+ { :reply , ref , state }
64110 end
65111 end
66112
67113 @ impl true
68- def handle_call ( { :delete_table , pid } , _from , table_refs ) do
69- { _ , table_refs } = delete_ets_table ( pid , table_refs )
70- { :reply , :ok , table_refs }
114+ def handle_call ( { :watch , pid } , { watcher , _ } , state ) do
115+ case Map . get ( state , pid ) do
116+ % TableInfo { } ->
117+ updated_state = update_watchers ( state , pid , & MapSet . put ( & 1 , watcher ) )
118+
119+ Process . monitor ( watcher )
120+
121+ { :reply , :ok , updated_state }
122+
123+ _ ->
124+ { :reply , { :error , :process_not_found } , state }
125+ end
71126 end
72127
73128 defp impl ( ) do
@@ -80,13 +135,17 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
80135 @ server_module LiveDebugger.GenServers.EtsTableServer
81136
82137 @ impl true
83- def table! ( pid ) do
138+ def table ( pid ) do
84139 GenServer . call ( @ server_module , { :get_or_create_table , pid } , 1000 )
85140 end
86141
87142 @ impl true
88- def delete_table! ( pid ) do
89- GenServer . call ( @ server_module , { :delete_table , pid } , 1000 )
143+ def watch ( pid ) do
144+ if LiveDebugger.Feature . enabled? ( :dead_view_mode ) do
145+ GenServer . call ( @ server_module , { :watch , pid } , 1000 )
146+ else
147+ { :error , :not_in_dead_view_mode }
148+ end
90149 end
91150 end
92151
@@ -95,15 +154,36 @@ defmodule LiveDebugger.GenServers.EtsTableServer do
95154 :ets . new ( @ ets_table_name , [ :ordered_set , :public ] )
96155 end
97156
98- @ spec delete_ets_table ( pid ( ) , table_refs ( ) ) :: { boolean ( ) , table_refs ( ) }
99- defp delete_ets_table ( pid , table_refs ) do
100- case Map . pop ( table_refs , pid ) do
101- { nil , table_refs } ->
102- { false , table_refs }
103-
104- { ref , updated_table_refs } ->
105- :ets . delete ( ref )
106- { true , updated_table_refs }
157+ @ spec maybe_delete_ets_table ( state ( ) , pid ( ) ) :: state ( )
158+ defp maybe_delete_ets_table ( state , pid ) do
159+ with { % TableInfo { alive?: false } = table_info , updated_state } <- Map . pop ( state , pid ) ,
160+ true <- Enum . empty? ( table_info . watchers ) do
161+ PubSubUtils . process_status_topic ( )
162+ |> PubSubUtils . broadcast ( { :process_status , { :dead , pid } } )
163+
164+ :ets . delete ( table_info . table )
165+ updated_state
166+ else
167+ _ ->
168+ state
107169 end
108170 end
171+
172+ @ spec remove_watcher ( state ( ) , pid ( ) ) :: { updated_state :: state ( ) , touched_pids :: [ pid ( ) ] }
173+ defp remove_watcher ( state , watcher ) when is_pid ( watcher ) do
174+ Enum . reduce ( state , { state , [ ] } , fn { pid , % { watchers: watchers } } , { state_acc , pids } ->
175+ if Enum . member? ( watchers , watcher ) do
176+ updated_state = update_watchers ( state_acc , pid , & MapSet . delete ( & 1 , watcher ) )
177+
178+ { updated_state , [ pid | pids ] }
179+ else
180+ { state_acc , pids }
181+ end
182+ end )
183+ end
184+
185+ defp update_watchers ( state , pid , update_fn ) when is_map_key ( state , pid ) do
186+ table_info = Map . get ( state , pid )
187+ Map . put ( state , pid , % { table_info | watchers: update_fn . ( table_info . watchers ) } )
188+ end
109189end
0 commit comments