Skip to content

Commit e435ace

Browse files
committed
Optimize Shard.list and Shard.get_by_key
Previous list and get_by_key had to go through GenServer to acquire values ets table and replicas information. In case GenServer was processing an update (e.g. heartbeat, track, untrack) then list and get_by_key functions were blocked until it was completed. We saw this behaviour in our cluster where simple list/get_by_key calls were sometimes taking over few hundred milliseconds. Storing down replicas information in an ets table allows us to avoid going through genserver and allows us to process list/get_by_key immediately. I removed dirty_list function which was not public / exposed and which was trying to resolve the same issue. dirty_list was called dirty because it didn't check for down_replicas. This solution checks down_replicas and doesn't change the api interface. This should also resolve #124
1 parent 1bc0b25 commit e435ace

3 files changed

Lines changed: 42 additions & 36 deletions

File tree

lib/phoenix/tracker/shard.ex

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,23 +68,24 @@ defmodule Phoenix.Tracker.Shard do
6868
end
6969

7070
@spec list(pid | atom, topic) :: [presence]
71-
def list(server_pid, topic) do
71+
def list(server_pid, topic) when is_pid(server_pid) do
7272
server_pid
7373
|> GenServer.call({:list, topic})
7474
|> State.get_by_topic(topic)
7575
end
76-
77-
@doc false
78-
def dirty_list(shard_name, topic) do
79-
State.tracked_values(shard_name, topic, [])
76+
def list(shard_name, topic) when is_atom(shard_name) do
77+
State.get_by_topic(shard_name, topic)
8078
end
8179

8280
@spec get_by_key(pid | atom, topic, term) :: [presence]
83-
def get_by_key(server_pid, topic, key) do
81+
def get_by_key(server_pid, topic, key) when is_pid(server_pid) do
8482
server_pid
8583
|> GenServer.call({:list, topic})
8684
|> State.get_by_key(topic, key)
8785
end
86+
def get_by_key(shard_name, topic, key) when is_atom(shard_name) do
87+
State.get_by_key(shard_name, topic, key)
88+
end
8889

8990
@spec graceful_permdown(pid) :: :ok
9091
def graceful_permdown(server_pid) do

lib/phoenix/tracker/state.ex

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ defmodule Phoenix.Tracker.State do
2121
@type pid_lookup :: {pid, topic, key}
2222

2323
@type t :: %State{
24-
replica: name,
25-
context: context,
26-
clouds: clouds,
27-
values: values,
28-
pids: ets_id,
29-
mode: :unset | :delta | :normal,
30-
delta: :unset | delta,
31-
replicas: %{name => :up | :down},
32-
range: {context, context}
24+
replica: name,
25+
context: context,
26+
clouds: clouds,
27+
values: values,
28+
pids: ets_id,
29+
mode: :unset | :delta | :normal,
30+
delta: :unset | delta,
31+
down_replicas: ets_id,
32+
range: {context, context}
3333
}
3434

3535
defstruct replica: nil,
@@ -39,7 +39,7 @@ defmodule Phoenix.Tracker.State do
3939
pids: nil,
4040
mode: :unset,
4141
delta: :unset,
42-
replicas: %{},
42+
down_replicas: nil,
4343
range: {%{}, %{}}
4444

4545
@compile {:inline, tag: 1, clock: 1, put_tag: 2, delete_tag: 2, remove_delta_tag: 2}
@@ -61,7 +61,7 @@ defmodule Phoenix.Tracker.State do
6161
mode: :normal,
6262
values: :ets.new(shard_name, [:named_table, :protected, :ordered_set]),
6363
pids: :ets.new(:pids, [:duplicate_bag]),
64-
replicas: %{replica => :up}})
64+
down_replicas: :ets.new(down_replicas_table(shard_name), [:named_table, :protected, :bag])})
6565
end
6666

6767
@doc """
@@ -120,6 +120,9 @@ defmodule Phoenix.Tracker.State do
120120
def get_by_topic(%State{values: values} = state, topic) do
121121
tracked_values(values, topic, down_replicas(state))
122122
end
123+
def get_by_topic(shard_name, topic) do
124+
tracked_values(shard_name, topic, down_replicas(shard_name))
125+
end
123126

124127
@doc """
125128
Returns a list of elements for the topic who belong to an online replica.
@@ -131,6 +134,12 @@ defmodule Phoenix.Tracker.State do
131134
[_|_] = metas -> metas
132135
end
133136
end
137+
def get_by_key(shard_name, topic, key) do
138+
case tracked_key(shard_name, topic, key, down_replicas(shard_name)) do
139+
[] -> []
140+
[_|_] = metas -> metas
141+
end
142+
end
134143

