Skip to content

Commit 7e41005

Browse files
benwilson512José Valim
authored andcommitted
Introduce Registry.unregister_match/4 (#6326)
1 parent 288e8ea commit 7e41005

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

lib/elixir/lib/registry.ex

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,14 +588,36 @@ defmodule Registry do
588588
def keys(registry, pid) when is_atom(registry) and is_pid(pid) do
589589
{kind, partitions, _, pid_ets, _} = info!(registry)
590590
{_, pid_ets} = pid_ets || pid_ets!(registry, pid, partitions)
591-
keys = safe_lookup_second(pid_ets, pid)
591+
592+
keys = try do
593+
spec = [{{pid, :'$1', :'$2'}, [], [{{:'$1', :'$2'}}]}]
594+
:ets.select(pid_ets, spec)
595+
catch
596+
:error, :badarg -> []
597+
end
598+
599+
# Handle the possibility of fake keys
600+
keys = gather_keys(keys, [], false)
592601

593602
cond do
594603
kind == :unique -> Enum.uniq(keys)
595604
true -> keys
596605
end
597606
end
598607

608+
defp gather_keys([{key, {_, remaining}} | rest], acc, _fake) do
609+
gather_keys(rest, [key | acc], {key, remaining})
610+
end
611+
defp gather_keys([{key, _} | rest], acc, fake) do
612+
gather_keys(rest, [key | acc], fake)
613+
end
614+
defp gather_keys([], acc, {key, remaining}) do
615+
List.duplicate(key, remaining) ++ Enum.reject(acc, & &1 === key)
616+
end
617+
defp gather_keys([], acc, false) do
618+
acc
619+
end
620+
599621
@doc """
600622
Unregisters all entries for the given `key` associated to the current
601623
process in `registry`.
@@ -653,6 +675,95 @@ defmodule Registry do
653675
:ok
654676
end
655677

678+
@doc """
679+
Unregister entries for a given key matching a pattern.
680+
681+
## Examples
682+
683+
For unique registries it can be used to conditionally unregister a key on
684+
the basis of whether or not it matches a particular value.
685+
686+
iex> Registry.start_link(:unique, Registry.UniqueUnregisterMatchTest)
687+
iex> Registry.register(Registry.UniqueUnregisterMatchTest, "hello", :world)
688+
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
689+
["hello"]
690+
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :foo)
691+
:ok
692+
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
693+
["hello"]
694+
iex> Registry.unregister_match(Registry.UniqueUnregisterMatchTest, "hello", :world)
695+
:ok
696+
iex> Registry.keys(Registry.UniqueUnregisterMatchTest, self())
697+
[]
698+
699+
For duplicate registries:
700+
701+
iex> Registry.start_link(:duplicate, Registry.DuplicateUnregisterMatchTest)
702+
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
703+
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_b)
704+
iex> Registry.register(Registry.DuplicateUnregisterMatchTest, "hello", :world_c)
705+
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
706+
["hello", "hello", "hello"]
707+
iex> Registry.unregister_match(Registry.DuplicateUnregisterMatchTest, "hello", :world_a)
708+
:ok
709+
iex> Registry.keys(Registry.DuplicateUnregisterMatchTest, self())
710+
["hello", "hello"]
711+
iex> Registry.lookup(Registry.DuplicateUnregisterMatchTest, "hello")
712+
[{self(), :world_b}, {self(), :world_c}]
713+
"""
714+
def unregister_match(registry, key, pattern, guards \\ []) when is_list(guards) do
715+
self = self()
716+
717+
{kind, partitions, key_ets, pid_ets, listeners} = info!(registry)
718+
{key_partition, pid_partition} = partitions(kind, key, self, partitions)
719+
key_ets = key_ets || key_ets!(registry, key_partition)
720+
{pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition)
721+
722+
# Remove first from the key_ets because in case of crashes
723+
# the pid_ets will still be able to clean up. The last step is
724+
# to clean if we have no more entries.
725+
726+
# Here we want to count all entries for this pid under this key, regardless
727+
# of pattern.
728+
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
729+
total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}]
730+
total = :ets.select_count(key_ets, total_spec)
731+
732+
# We only want to delete things that match the pattern
733+
delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards] ,[true]}]
734+
case :ets.select_delete(key_ets, delete_spec) do
735+
# We deleted everything, we can just delete the object
736+
^total ->
737+
true = :ets.delete_object(pid_ets, {self, key, key_ets})
738+
739+
unlink_if_unregistered(pid_server, pid_ets, self)
740+
741+
for listener <- listeners do
742+
Kernel.send(listener, {:unregister, registry, key, self})
743+
end
744+
745+
0 ->
746+
:ok
747+
748+
deleted ->
749+
# There are still entries remaining for this pid. delete_object/2 with
750+
# duplicate_bag tables will remove every entry, but we only want to
751+
# remove those we have deleted. The solution is to introduce a temp_entry
752+
# that indicates how many keys WILL be remaining after the delete operation.
753+
remaining = total - deleted
754+
temp_entry = {self, key, {key_ets, remaining}}
755+
true = :ets.insert(pid_ets, temp_entry)
756+
true = :ets.delete_object(pid_ets, {self, key, key_ets})
757+
real_keys = List.duplicate({self, key, key_ets}, remaining)
758+
true = :ets.insert(pid_ets, real_keys)
759+
# We've recreated the real remaining key entries, so we can now delete
760+
# our temporary entry.
761+
true = :ets.delete_object(pid_ets, temp_entry)
762+
end
763+
764+
:ok
765+
end
766+
656767
@doc """
657768
Registers the current process under the given `key` in `registry`.
658769
@@ -977,6 +1088,12 @@ defmodule Registry.Partition do
9771088
def handle_info({:EXIT, pid, _reason}, ets) do
9781089
entries = :ets.take(ets, pid)
9791090
for {_pid, key, key_ets} <- entries do
1091+
key_ets = case key_ets do
1092+
# In case the fake key ets is being used. See unregister_match/2.
1093+
{key_ets, _} -> key_ets
1094+
_ -> key_ets
1095+
end
1096+
9801097
try do
9811098
:ets.match_delete(key_ets, {key, {pid, :_}})
9821099
catch

