From 6208b333bef85284a17f2d57453c499ab02fc6ff Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:44:40 +0100 Subject: [PATCH 01/23] Switch {:duplicate, :key} key_ets to ordered_set Change init_key_ets to create an ordered_set table for {:duplicate, :key} and add ordered boolean to Partition state. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index ad986af53cb..e8d3cfff6c3 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1734,7 +1734,8 @@ defmodule Registry.Partition do true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}}) end - {:ok, {pid_ets, %{}}} + ordered = kind == {:duplicate, :key} + {:ok, {pid_ets, %{}, ordered}} end # The key partition is a set for unique keys, @@ -1744,7 +1745,12 @@ defmodule Registry.Partition do :ets.new(key_partition, compression_opt(opts, compressed)) end - defp init_key_ets({:duplicate, _}, key_partition, compressed) do + defp init_key_ets({:duplicate, :key}, key_partition, compressed) do + opts = [:ordered_set, :public, read_concurrency: true, write_concurrency: true] + :ets.new(key_partition, compression_opt(opts, compressed)) + end + + defp init_key_ets({:duplicate, :pid}, key_partition, compressed) do opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true] :ets.new(key_partition, compression_opt(opts, compressed)) end @@ -1768,7 +1774,7 @@ defmodule Registry.Partition do {:reply, :ok, state} end - def handle_call({:lock, key}, from, {ets, lock}) do + def handle_call({:lock, key}, from, {ets, lock, ordered}) do lock = case lock do %{^key => queue} -> @@ -1779,10 +1785,10 @@ defmodule Registry.Partition do Map.put(lock, key, :queue.new()) end - {:noreply, {ets, lock}} + {:noreply, {ets, lock, ordered}} end - def handle_info({:EXIT, pid, _reason}, {ets, lock}) do + def handle_info({:EXIT, pid, _reason}, {ets, lock, ordered}) do entries = :ets.take(ets, pid) for {_pid, key, key_ets, _counter} <- entries do @@ -1803,7 +1809,7 @@ defmodule Registry.Partition do end end - {:noreply, {ets, lock}} + {:noreply, {ets, lock, ordered}} end def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do @@ -1815,7 +1821,7 @@ defmodule Registry.Partition do unlock(key, state) end - defp unlock(key, {ets, lock}) do + defp unlock(key, {ets, lock, ordered}) do %{^key => queue} = lock lock = @@ -1824,7 +1830,7 @@ defmodule Registry.Partition do {:not_empty, queue} -> Map.put(lock, key, queue) end - {:noreply, {ets, lock}} + {:noreply, {ets, lock, ordered}} end defp dequeue(queue, key) do From 2f0a03875fc24c8e14b962ecc83627d01afd19cb Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:45:27 +0100 Subject: [PATCH 02/23] Build composite key entry for {:duplicate, :key} registration --- lib/elixir/lib/registry.ex | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index e8d3cfff6c3..6d24a41f7b1 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1180,7 +1180,14 @@ defmodule Registry do counter = System.unique_integer() true = :ets.insert(pid_ets, {self, key, key_ets, counter}) - case register_key(kind, key_ets, key, {key, {self, value}}) do + key_entry = + if ordered?(kind) do + {{key, self, counter}, value} + else + {key, {self, value}} + end + + case register_key(kind, key_ets, key, key_entry) do :ok -> for listener <- listeners do Kernel.send(listener, {:register, registry, key, self, value}) @@ -1623,6 +1630,9 @@ defmodule Registry do defp reserved_atom?("_"), do: true defp reserved_atom?("$" <> _), do: true defp reserved_atom?(_), do: false + + defp ordered?({:duplicate, :key}), do: true + defp ordered?(_), do: false end defmodule Registry.Supervisor do From 5c9527d1a8176cee02e84a5d5b5fe02eaa0c7fec Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:47:37 +0100 Subject: [PATCH 03/23] Use ordered_lookup_second for {:duplicate, :key} lookups Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 48 +++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 6d24a41f7b1..096dabcc81d 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -554,12 +554,22 @@ defmodule Registry do |> List.wrap() |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) - {{:duplicate, _}, 1, key_ets} -> + {{:duplicate, :key}, 1, key_ets} -> + key_ets + |> ordered_lookup_second(key) + |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) + + {{:duplicate, :pid}, 1, key_ets} -> key_ets |> safe_lookup_second(key) |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) - {{:duplicate, _}, partitions, _} -> + {{:duplicate, :key}, partitions, _} -> + key_ets!(registry, key, partitions) + |> ordered_lookup_second(key) + |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) + + {{:duplicate, :pid}, partitions, _} -> if Keyword.get(opts, :parallel, false) do registry |> dispatch_parallel(key, mfa_or_fun, partitions) @@ -671,12 +681,14 @@ defmodule Registry do [] end - {{:duplicate, _}, 1, key_ets} -> + {{:duplicate, :key}, 1, key_ets} -> + ordered_lookup_second(key_ets, key) + + {{:duplicate, :pid}, 1, key_ets} -> safe_lookup_second(key_ets, key) {{:duplicate, :key}, partitions, _key_ets} -> - partition = hash(key, partitions) - safe_lookup_second(key_ets!(registry, partition), key) + ordered_lookup_second(key_ets!(registry, key, partitions), key) {{:duplicate, :pid}, partitions, _key_ets} -> for partition <- 0..(partitions - 1), @@ -951,13 +963,15 @@ defmodule Registry do [] end - {{:duplicate, _}, 1, key_ets} -> + {{:duplicate, :key}, 1, key_ets} -> + for {^pid, value} <- ordered_lookup_second(key_ets, key), do: value + + {{:duplicate, :pid}, 1, key_ets} -> for {^pid, value} <- safe_lookup_second(key_ets, key), do: value {{:duplicate, :key}, partitions, _key_ets} -> - partition = hash(key, partitions) - key_ets = key_ets!(registry, partition) - for {^pid, value} <- safe_lookup_second(key_ets, key), do: value + key_ets = key_ets!(registry, key, partitions) + for {^pid, value} <- ordered_lookup_second(key_ets, key), do: value {{:duplicate, :pid}, partitions, _key_ets} -> partition = hash(pid, partitions) @@ -1593,6 +1607,22 @@ defmodule Registry do end end + defp ordered_lookup_second(ets, key) do + spec = + if is_atom(key) and reserved_atom?(Atom.to_string(key)) do + guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} + [{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}] + else + [{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}] + end + + try do + :ets.select(ets, spec) + catch + :error, :badarg -> [] + end + end + defp partitions(:unique, key, pid, partitions) do {hash(key, partitions), hash(pid, partitions)} end From 014dc9f9ae599833a0205ee2819a3468df448088 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:49:04 +0100 Subject: [PATCH 04/23] Use ordered_unregister_key for {:duplicate, :key} unregistration Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 096dabcc81d..c31e478886b 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1030,7 +1030,13 @@ defmodule Registry do # Remove first from the key_ets because in case of crashes # the pid_ets will still be able to clean up. The last step is # to clean if we have no more entries. - true = __unregister__(key_ets, {key, {self, :_}}, 1) + true = + if ordered?(kind) do + ordered_unregister_key(key_ets, key, self) + else + __unregister__(key_ets, {key, {self, :_}}, 1) + end + true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) unlink_if_unregistered(pid_server, pid_ets, self) @@ -1657,6 +1663,17 @@ defmodule Registry do end end + @doc false + def ordered_unregister_key(key_ets, key, pid) do + if is_atom(key) and reserved_atom?(Atom.to_string(key)) do + guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} + pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}} + :ets.select_delete(key_ets, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0 + else + :ets.match_delete(key_ets, {{key, pid, :_}, :_}) + end + end + defp reserved_atom?("_"), do: true defp reserved_atom?("$" <> _), do: true defp reserved_atom?(_), do: false @@ -1843,7 +1860,11 @@ defmodule Registry.Partition do end try do - Registry.__unregister__(key_ets, {key, {pid, :_}}, 1) + if ordered do + Registry.ordered_unregister_key(key_ets, key, pid) + else + Registry.__unregister__(key_ets, {key, {pid, :_}}, 1) + end catch :error, :badarg -> :badarg end From 47ed260b7041aa8957d3affd18447b776bb2da53 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:50:47 +0100 Subject: [PATCH 05/23] Update match/4 and count_match/4 for ordered_set key_ets Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 68 ++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index c31e478886b..df258c5ff5a 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -803,18 +803,29 @@ defmodule Registry do @doc since: "1.4.0" @spec match(registry, key, match_pattern, guards) :: [{pid, term}] def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] - case key_info!(registry) do {:unique, partitions, key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] key_ets = key_ets || key_ets!(registry, key, partitions) :ets.select(key_ets, spec) - {{:duplicate, _}, 1, key_ets} -> + {{:duplicate, :key}, 1, key_ets} -> + spec = ordered_match_spec(key, pattern, guards) + :ets.select(key_ets, spec) + + {{:duplicate, :key}, partitions, _key_ets} -> + spec = ordered_match_spec(key, pattern, guards) + :ets.select(key_ets!(registry, key, partitions), spec) + + {{:duplicate, :pid}, 1, key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] :ets.select(key_ets, spec) - {{:duplicate, _}, partitions, _key_ets} -> + {{:duplicate, :pid}, partitions, _key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] for partition <- 0..(partitions - 1), pair <- :ets.select(key_ets!(registry, partition), spec), do: pair @@ -1436,18 +1447,29 @@ defmodule Registry do @spec count_match(registry, key, match_pattern, guards) :: non_neg_integer() def count_match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [true]}] - case key_info!(registry) do {:unique, partitions, key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [true]}] key_ets = key_ets || key_ets!(registry, key, partitions) :ets.select_count(key_ets, spec) - {{:duplicate, _}, 1, key_ets} -> + {{:duplicate, :key}, 1, key_ets} -> + spec = ordered_count_match_spec(key, pattern, guards) + :ets.select_count(key_ets, spec) + + {{:duplicate, :key}, partitions, _key_ets} -> + spec = ordered_count_match_spec(key, pattern, guards) + :ets.select_count(key_ets!(registry, key, partitions), spec) + + {{:duplicate, :pid}, 1, key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [true]}] :ets.select_count(key_ets, spec) - {{:duplicate, _}, partitions, _key_ets} -> + {{:duplicate, :pid}, partitions, _key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [true]}] Enum.sum_by(0..(partitions - 1), fn partition_index -> :ets.select_count(key_ets!(registry, partition_index), spec) end) @@ -1674,6 +1696,32 @@ defmodule Registry do end end + defp ordered_match_spec(key, pattern, guards) do + body = [{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}] + + if is_atom(key) and reserved_atom?(Atom.to_string(key)) do + guards = [ + {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards + ] + + [{{{:_, :_, :_}, pattern}, guards, body}] + else + [{{{key, :_, :_}, pattern}, guards, body}] + end + end + + defp ordered_count_match_spec(key, pattern, guards) do + if is_atom(key) and reserved_atom?(Atom.to_string(key)) do + guards = [ + {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards + ] + + [{{{:_, :_, :_}, pattern}, guards, [true]}] + else + [{{{key, :_, :_}, pattern}, guards, [true]}] + end + end + defp reserved_atom?("_"), do: true defp reserved_atom?("$" <> _), do: true defp reserved_atom?(_), do: false From 56c6c149439e3e514fe5a41cfef70cdb02cbb9d7 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:52:34 +0100 Subject: [PATCH 06/23] Update select/2 and count_select/2 for ordered_set key_ets Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 52 ++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index df258c5ff5a..ecb5dfd2627 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1528,15 +1528,16 @@ defmodule Registry do @spec select(registry, spec) :: [term] def select(registry, spec) when is_atom(registry) and is_list(spec) do - spec = group_match_headers(spec, __ENV__.function) - case key_info!(registry) do - {_kind, partitions, nil} -> + {kind, partitions, nil} -> + spec = group_match_headers(spec, kind, __ENV__.function) + Enum.flat_map(0..(partitions - 1), fn partition_index -> :ets.select(key_ets!(registry, partition_index), spec) end) - {_kind, 1, key_ets} -> + {kind, 1, key_ets} -> + spec = group_match_headers(spec, kind, __ENV__.function) :ets.select(key_ets, spec) end end @@ -1559,24 +1560,32 @@ defmodule Registry do @spec count_select(registry, spec) :: non_neg_integer() def count_select(registry, spec) when is_atom(registry) and is_list(spec) do - spec = group_match_headers(spec, __ENV__.function) - case key_info!(registry) do - {_kind, partitions, nil} -> + {kind, partitions, nil} -> + spec = group_match_headers(spec, kind, __ENV__.function) + Enum.sum_by(0..(partitions - 1), fn partition_index -> :ets.select_count(key_ets!(registry, partition_index), spec) end) - {_kind, 1, key_ets} -> + {kind, 1, key_ets} -> + spec = group_match_headers(spec, kind, __ENV__.function) :ets.select_count(key_ets, spec) end end - defp group_match_headers(spec, {fun, arity}) do + defp group_match_headers(spec, kind, {fun, arity}) do + ordered = ordered?(kind) + for part <- spec do case part do - {{key, pid, value}, guards, select} -> - {{key, {pid, value}}, guards, select} + {{key, pid, value}, guards, body} when ordered -> + guards = ordered_rewrite(guards) + body = ordered_rewrite(body) + {{{key, pid, :_}, value}, guards, body} + + {{key, pid, value}, guards, body} -> + {{key, {pid, value}}, guards, body} _ -> raise ArgumentError, @@ -1585,6 +1594,27 @@ defmodule Registry do end end + defp ordered_rewrite(term) when is_tuple(term) do + ordered_rewrite_tuple(term) + end + + defp ordered_rewrite(term) when is_list(term) do + Enum.map(term, &ordered_rewrite/1) + end + + defp ordered_rewrite(term), do: term + + defp ordered_rewrite_tuple({:element, 1, :"$_"}) do + {:element, 1, {:element, 1, :"$_"}} + end + + defp ordered_rewrite_tuple(tuple) do + tuple + |> Tuple.to_list() + |> Enum.map(&ordered_rewrite/1) + |> List.to_tuple() + end + ## Helpers @compile {:inline, hash: 2} From 009d93b6ecc62f24cdeae6e96b63b6fe9a204e09 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:54:34 +0100 Subject: [PATCH 07/23] Update unregister_match/4 for ordered_set key_ets Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 71 +++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index ecb5dfd2627..9621becd6dd 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1107,20 +1107,23 @@ defmodule Registry do key_ets = key_ets || key_ets!(registry, key_partition) {pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition) - # Remove first from the key_ets because in case of crashes - # the pid_ets will still be able to clean up. The last step is - # to clean if we have no more entries. + if ordered?(kind) do + ordered_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) + else + bag_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) + end + + :ok + end - # Here we want to count all entries for this pid under this key, regardless of pattern. + defp bag_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) do underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}} total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}] total = :ets.select_count(key_ets, total_spec) - # We only want to delete things that match the pattern delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}] case :ets.select_delete(key_ets, delete_spec) do - # We deleted everything, we can just delete the object ^total -> true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) unlink_if_unregistered(pid_server, pid_ets, self) @@ -1133,10 +1136,6 @@ defmodule Registry do :ok deleted -> - # There are still entries remaining for this pid. delete_object/2 with - # duplicate_bag tables will remove every entry, but we only want to - # remove those we have deleted. The solution is to introduce a temp_entry - # that indicates how many keys WILL be remaining after the delete operation. counter = System.unique_integer() remaining = total - deleted temp_entry = {self, key, {key_ets, remaining}, counter} @@ -1144,12 +1143,58 @@ defmodule Registry do true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) real_keys = List.duplicate({self, key, key_ets, counter}, remaining) true = :ets.insert(pid_ets, real_keys) - # We've recreated the real remaining key entries, so we can now delete - # our temporary entry. true = :ets.delete_object(pid_ets, temp_entry) end + end - :ok + defp ordered_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) do + total_spec = ordered_unregister_match_total_spec(key, self) + total = :ets.select_count(key_ets, total_spec) + + delete_spec = ordered_unregister_match_delete_spec(key, self, pattern, guards) + + case :ets.select_delete(key_ets, delete_spec) do + ^total -> + true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) + unlink_if_unregistered(pid_server, pid_ets, self) + + for listener <- listeners do + Kernel.send(listener, {:unregister, registry, key, self}) + end + + 0 -> + :ok + + deleted -> + counter = System.unique_integer() + remaining = total - deleted + temp_entry = {self, key, {key_ets, remaining}, counter} + true = :ets.insert(pid_ets, temp_entry) + true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) + real_keys = List.duplicate({self, key, key_ets, counter}, remaining) + true = :ets.insert(pid_ets, real_keys) + true = :ets.delete_object(pid_ets, temp_entry) + end + end + + defp ordered_unregister_match_total_spec(key, self) do + if is_atom(key) and reserved_atom?(Atom.to_string(key)) do + guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} + pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}} + [{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}] + else + [{{{key, self, :_}, :_}, [], [true]}] + end + end + + defp ordered_unregister_match_delete_spec(key, self, pattern, guards) do + if is_atom(key) and reserved_atom?(Atom.to_string(key)) do + guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} + pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}} + [{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}] + else + [{{{key, self, :_}, pattern}, guards, [true]}] + end end @doc """ From 65dfef14ab944a1d3ae0147597be02f76dd3de70 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Fri, 27 Mar 2026 16:58:57 +0100 Subject: [PATCH 08/23] Fix match spec body tuple wrapping and parallel dispatch The match spec body needs triple braces {{{A, B}}} in Elixir to produce a tuple result in ETS. Also preserve parallel dispatch contract for {:duplicate, :key} when parallel: true option is passed. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 9621becd6dd..03a14b50c70 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -565,9 +565,25 @@ defmodule Registry do |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) {{:duplicate, :key}, partitions, _} -> - key_ets!(registry, key, partitions) - |> ordered_lookup_second(key) - |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) + if Keyword.get(opts, :parallel, false) do + parent = self() + + task = + Task.async(fn -> + key_ets!(registry, key, partitions) + |> ordered_lookup_second(key) + |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) + + Process.unlink(parent) + :ok + end) + + Task.await(task, :infinity) + else + key_ets!(registry, key, partitions) + |> ordered_lookup_second(key) + |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) + end {{:duplicate, :pid}, partitions, _} -> if Keyword.get(opts, :parallel, false) do @@ -1772,7 +1788,7 @@ defmodule Registry do end defp ordered_match_spec(key, pattern, guards) do - body = [{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}] + body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}] if is_atom(key) and reserved_atom?(Atom.to_string(key)) do guards = [ From 4c476f5467c32d536f03dd3230b89d99da5467e5 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 16:00:10 +0200 Subject: [PATCH 09/23] Remove unnecessary parallel dispatch for {:duplicate, :key} All entries for a given key are in a single partition, so parallel dispatch has no effect. Simplify by always dispatching in-process and document this in the :parallel option. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 28 ++++++------------- .../test/elixir/registry/duplicate_test.exs | 10 +++++-- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 03a14b50c70..3df8006e13e 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -532,13 +532,17 @@ defmodule Registry do given as an option, the dispatching happens in parallel. In both cases, the callback is only invoked if there are entries for that partition. + For `{:duplicate, :key}` registries, all entries for a given key are in a + single partition, so the `:parallel` option has no effect. + See the module documentation for examples of using the `dispatch/3` function for building custom dispatching or a pubsub system. ## Options * `:parallel` - if `true`, the dispatching is done in parallel - across all partitions. Defaults to `false`. + across all partitions. Only meaningful for `{:duplicate, :pid}` + registries. Defaults to `false`. """ @doc since: "1.4.0" @@ -565,25 +569,9 @@ defmodule Registry do |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) {{:duplicate, :key}, partitions, _} -> - if Keyword.get(opts, :parallel, false) do - parent = self() - - task = - Task.async(fn -> - key_ets!(registry, key, partitions) - |> ordered_lookup_second(key) - |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) - - Process.unlink(parent) - :ok - end) - - Task.await(task, :infinity) - else - key_ets!(registry, key, partitions) - |> ordered_lookup_second(key) - |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) - end + key_ets!(registry, key, partitions) + |> ordered_lookup_second(key) + |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) {{:duplicate, :pid}, partitions, _} -> if Keyword.get(opts, :parallel, false) do diff --git a/lib/elixir/test/elixir/registry/duplicate_test.exs b/lib/elixir/test/elixir/registry/duplicate_test.exs index 00f1c6191a5..fe431adb70e 100644 --- a/lib/elixir/test/elixir/registry/duplicate_test.exs +++ b/lib/elixir/test/elixir/registry/duplicate_test.exs @@ -113,10 +113,14 @@ defmodule Registry.DuplicateTest do end test "dispatches to multiple keys in parallel", context do - %{registry: registry, partitions: partitions} = context + %{registry: registry, keys: keys, partitions: partitions} = context Process.flag(:trap_exit, true) parent = self() + # {:duplicate, :key} dispatches from a single partition, + # so parallel: true has no effect. + parallel? = partitions == 8 and keys != {:duplicate, :key} + fun = fn _ -> raise "will never be invoked" end assert Registry.dispatch(registry, "hello", fun, parallel: true) == :ok @@ -125,7 +129,7 @@ defmodule Registry.DuplicateTest do {:ok, _} = Registry.register(registry, "world", :value3) fun = fn entries -> - if partitions == 8 do + if parallel? do assert parent != self() else assert parent == self() @@ -141,7 +145,7 @@ defmodule Registry.DuplicateTest do refute_received {:dispatch, :value3} fun = fn entries -> - if partitions == 8 do + if parallel? do assert parent != self() else assert parent == self() From 6cf77281c14e79e9b12384e38479120357dd5689 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 16:13:54 +0200 Subject: [PATCH 10/23] Reduce duplication in match/4 and count_match/4 Handle {:duplicate, :key} (ordered) first, then {:duplicate, :pid} multi-partition, then collapse the remaining cases (:unique and {:duplicate, :pid} single-partition) into a single branch that builds the bag spec once. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 48 ++++++++++++++------------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 3df8006e13e..37b90554ec2 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -808,31 +808,25 @@ defmodule Registry do @spec match(registry, key, match_pattern, guards) :: [{pid, term}] def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do case key_info!(registry) do - {:unique, partitions, key_ets} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] - key_ets = key_ets || key_ets!(registry, key, partitions) - :ets.select(key_ets, spec) - {{:duplicate, :key}, 1, key_ets} -> - spec = ordered_match_spec(key, pattern, guards) - :ets.select(key_ets, spec) + :ets.select(key_ets, ordered_match_spec(key, pattern, guards)) {{:duplicate, :key}, partitions, _key_ets} -> - spec = ordered_match_spec(key, pattern, guards) - :ets.select(key_ets!(registry, key, partitions), spec) + :ets.select(key_ets!(registry, key, partitions), ordered_match_spec(key, pattern, guards)) - {{:duplicate, :pid}, 1, key_ets} -> + {{:duplicate, :pid}, partitions, nil} -> guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] - :ets.select(key_ets, spec) - {{:duplicate, :pid}, partitions, _key_ets} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] for partition <- 0..(partitions - 1), pair <- :ets.select(key_ets!(registry, partition), spec), do: pair + + {_kind, partitions, key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] + key_ets = key_ets || key_ets!(registry, key, partitions) + :ets.select(key_ets, spec) end end @@ -1497,31 +1491,25 @@ defmodule Registry do def count_match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do case key_info!(registry) do - {:unique, partitions, key_ets} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [true]}] - key_ets = key_ets || key_ets!(registry, key, partitions) - :ets.select_count(key_ets, spec) - {{:duplicate, :key}, 1, key_ets} -> - spec = ordered_count_match_spec(key, pattern, guards) - :ets.select_count(key_ets, spec) + :ets.select_count(key_ets, ordered_count_match_spec(key, pattern, guards)) {{:duplicate, :key}, partitions, _key_ets} -> - spec = ordered_count_match_spec(key, pattern, guards) - :ets.select_count(key_ets!(registry, key, partitions), spec) + :ets.select_count(key_ets!(registry, key, partitions), ordered_count_match_spec(key, pattern, guards)) - {{:duplicate, :pid}, 1, key_ets} -> + {{:duplicate, :pid}, partitions, nil} -> guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] spec = [{{:_, {:_, pattern}}, guards, [true]}] - :ets.select_count(key_ets, spec) - {{:duplicate, :pid}, partitions, _key_ets} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [true]}] Enum.sum_by(0..(partitions - 1), fn partition_index -> :ets.select_count(key_ets!(registry, partition_index), spec) end) + + {_kind, partitions, key_ets} -> + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + spec = [{{:_, {:_, pattern}}, guards, [true]}] + key_ets = key_ets || key_ets!(registry, key, partitions) + :ets.select_count(key_ets, spec) end end From 3e46f6849cea7b478b30a6b230ba3e194d3a81b9 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 16:22:17 +0200 Subject: [PATCH 11/23] Unify __unregister__ to handle both bag and ordered key_ets Add an optional `ordered` parameter to __unregister__/4 so callers don't need to branch between __unregister__ and ordered_unregister_key. When ordered is true and pos is 1, the function builds the composite key match pattern internally. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 37b90554ec2..52df7db5b5e 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1039,12 +1039,7 @@ defmodule Registry do # Remove first from the key_ets because in case of crashes # the pid_ets will still be able to clean up. The last step is # to clean if we have no more entries. - true = - if ordered?(kind) do - ordered_unregister_key(key_ets, key, self) - else - __unregister__(key_ets, {key, {self, :_}}, 1) - end + true = __unregister__(key_ets, {key, {self, :_}}, 1, ordered?(kind)) true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) @@ -1739,7 +1734,19 @@ defmodule Registry do end @doc false - def __unregister__(table, match, pos) do + def __unregister__(table, match, pos, ordered \\ false) + + def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do + if is_atom(key) and reserved_atom?(Atom.to_string(key)) do + guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} + pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}} + :ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0 + else + :ets.match_delete(table, {{key, pid, :_}, :_}) + end + end + + def __unregister__(table, match, pos, false = _ordered) do key = :erlang.element(pos, match) # We need to perform an element comparison if we have a special atom key. @@ -1752,17 +1759,6 @@ defmodule Registry do end end - @doc false - def ordered_unregister_key(key_ets, key, pid) do - if is_atom(key) and reserved_atom?(Atom.to_string(key)) do - guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} - pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}} - :ets.select_delete(key_ets, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0 - else - :ets.match_delete(key_ets, {{key, pid, :_}, :_}) - end - end - defp ordered_match_spec(key, pattern, guards) do body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}] @@ -1975,11 +1971,7 @@ defmodule Registry.Partition do end try do - if ordered do - Registry.ordered_unregister_key(key_ets, key, pid) - else - Registry.__unregister__(key_ets, {key, {pid, :_}}, 1) - end + Registry.__unregister__(key_ets, {key, {pid, :_}}, 1, ordered) catch :error, :badarg -> :badarg end From d78f3399e9372a17c41c38590e587497451ce2d8 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 16:24:45 +0200 Subject: [PATCH 12/23] Collapse unregister_match into single function with spec builder Remove the duplicated bag_unregister_match/ordered_unregister_match 9-argument functions. The only difference was the match specs, so extract a single unregister_match_specs/5 that returns {total_spec, delete_spec} for both ordered and bag formats. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 70 +++++++++----------------------------- 1 file changed, 17 insertions(+), 53 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 52df7db5b5e..2659b8feb2f 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1100,22 +1100,14 @@ defmodule Registry do key_ets = key_ets || key_ets!(registry, key_partition) {pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition) - if ordered?(kind) do - ordered_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) - else - bag_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) - end - - :ok - end + # Remove first from the key_ets because in case of crashes + # the pid_ets will still be able to clean up. The last step is + # to clean if we have no more entries. - defp bag_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) do - underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}} - total_spec = [{{:_, {self, :_}}, [underscore_guard], [true]}] + # Here we want to count all entries for this pid under this key, regardless of pattern. + {total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards) total = :ets.select_count(key_ets, total_spec) - delete_spec = [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}] - case :ets.select_delete(key_ets, delete_spec) do ^total -> true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) @@ -1138,56 +1130,28 @@ defmodule Registry do true = :ets.insert(pid_ets, real_keys) true = :ets.delete_object(pid_ets, temp_entry) end - end - - defp ordered_unregister_match(key, self, pattern, guards, key_ets, pid_server, pid_ets, registry, listeners) do - total_spec = ordered_unregister_match_total_spec(key, self) - total = :ets.select_count(key_ets, total_spec) - - delete_spec = ordered_unregister_match_delete_spec(key, self, pattern, guards) - - case :ets.select_delete(key_ets, delete_spec) do - ^total -> - true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) - unlink_if_unregistered(pid_server, pid_ets, self) - - for listener <- listeners do - Kernel.send(listener, {:unregister, registry, key, self}) - end - 0 -> - :ok - - deleted -> - counter = System.unique_integer() - remaining = total - deleted - temp_entry = {self, key, {key_ets, remaining}, counter} - true = :ets.insert(pid_ets, temp_entry) - true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) - real_keys = List.duplicate({self, key, key_ets, counter}, remaining) - true = :ets.insert(pid_ets, real_keys) - true = :ets.delete_object(pid_ets, temp_entry) - end + :ok end - defp ordered_unregister_match_total_spec(key, self) do + defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do if is_atom(key) and reserved_atom?(Atom.to_string(key)) do guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}} - [{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}] + + {[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}], + [{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]} else - [{{{key, self, :_}, :_}, [], [true]}] + {[{{{key, self, :_}, :_}, [], [true]}], + [{{{key, self, :_}, pattern}, guards, [true]}]} end end - defp ordered_unregister_match_delete_spec(key, self, pattern, guards) do - if is_atom(key) and reserved_atom?(Atom.to_string(key)) do - guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} - pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}} - [{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}] - else - [{{{key, self, :_}, pattern}, guards, [true]}] - end + defp unregister_match_specs(_kind, key, self, pattern, guards) do + underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}} + + {[{{:_, {self, :_}}, [underscore_guard], [true]}], + [{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]} end @doc """ From 9e3da790cb61168bfcd1559702782c3ee115503d Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 16:45:25 +0200 Subject: [PATCH 13/23] Add comment explaining ordered_rewrite for select specs The rewrite is needed because existing code (including tests) uses {:element, 1, :"$_"} in select body to extract the key. Without rewriting, this returns the composite tuple {key, pid, counter} instead of just the key in ordered_set tables. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 2659b8feb2f..345adeecb8e 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1590,6 +1590,9 @@ defmodule Registry do end end + # In duplicate_bag, :"$_" is {key, {pid, value}}, so {:element, 1, :"$_"} = key. + # In ordered_set, :"$_" is {{key, pid, counter}, value}, so we need + # {:element, 1, {:element, 1, :"$_"}} to reach the key. defp ordered_rewrite(term) when is_tuple(term) do ordered_rewrite_tuple(term) end From e40af6e22b02c677f71fc9adc4e81c562444e3b1 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 17:00:59 +0200 Subject: [PATCH 14/23] Remove ordered_rewrite from select/count_select Users of {:duplicate, :key} registries should use capture variables (:"$1", :"$2") instead of :"$_" in select specs. The internal ETS layout differs between registry kinds, and we don't paper over that. Updated docs to note this explicitly and fixed the test that relied on {:element, 1, :"$_"} to use capture variables instead. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 29 ++----------------- .../test/elixir/registry/duplicate_test.exs | 4 +-- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 345adeecb8e..e330e4d7595 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1497,6 +1497,9 @@ defmodule Registry do some operations like `:element` to modify the output format. Do not use special match variables `:"$_"` and `:"$$"`, because they might not work as expected. + In particular, `{:duplicate, :key}` registries use a different internal ETS layout, so match specs + that reference the underlying entry structure via `:"$_"` will return different results. + Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead. Note that for large registries with many partitions this will be costly as it builds the result by concatenating all the partitions. @@ -1576,8 +1579,6 @@ defmodule Registry do for part <- spec do case part do {{key, pid, value}, guards, body} when ordered -> - guards = ordered_rewrite(guards) - body = ordered_rewrite(body) {{{key, pid, :_}, value}, guards, body} {{key, pid, value}, guards, body} -> @@ -1590,30 +1591,6 @@ defmodule Registry do end end - # In duplicate_bag, :"$_" is {key, {pid, value}}, so {:element, 1, :"$_"} = key. - # In ordered_set, :"$_" is {{key, pid, counter}, value}, so we need - # {:element, 1, {:element, 1, :"$_"}} to reach the key. - defp ordered_rewrite(term) when is_tuple(term) do - ordered_rewrite_tuple(term) - end - - defp ordered_rewrite(term) when is_list(term) do - Enum.map(term, &ordered_rewrite/1) - end - - defp ordered_rewrite(term), do: term - - defp ordered_rewrite_tuple({:element, 1, :"$_"}) do - {:element, 1, {:element, 1, :"$_"}} - end - - defp ordered_rewrite_tuple(tuple) do - tuple - |> Tuple.to_list() - |> Enum.map(&ordered_rewrite/1) - |> List.to_tuple() - end - ## Helpers @compile {:inline, hash: 2} diff --git a/lib/elixir/test/elixir/registry/duplicate_test.exs b/lib/elixir/test/elixir/registry/duplicate_test.exs index fe431adb70e..c1d81a226fa 100644 --- a/lib/elixir/test/elixir/registry/duplicate_test.exs +++ b/lib/elixir/test/elixir/registry/duplicate_test.exs @@ -428,8 +428,8 @@ defmodule Registry.DuplicateTest do assert ["hello", "world"] == Registry.select(registry, [ - {{"hello", :_, :_}, [], [{:element, 1, :"$_"}]}, - {{"world", :_, :_}, [], [{:element, 1, :"$_"}]} + {{:"$1", :_, :_}, [{:"=:=", :"$1", {:const, "hello"}}], [:"$1"]}, + {{:"$1", :_, :_}, [{:"=:=", :"$1", {:const, "world"}}], [:"$1"]} ]) |> Enum.sort() end From 10f3321058df1851f2a3a71b6a44a1a0d11a3303 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 17:12:35 +0200 Subject: [PATCH 15/23] Document {:duplicate, :key} ETS layout difference in start_link docs Users switching from {:duplicate, :pid} to {:duplicate, :key} should know that match specs using :"$_" in select/count_select may behave differently due to the ordered_set internal layout. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index e330e4d7595..088548a1a0f 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -369,7 +369,10 @@ defmodule Registry do * `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across many different keys (e.g., many topics with few subscribers each). This makes key-based lookups more efficient as they only need to check a single partition - instead of all partitions. + instead of all partitions. This option uses a different internal ETS table type + (`ordered_set` instead of `duplicate_bag`), which means match specs passed to + `select/2` and `count_select/2` that reference `:"$_"` may behave differently. + Use named match variables (`:"$1"`, `:"$2"`, etc.) instead. """ @doc since: "1.5.0" From 7e756411a4876872e8c450e480a646c8fe08eaff Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 17:14:47 +0200 Subject: [PATCH 16/23] Restore comment explaining total_spec vs delete_spec Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 088548a1a0f..12e6329288c 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1111,7 +1111,9 @@ defmodule Registry do {total_spec, delete_spec} = unregister_match_specs(kind, key, self, pattern, guards) total = :ets.select_count(key_ets, total_spec) + # We only want to delete things that match the pattern case :ets.select_delete(key_ets, delete_spec) do + # We deleted everything, we can just delete the object ^total -> true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) unlink_if_unregistered(pid_server, pid_ets, self) @@ -1124,6 +1126,10 @@ defmodule Registry do :ok deleted -> + # There are still entries remaining for this pid. delete_object/2 with + # duplicate_bag tables will remove every entry, but we only want to + # remove those we have deleted. The solution is to introduce a temp_entry + # that indicates how many keys WILL be remaining after the delete operation. counter = System.unique_integer() remaining = total - deleted temp_entry = {self, key, {key_ets, remaining}, counter} @@ -1131,6 +1137,8 @@ defmodule Registry do true = __unregister__(pid_ets, {self, key, key_ets, :_}, 2) real_keys = List.duplicate({self, key, key_ets, counter}, remaining) true = :ets.insert(pid_ets, real_keys) + # We've recreated the real remaining key entries, so we can now delete + # our temporary entry. true = :ets.delete_object(pid_ets, temp_entry) end From efaeecc7115be9205df2f6ab6c4a4b48659a1e2a Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 17:32:42 +0200 Subject: [PATCH 17/23] Unify safe_lookup_second and ordered_lookup_second into lookup_second/3 Replace the two separate lookup helpers with a single lookup_second/3 that dispatches on kind. This also allows collapsing the single-partition {:duplicate, :key} and {:duplicate, :pid} branches in dispatch, lookup, and values. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 77 ++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 12e6329288c..de0ba3fdbbf 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -258,7 +258,7 @@ defmodule Registry do {:unique, partitions, key_ets} -> key_ets = key_ets || key_ets!(registry, key, partitions) - case safe_lookup_second(key_ets, key) do + case lookup_second(:unique, key_ets, key) do {pid, _} -> if Process.alive?(pid), do: pid, else: :undefined @@ -556,24 +556,21 @@ defmodule Registry do when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do case key_info!(registry) do {:unique, partitions, key_ets} -> - (key_ets || key_ets!(registry, key, partitions)) - |> safe_lookup_second(key) - |> List.wrap() - |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) + key_ets = key_ets || key_ets!(registry, key, partitions) - {{:duplicate, :key}, 1, key_ets} -> - key_ets - |> ordered_lookup_second(key) + :unique + |> lookup_second(key_ets, key) + |> List.wrap() |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) - {{:duplicate, :pid}, 1, key_ets} -> - key_ets - |> safe_lookup_second(key) + {{:duplicate, _} = kind, 1, key_ets} -> + kind + |> lookup_second(key_ets, key) |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) {{:duplicate, :key}, partitions, _} -> - key_ets!(registry, key, partitions) - |> ordered_lookup_second(key) + {:duplicate, :key} + |> lookup_second(key_ets!(registry, key, partitions), key) |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) {{:duplicate, :pid}, partitions, _} -> @@ -596,9 +593,8 @@ defmodule Registry do defp dispatch_serial(registry, key, mfa_or_fun, partition) do partition = partition - 1 - registry - |> key_ets!(partition) - |> safe_lookup_second(key) + {:duplicate, :pid} + |> lookup_second(key_ets!(registry, partition), key) |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) dispatch_serial(registry, key, mfa_or_fun, partition) @@ -614,9 +610,8 @@ defmodule Registry do task = Task.async(fn -> - registry - |> key_ets!(partition) - |> safe_lookup_second(key) + {:duplicate, :pid} + |> lookup_second(key_ets!(registry, partition), key) |> apply_non_empty_to_mfa_or_fun(mfa_or_fun) Process.unlink(parent) @@ -680,7 +675,7 @@ defmodule Registry do {:unique, partitions, key_ets} -> key_ets = key_ets || key_ets!(registry, key, partitions) - case safe_lookup_second(key_ets, key) do + case lookup_second(:unique, key_ets, key) do {_, _} = pair -> [pair] @@ -688,18 +683,15 @@ defmodule Registry do [] end - {{:duplicate, :key}, 1, key_ets} -> - ordered_lookup_second(key_ets, key) - - {{:duplicate, :pid}, 1, key_ets} -> - safe_lookup_second(key_ets, key) + {{:duplicate, _} = kind, 1, key_ets} -> + lookup_second(kind, key_ets, key) {{:duplicate, :key}, partitions, _key_ets} -> - ordered_lookup_second(key_ets!(registry, key, partitions), key) + lookup_second({:duplicate, :key}, key_ets!(registry, key, partitions), key) {{:duplicate, :pid}, partitions, _key_ets} -> for partition <- 0..(partitions - 1), - pair <- safe_lookup_second(key_ets!(registry, partition), key), + pair <- lookup_second({:duplicate, :pid}, key_ets!(registry, partition), key), do: pair end end @@ -967,7 +959,7 @@ defmodule Registry do {:unique, partitions, key_ets} -> key_ets = key_ets || key_ets!(registry, key, partitions) - case safe_lookup_second(key_ets, key) do + case lookup_second(:unique, key_ets, key) do {^pid, value} -> [value] @@ -975,20 +967,17 @@ defmodule Registry do [] end - {{:duplicate, :key}, 1, key_ets} -> - for {^pid, value} <- ordered_lookup_second(key_ets, key), do: value - - {{:duplicate, :pid}, 1, key_ets} -> - for {^pid, value} <- safe_lookup_second(key_ets, key), do: value + {{:duplicate, _} = kind, 1, key_ets} -> + for {^pid, value} <- lookup_second(kind, key_ets, key), do: value {{:duplicate, :key}, partitions, _key_ets} -> key_ets = key_ets!(registry, key, partitions) - for {^pid, value} <- ordered_lookup_second(key_ets, key), do: value + for {^pid, value} <- lookup_second({:duplicate, :key}, key_ets, key), do: value {{:duplicate, :pid}, partitions, _key_ets} -> partition = hash(pid, partitions) key_ets = key_ets!(registry, partition) - for {^pid, value} <- safe_lookup_second(key_ets, key), do: value + for {^pid, value} <- lookup_second({:duplicate, :pid}, key_ets, key), do: value end end @@ -1644,15 +1633,7 @@ defmodule Registry do :ets.lookup_element(registry, partition, 3) end - defp safe_lookup_second(ets, key) do - try do - :ets.lookup_element(ets, key, 2) - catch - :error, :badarg -> [] - end - end - - defp ordered_lookup_second(ets, key) do + defp lookup_second({:duplicate, :key}, ets, key) do spec = if is_atom(key) and reserved_atom?(Atom.to_string(key)) do guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} @@ -1668,6 +1649,14 @@ defmodule Registry do end end + defp lookup_second(_kind, ets, key) do + try do + :ets.lookup_element(ets, key, 2) + catch + :error, :badarg -> [] + end + end + defp partitions(:unique, key, pid, partitions) do {hash(key, partitions), hash(pid, partitions)} end From f68424d2811c71e44f55edaae3e68c5d28415fe4 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 17:34:02 +0200 Subject: [PATCH 18/23] Unify match spec helpers into match_spec/4 and count_match_spec/4 Replace ordered_match_spec/3 and ordered_count_match_spec/3 with match_spec/4 and count_match_spec/4 that dispatch on kind. The bag spec construction moves from inline in match/4 and count_match/4 into the helpers, simplifying both functions from 5 branches to 2. Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 42 ++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index de0ba3fdbbf..929936a4270 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -803,23 +803,15 @@ defmodule Registry do @spec match(registry, key, match_pattern, guards) :: [{pid, term}] def match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do case key_info!(registry) do - {{:duplicate, :key}, 1, key_ets} -> - :ets.select(key_ets, ordered_match_spec(key, pattern, guards)) - - {{:duplicate, :key}, partitions, _key_ets} -> - :ets.select(key_ets!(registry, key, partitions), ordered_match_spec(key, pattern, guards)) - {{:duplicate, :pid}, partitions, nil} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] + spec = match_spec({:duplicate, :pid}, key, pattern, guards) for partition <- 0..(partitions - 1), pair <- :ets.select(key_ets!(registry, partition), spec), do: pair - {_kind, partitions, key_ets} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] + {kind, partitions, key_ets} -> + spec = match_spec(kind, key, pattern, guards) key_ets = key_ets || key_ets!(registry, key, partitions) :ets.select(key_ets, spec) end @@ -1450,23 +1442,15 @@ defmodule Registry do def count_match(registry, key, pattern, guards \\ []) when is_atom(registry) and is_list(guards) do case key_info!(registry) do - {{:duplicate, :key}, 1, key_ets} -> - :ets.select_count(key_ets, ordered_count_match_spec(key, pattern, guards)) - - {{:duplicate, :key}, partitions, _key_ets} -> - :ets.select_count(key_ets!(registry, key, partitions), ordered_count_match_spec(key, pattern, guards)) - {{:duplicate, :pid}, partitions, nil} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [true]}] + spec = count_match_spec({:duplicate, :pid}, key, pattern, guards) Enum.sum_by(0..(partitions - 1), fn partition_index -> :ets.select_count(key_ets!(registry, partition_index), spec) end) - {_kind, partitions, key_ets} -> - guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] - spec = [{{:_, {:_, pattern}}, guards, [true]}] + {kind, partitions, key_ets} -> + spec = count_match_spec(kind, key, pattern, guards) key_ets = key_ets || key_ets!(registry, key, partitions) :ets.select_count(key_ets, spec) end @@ -1703,7 +1687,7 @@ defmodule Registry do end end - defp ordered_match_spec(key, pattern, guards) do + defp match_spec({:duplicate, :key}, key, pattern, guards) do body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}] if is_atom(key) and reserved_atom?(Atom.to_string(key)) do @@ -1717,7 +1701,12 @@ defmodule Registry do end end - defp ordered_count_match_spec(key, pattern, guards) do + defp match_spec(_kind, key, pattern, guards) do + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + [{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}] + end + + defp count_match_spec({:duplicate, :key}, key, pattern, guards) do if is_atom(key) and reserved_atom?(Atom.to_string(key)) do guards = [ {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards @@ -1729,6 +1718,11 @@ defmodule Registry do end end + defp count_match_spec(_kind, key, pattern, guards) do + guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards] + [{{:_, {:_, pattern}}, guards, [true]}] + end + defp reserved_atom?("_"), do: true defp reserved_atom?("$" <> _), do: true defp reserved_atom?(_), do: false From 88a961d0abeddb5bfcf009e44e4cef2f24f4c764 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Tue, 31 Mar 2026 18:06:14 +0200 Subject: [PATCH 19/23] Fix formatting in unregister_match_specs Assisted by AI Co-Authored-By: Claude --- lib/elixir/lib/registry.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 929936a4270..a67fbb6db6e 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1134,8 +1134,7 @@ defmodule Registry do {[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}], [{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]} else - {[{{{key, self, :_}, :_}, [], [true]}], - [{{{key, self, :_}, pattern}, guards, [true]}]} + {[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]} end end From 82a42a1cba670ce3489a2ef91d4ed9303fdff63d Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Wed, 15 Apr 2026 20:43:53 +0200 Subject: [PATCH 20/23] Add layout comment and tricky-key tests for match/count_match --- lib/elixir/lib/registry.ex | 4 ++++ .../test/elixir/registry/duplicate_test.exs | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index a67fbb6db6e..980a11af3ca 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1686,6 +1686,10 @@ defmodule Registry do end end + # For {:duplicate, :key} registries, the ETS entry layout is + # {{key, pid, counter}, value} (ordered_set with composite key), + # so :"$_" refers to that whole tuple. The body extracts {pid, value} + # to match the public API's return shape of [{pid, value}]. defp match_spec({:duplicate, :key}, key, pattern, guards) do body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}] diff --git a/lib/elixir/test/elixir/registry/duplicate_test.exs b/lib/elixir/test/elixir/registry/duplicate_test.exs index c1d81a226fa..8ee79db46fb 100644 --- a/lib/elixir/test/elixir/registry/duplicate_test.exs +++ b/lib/elixir/test/elixir/registry/duplicate_test.exs @@ -293,6 +293,28 @@ defmodule Registry.DuplicateTest do assert Registry.keys(registry, self()) |> Enum.sort() == [:_, "hello", "hello"] end + test "match supports tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, {1, :atom}) + {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, "hello", "a") + + assert Registry.match(registry, :_, {:_, :atom}) |> Enum.sort() == + [{self(), {1, :atom}}, {self(), {2, :atom}}] + + assert Registry.match(registry, :_, {1, :_}) == [{self(), {1, :atom}}] + assert Registry.match(registry, "hello", :_) == [{self(), "a"}] + end + + test "count_match supports tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, {1, :atom}) + {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, "hello", "a") + + assert Registry.count_match(registry, :_, {:_, :atom}) == 2 + assert Registry.count_match(registry, :_, {1, :_}) == 1 + assert Registry.count_match(registry, "hello", :_) == 1 + end + @tag base_listener: :unique_listener test "allows listeners", %{registry: registry, listeners: [listener]} do Process.register(self(), listener) From 688142c1bc046ad8ffdece319348d628b9f47938 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Wed, 15 Apr 2026 20:59:25 +0200 Subject: [PATCH 21/23] Add tricky-key tests for select/count_select --- .../test/elixir/registry/duplicate_test.exs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/elixir/test/elixir/registry/duplicate_test.exs b/lib/elixir/test/elixir/registry/duplicate_test.exs index 8ee79db46fb..dbcbf56c368 100644 --- a/lib/elixir/test/elixir/registry/duplicate_test.exs +++ b/lib/elixir/test/elixir/registry/duplicate_test.exs @@ -501,6 +501,40 @@ defmodule Registry.DuplicateTest do ]) end + test "select supports tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, {1, :atom}) + {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, "hello", "a") + + # Use a guard to match the literal :_ key, since :_ in the match head is a wildcard + assert [{self(), {1, :atom}}, {self(), {2, :atom}}] == + Registry.select(registry, [ + {{:"$1", :"$2", :"$3"}, [{:"=:=", :"$1", {:const, :_}}], [{{:"$2", :"$3"}}]} + ]) + |> Enum.sort() + + assert [{self(), "a"}] == + Registry.select(registry, [ + {{"hello", :"$1", :"$2"}, [], [{{:"$1", :"$2"}}]} + ]) + end + + test "count_select supports tricky keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, {1, :atom}) + {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, "hello", "a") + + assert 2 == + Registry.count_select(registry, [ + {{:"$1", :_, :_}, [{:"=:=", :"$1", {:const, :_}}], [true]} + ]) + + assert 1 == + Registry.count_select(registry, [ + {{"hello", :_, :_}, [], [true]} + ]) + end + test "rejects invalid tuple syntax", %{partitions: partitions} do name = :"test_invalid_tuple_#{partitions}" From 53b44ec8c6ebef9bcf89093d5af2f3ea983eac9a Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Thu, 16 Apr 2026 10:26:16 +0200 Subject: [PATCH 22/23] Cover "$1" reserved-atom key in duplicate registry tests --- .../test/elixir/registry/duplicate_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/elixir/test/elixir/registry/duplicate_test.exs b/lib/elixir/test/elixir/registry/duplicate_test.exs index dbcbf56c368..14c6eaf7976 100644 --- a/lib/elixir/test/elixir/registry/duplicate_test.exs +++ b/lib/elixir/test/elixir/registry/duplicate_test.exs @@ -296,22 +296,26 @@ defmodule Registry.DuplicateTest do test "match supports tricky keys", %{registry: registry} do {:ok, _} = Registry.register(registry, :_, {1, :atom}) {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) {:ok, _} = Registry.register(registry, "hello", "a") assert Registry.match(registry, :_, {:_, :atom}) |> Enum.sort() == [{self(), {1, :atom}}, {self(), {2, :atom}}] assert Registry.match(registry, :_, {1, :_}) == [{self(), {1, :atom}}] + assert Registry.match(registry, :"$1", {:_, :atom}) == [{self(), {3, :atom}}] assert Registry.match(registry, "hello", :_) == [{self(), "a"}] end test "count_match supports tricky keys", %{registry: registry} do {:ok, _} = Registry.register(registry, :_, {1, :atom}) {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) {:ok, _} = Registry.register(registry, "hello", "a") assert Registry.count_match(registry, :_, {:_, :atom}) == 2 assert Registry.count_match(registry, :_, {1, :_}) == 1 + assert Registry.count_match(registry, :"$1", {:_, :atom}) == 1 assert Registry.count_match(registry, "hello", :_) == 1 end @@ -504,6 +508,7 @@ defmodule Registry.DuplicateTest do test "select supports tricky keys", %{registry: registry} do {:ok, _} = Registry.register(registry, :_, {1, :atom}) {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) {:ok, _} = Registry.register(registry, "hello", "a") # Use a guard to match the literal :_ key, since :_ in the match head is a wildcard @@ -517,11 +522,17 @@ defmodule Registry.DuplicateTest do Registry.select(registry, [ {{"hello", :"$1", :"$2"}, [], [{{:"$1", :"$2"}}]} ]) + + assert [{self(), {3, :atom}}] == + Registry.select(registry, [ + {{:"$2", :"$3", :"$4"}, [{:"=:=", :"$2", {:const, :"$1"}}], [{{:"$3", :"$4"}}]} + ]) end test "count_select supports tricky keys", %{registry: registry} do {:ok, _} = Registry.register(registry, :_, {1, :atom}) {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) {:ok, _} = Registry.register(registry, "hello", "a") assert 2 == @@ -533,6 +544,11 @@ defmodule Registry.DuplicateTest do Registry.count_select(registry, [ {{"hello", :_, :_}, [], [true]} ]) + + assert 1 == + Registry.count_select(registry, [ + {{:"$2", :_, :_}, [{:"=:=", :"$2", {:const, :"$1"}}], [true]} + ]) end test "rejects invalid tuple syntax", %{partitions: partitions} do From 564f5489c40d6f28e2587616d39b8fa81416a270 Mon Sep 17 00:00:00 2001 From: Rafal Studnicki Date: Sat, 18 Apr 2026 14:30:28 +0200 Subject: [PATCH 23/23] Group reserved-atom key tests together in duplicate registry tests --- .../test/elixir/registry/duplicate_test.exs | 131 +++++++++--------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/lib/elixir/test/elixir/registry/duplicate_test.exs b/lib/elixir/test/elixir/registry/duplicate_test.exs index 14c6eaf7976..ea35ad3b4e3 100644 --- a/lib/elixir/test/elixir/registry/duplicate_test.exs +++ b/lib/elixir/test/elixir/registry/duplicate_test.exs @@ -180,16 +180,6 @@ defmodule Registry.DuplicateTest do assert Registry.unregister(registry, "hello") == :ok end - test "unregisters with tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, :foo) - {:ok, _} = Registry.register(registry, :_, :bar) - {:ok, _} = Registry.register(registry, "hello", "a") - {:ok, _} = Registry.register(registry, "hello", "b") - - Registry.unregister(registry, :_) - assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello"] - end - test "supports match patterns", %{registry: registry} do value1 = {1, :atom, 1} value2 = {2, :atom, 2} @@ -281,44 +271,6 @@ defmodule Registry.DuplicateTest do assert Registry.lookup(registry, "hello") == [{self(), value2}] end - test "unregister_match supports tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, :foo) - {:ok, _} = Registry.register(registry, :_, :bar) - {:ok, _} = Registry.register(registry, "hello", "a") - {:ok, _} = Registry.register(registry, "hello", "b") - - Registry.unregister_match(registry, :_, :foo) - assert Registry.lookup(registry, :_) == [{self(), :bar}] - - assert Registry.keys(registry, self()) |> Enum.sort() == [:_, "hello", "hello"] - end - - test "match supports tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, {1, :atom}) - {:ok, _} = Registry.register(registry, :_, {2, :atom}) - {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) - {:ok, _} = Registry.register(registry, "hello", "a") - - assert Registry.match(registry, :_, {:_, :atom}) |> Enum.sort() == - [{self(), {1, :atom}}, {self(), {2, :atom}}] - - assert Registry.match(registry, :_, {1, :_}) == [{self(), {1, :atom}}] - assert Registry.match(registry, :"$1", {:_, :atom}) == [{self(), {3, :atom}}] - assert Registry.match(registry, "hello", :_) == [{self(), "a"}] - end - - test "count_match supports tricky keys", %{registry: registry} do - {:ok, _} = Registry.register(registry, :_, {1, :atom}) - {:ok, _} = Registry.register(registry, :_, {2, :atom}) - {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) - {:ok, _} = Registry.register(registry, "hello", "a") - - assert Registry.count_match(registry, :_, {:_, :atom}) == 2 - assert Registry.count_match(registry, :_, {1, :_}) == 1 - assert Registry.count_match(registry, :"$1", {:_, :atom}) == 1 - assert Registry.count_match(registry, "hello", :_) == 1 - end - @tag base_listener: :unique_listener test "allows listeners", %{registry: registry, listeners: [listener]} do Process.register(self(), listener) @@ -505,7 +457,72 @@ defmodule Registry.DuplicateTest do ]) end - test "select supports tricky keys", %{registry: registry} do + test "rejects invalid tuple syntax", %{partitions: partitions} do + name = :"test_invalid_tuple_#{partitions}" + + assert_raise ArgumentError, ~r/expected :keys to be given and be one of/, fn -> + Registry.start_link(keys: {:duplicate, :invalid}, name: name, partitions: partitions) + end + end + + test "update_value is not supported", %{registry: registry} do + assert_raise ArgumentError, ~r/Registry.update_value\/3 is not supported/, fn -> + Registry.update_value(registry, "hello", fn val -> val end) + end + end + + # Keys like :_ and :"$1" are reserved atoms in ETS match spec syntax. + # These tests verify that they work correctly as literal registry keys. + + test "unregister with reserved-atom keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, :foo) + {:ok, _} = Registry.register(registry, :_, :bar) + {:ok, _} = Registry.register(registry, "hello", "a") + {:ok, _} = Registry.register(registry, "hello", "b") + + Registry.unregister(registry, :_) + assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello"] + end + + test "unregister_match with reserved-atom keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, :foo) + {:ok, _} = Registry.register(registry, :_, :bar) + {:ok, _} = Registry.register(registry, "hello", "a") + {:ok, _} = Registry.register(registry, "hello", "b") + + Registry.unregister_match(registry, :_, :foo) + assert Registry.lookup(registry, :_) == [{self(), :bar}] + + assert Registry.keys(registry, self()) |> Enum.sort() == [:_, "hello", "hello"] + end + + test "match with reserved-atom keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, {1, :atom}) + {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) + {:ok, _} = Registry.register(registry, "hello", "a") + + assert Registry.match(registry, :_, {:_, :atom}) |> Enum.sort() == + [{self(), {1, :atom}}, {self(), {2, :atom}}] + + assert Registry.match(registry, :_, {1, :_}) == [{self(), {1, :atom}}] + assert Registry.match(registry, :"$1", {:_, :atom}) == [{self(), {3, :atom}}] + assert Registry.match(registry, "hello", :_) == [{self(), "a"}] + end + + test "count_match with reserved-atom keys", %{registry: registry} do + {:ok, _} = Registry.register(registry, :_, {1, :atom}) + {:ok, _} = Registry.register(registry, :_, {2, :atom}) + {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) + {:ok, _} = Registry.register(registry, "hello", "a") + + assert Registry.count_match(registry, :_, {:_, :atom}) == 2 + assert Registry.count_match(registry, :_, {1, :_}) == 1 + assert Registry.count_match(registry, :"$1", {:_, :atom}) == 1 + assert Registry.count_match(registry, "hello", :_) == 1 + end + + test "select with reserved-atom keys", %{registry: registry} do {:ok, _} = Registry.register(registry, :_, {1, :atom}) {:ok, _} = Registry.register(registry, :_, {2, :atom}) {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) @@ -529,7 +546,7 @@ defmodule Registry.DuplicateTest do ]) end - test "count_select supports tricky keys", %{registry: registry} do + test "count_select with reserved-atom keys", %{registry: registry} do {:ok, _} = Registry.register(registry, :_, {1, :atom}) {:ok, _} = Registry.register(registry, :_, {2, :atom}) {:ok, _} = Registry.register(registry, :"$1", {3, :atom}) @@ -551,20 +568,6 @@ defmodule Registry.DuplicateTest do ]) end - test "rejects invalid tuple syntax", %{partitions: partitions} do - name = :"test_invalid_tuple_#{partitions}" - - assert_raise ArgumentError, ~r/expected :keys to be given and be one of/, fn -> - Registry.start_link(keys: {:duplicate, :invalid}, name: name, partitions: partitions) - end - end - - test "update_value is not supported", %{registry: registry} do - assert_raise ArgumentError, ~r/Registry.update_value\/3 is not supported/, fn -> - Registry.update_value(registry, "hello", fn val -> val end) - end - end - defp register_task(registry, key, value) do parent = self()