135144
@doc """
136145
Performs table lookup for tracked elements in the topic.
@@ -393,18 +402,18 @@ defmodule Phoenix.Tracker.State do
393402
Marks a replica as up in the set and returns rejoined users.
394403
"""
395404
@spec replica_up(t, name) :: {t, joins :: [values], leaves :: []}
396-
def replica_up(%State{replicas: replicas, context: ctx} = state, replica) do
397-
{%State{state |
398-
context: Map.put_new(ctx, replica, 0),
399-
replicas: Map.put(replicas, replica, :up)}, replica_users(state, replica), []}
405+
def replica_up(%State{down_replicas: down_replicas, context: ctx} = state, replica) do
406+
:ets.delete_object(down_replicas, replica)
407+
{%State{state | context: Map.put_new(ctx, replica, 0)}, replica_users(state, replica), []}
400408
end
401409

402410
@doc """
403411
Marks a replica as down in the set and returns left users.
404412
"""
405413
@spec replica_down(t, name) :: {t, joins:: [], leaves :: [values]}
406-
def replica_down(%State{replicas: replicas} = state, replica) do
407-
{%State{state | replicas: Map.put(replicas, replica, :down)}, [], replica_users(state, replica)}
414+
def replica_down(%State{down_replicas: down_replicas} = state, replica) do
415+
:ets.insert(down_replicas, replica)
416+
{state, [], replica_users(state, replica)}
408417
end
409418

410419
@doc """
@@ -556,9 +565,8 @@ defmodule Phoenix.Tracker.State do
556565
end
557566

558567
@spec down_replicas(t) :: [name]
559-
defp down_replicas(%State{replicas: replicas}) do
560-
for {replica, :down} <- replicas, do: replica
561-
end
568+
defp down_replicas(%State{down_replicas: down_replicas}), do: :ets.tab2list(down_replicas)
569+
defp down_replicas(shard_name), do: :ets.tab2list(down_replicas_table(shard_name))
562570

563571
@spec replica_users(t, name) :: [value]
564572
defp replica_users(%State{values: values}, replica) do
@@ -575,4 +583,8 @@ defmodule Phoenix.Tracker.State do
575583
defp foldl({objects, cont}, acc, func) do
576584
foldl(:ets.select(cont), Enum.reduce(objects, acc, func), func)
577585
end
586+
587+
defp down_replicas_table(shard_name) do
588+
:"#{shard_name}.down_replicas"
589+
end
578590
end

test/phoenix/tracker/shard_replication_test.exs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ defmodule Phoenix.Tracker.ShardReplicationTest do
4242
# node1 fulfills tranfer request and sends transfer_ack to primary
4343
assert_transfer_ack ref, from: @node1
4444
assert_heartbeat to: @node1, from: @primary
45+
46+
# small delay to ensure transfer_ack has been processed before calling list
47+
:timer.sleep(10)
4548
assert [{"node1", _}] = list(shard, topic)
4649
end
4750

@@ -237,21 +240,15 @@ defmodule Phoenix.Tracker.ShardReplicationTest do
237240
assert_join ^topic, "node1", %{name: "s1"}
238241
assert %{@node1 => %Replica{status: :up}} = replicas(shard)
239242
assert [{"local1", _}, {"node1", _}] = list(shard, topic)
240-
assert [{"local1", _}, {"node1", _}] = dirty_list(shard, topic)
241243

242244
# nodedown
243245
Process.unlink(node_pid)
244246
Process.exit(node1_server, :kill)
245247
assert_leave ^topic, "node1", %{name: "s1"}
246248
assert %{@node1 => %Replica{status: :down}} = replicas(shard)
247249
assert [{"local1", _}] = list(shard, topic)
248-
assert [{"local1", _}, {"node1", _}] = dirty_list(shard, topic)
249-
250-
:timer.sleep(@permdown + 2*@heartbeat)
251-
assert [{"local1", _}] = dirty_list(shard, topic)
252250
end
253251

254-
255252
test "untrack with no tracked topic is a noop",
256253
%{shard: shard, topic: topic} do
257254
assert Shard.untrack(shard, self(), topic, "foo") == :ok
@@ -382,8 +379,4 @@ defmodule Phoenix.Tracker.ShardReplicationTest do
382379
defp list(shard, topic) do
383380
Enum.sort(Shard.list(shard, topic))
384381
end
385-
386-
defp dirty_list(shard, topic) do
387-
Enum.sort(Shard.dirty_list(shard, topic))
388-
end
389382
end

0 commit comments

Comments
 (0)