lib/elixir/test/elixir/registry_test.exs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,42 @@ defmodule RegistryTest do
8686
[{self(), value}]
8787
end
8888

89+
test "unregister_match supports patterns", %{registry: registry} do
90+
value = {1, :atom, 1}
91+
{:ok, _} = Registry.register(registry, "hello", value)
92+
93+
Registry.unregister_match(registry, "hello", {2, :_, :_})
94+
assert Registry.lookup(registry, "hello") ==
95+
[{self(), value}]
96+
Registry.unregister_match(registry, "hello", {1.0, :_, :_})
97+
assert Registry.lookup(registry, "hello") ==
98+
[{self(), value}]
99+
Registry.unregister_match(registry, "hello", {:_, :atom, :_})
100+
assert Registry.lookup(registry, "hello") ==
101+
[]
102+
end
103+
104+
test "unregister_match supports guards", %{registry: registry} do
105+
value = {1, :atom, 1}
106+
{:ok, _} = Registry.register(registry, "hello", value)
107+
108+
Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}])
109+
assert Registry.lookup(registry, "hello") ==
110+
[]
111+
end
112+
113+
test "unregister_match supports tricky keys", %{registry: registry} do
114+
{:ok, _} = Registry.register(registry, :_, :foo)
115+
{:ok, _} = Registry.register(registry, "hello", "b")
116+
117+
Registry.unregister_match(registry, :_, :foo)
118+
assert Registry.lookup(registry, :_) ==
119+
[]
120+
121+
assert Registry.keys(registry, self()) |> Enum.sort ==
122+
["hello"]
123+
end
124+
89125
test "compares using ===", %{registry: registry} do
90126
{:ok, _} = Registry.register(registry, 1.0, :value)
91127
{:ok, _} = Registry.register(registry, 1, :value)
@@ -351,6 +387,50 @@ defmodule RegistryTest do
351387
[{self(), value1}, {self(), value2}]
352388
end
353389

390+
test "unregister_match supports patterns", %{registry: registry} do
391+
value1 = {1, :atom, 1}
392+
{:ok, _} = Registry.register(registry, "hello", value1)
393+
value2 = {2, :atom, 2}
394+
{:ok, _} = Registry.register(registry, "hello", value2)
395+
396+
Registry.unregister_match(registry, "hello", {2, :_, :_})
397+
assert Registry.lookup(registry, "hello") ==
398+
[{self(), value1}]
399+
400+
{:ok, _} = Registry.register(registry, "hello", value2)
401+
Registry.unregister_match(registry, "hello", {2.0, :_, :_})
402+
assert Registry.lookup(registry, "hello") ==
403+
[{self(), value1}, {self(), value2}]
404+
Registry.unregister_match(registry, "hello", {:_, :atom, :_})
405+
assert Registry.lookup(registry, "hello") ==
406+
[]
407+
end
408+
409+
test "unregister_match supports guards", %{registry: registry} do
410+
value1 = {1, :atom, 1}
411+
{:ok, _} = Registry.register(registry, "hello", value1)
412+
value2 = {2, :atom, 2}
413+
{:ok, _} = Registry.register(registry, "hello", value2)
414+
415+
Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}])
416+
assert Registry.lookup(registry, "hello") ==
417+
[{self(), value2}]
418+
end
419+
420+
test "unregister_match supports tricky keys", %{registry: registry} do
421+
{:ok, _} = Registry.register(registry, :_, :foo)
422+
{:ok, _} = Registry.register(registry, :_, :bar)
423+
{:ok, _} = Registry.register(registry, "hello", "a")
424+
{:ok, _} = Registry.register(registry, "hello", "b")
425+
426+
Registry.unregister_match(registry, :_, :foo)
427+
assert Registry.lookup(registry, :_) ==
428+
[{self(), :bar}]
429+
430+
assert Registry.keys(registry, self()) |> Enum.sort ==
431+
[:_, "hello", "hello"]
432+
end
433+
354434
@tag listener: :"duplicate_listener_#{partitions}"
355435
test "allows listeners", %{registry: registry, listener: listener} do
356436
Process.register(self(), listener)

0 commit comments

Comments
 (0)