Skip to content

Commit 4b72da2

Browse files
committed
Add PartitionSupervisor.resize!/2
1 parent 1572aac commit 4b72da2

File tree

2 files changed

+162
-41
lines changed

2 files changed

+162
-41
lines changed

lib/elixir/lib/partition_supervisor.ex

Lines changed: 103 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ defmodule PartitionSupervisor do
264264
raise "the call to the function in :with_arguments must return a list, got: #{inspect(args)}"
265265
end
266266

267-
start = {__MODULE__, :start_child, [mod, fun, args, name, partition]}
267+
start = {__MODULE__, :start_child, [mod, fun, args, partition]}
268268
Map.merge(map, %{id: partition, start: start, modules: modules})
269269
end
270270

@@ -282,48 +282,112 @@ defmodule PartitionSupervisor do
282282
end
283283

284284
@doc false
285-
def start_child(mod, fun, args, name, partition) do
285+
def start_child(mod, fun, args, partition) do
286286
case apply(mod, fun, args) do
287287
{:ok, pid} ->
288-
register_child(name, partition, pid)
288+
register_child(partition, pid)
289289
{:ok, pid}
290290

291291
{:ok, pid, info} ->
292-
register_child(name, partition, pid)
292+
register_child(partition, pid)
293293
{:ok, pid, info}
294294

295295
other ->
296296
other
297297
end
298298
end
299299

300-
defp register_child(name, partition, pid) when is_atom(name) do
301-
:ets.insert(name, {partition, pid})
302-
end
303-
304-
defp register_child({:via, _, _}, partition, pid) do
305-
Registry.register(@registry, {self(), partition}, pid)
300+
defp register_child(partition, pid) do
301+
:ets.insert(Process.get(:ets_table), {partition, pid})
306302
end
307303

308304
@impl true
309305
def init({name, partitions, children, init_opts}) do
310-
init_partitions(name, partitions)
306+
table = init_table(name)
307+
:ets.insert(table, {:partitions, partitions, partitions})
308+
Process.put(:ets_table, table)
311309
Supervisor.init(children, Keyword.put_new(init_opts, :strategy, :one_for_one))
312310
end
313311

314-
defp init_partitions(name, partitions) when is_atom(name) do
315-
:ets.new(name, [:set, :named_table, :protected, read_concurrency: true])
316-
:ets.insert(name, {:partitions, partitions})
312+
defp init_table(name) when is_atom(name) do
313+
:ets.new(name, [:set, :named_table, :public, read_concurrency: true])
317314
end
318315

319-
defp init_partitions({:via, _, _}, partitions) do
320-
child_spec = {Registry, keys: :unique, name: @registry}
316+
defp init_table({:via, _, _}) do
317+
table = :ets.new(__MODULE__, [:set, :public, read_concurrency: true])
318+
ensure_registry()
319+
Registry.register(@registry, self(), table)
320+
table
321+
end
321322

322-
if !Process.whereis(@registry) do
323-
Supervisor.start_child(:elixir_sup, child_spec)
323+
defp ensure_registry do
324+
if Process.whereis(@registry) == nil do
325+
Supervisor.start_child(:elixir_sup, {Registry, keys: :unique, name: @registry})
324326
end
327+
end
328+
329+
@doc """
330+
Resizes the number of partitions in the PartitionSupervisor.
325331
326-
Registry.register(@registry, self(), partitions)
332+
This is done by starting or stopping a given number of
333+
partitions in the supervisor. All of the child specifications
334+
are kept in the `PartitionSupervisor` itself.
335+
336+
The final number of partitions cannot be less than zero and
337+
cannot be more than the number of partitions the supervisor
338+
started with.
339+
"""
340+
@doc since: "1.18.0"
341+
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
342+
def resize!(name, partitions) when is_integer(partitions) do
343+
supervisor =
344+
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
345+
346+
table = table(name)
347+
ensure_registry()
348+
349+
Registry.lock(@registry, supervisor, fn ->
350+
case :ets.lookup(table, :partitions) do
351+
[{:partitions, _current, max}] when partitions not in 0..max//1 ->
352+
raise ArgumentError,
353+
"the number of partitions to resize to must be a number between 0 and #{max}, got: #{partitions}"
354+
355+
[{:partitions, current, max}] when partitions > current ->
356+
for id <- current..(partitions - 1) do
357+
case Supervisor.restart_child(supervisor, id) do
358+
{:ok, _} ->
359+
:ok
360+
361+
{:ok, _, _} ->
362+
:ok
363+
364+
{:error, reason} ->
365+
raise "cannot restart partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
366+
end
367+
end
368+
369+
:ets.insert(table, {:partitions, partitions, max})
370+
current
371+
372+
[{:partitions, current, max}] when partitions < current ->
373+
:ets.insert(table, {:partitions, partitions, max})
374+
375+
for id <- partitions..(current - 1) do
376+
case Supervisor.terminate_child(supervisor, id) do
377+
:ok ->
378+
:ok
379+
380+
{:error, reason} ->
381+
raise "cannot terminate partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
382+
end
383+
end
384+
385+
current
386+
387+
[{:partitions, current, _max}] ->
388+
current
389+
end
390+
end)
327391
end
328392

329393
@doc """
@@ -332,24 +396,27 @@ defmodule PartitionSupervisor do
332396
@doc since: "1.14.0"
333397
@spec partitions(name()) :: pos_integer()
334398
def partitions(name) do
335-
{_name, partitions} = name_partitions(name)
336-
partitions
399+
name |> table() |> partitions(name)
337400
end
338401

339-
# For whereis_name, we want to lookup on GenServer.whereis/1
340-
# just once, so we lookup the name and partitions together.
341-
defp name_partitions(name) when is_atom(name) do
402+
defp partitions(table, name) do
342403
try do
343-
{name, :ets.lookup_element(name, :partitions, 2)}
404+
:ets.lookup_element(table, :partitions, 2)
344405
rescue
345406
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
346407
end
347408
end
348409

349-
defp name_partitions(name) when is_tuple(name) do
410+
defp table(name) when is_atom(name) do
411+
name
412+
end
413+
414+
# For whereis_name, we want to lookup on GenServer.whereis/1
415+
# just once, so we lookup the name and partitions together.
416+
defp table(name) when is_tuple(name) do
350417
with pid when is_pid(pid) <- GenServer.whereis(name),
351-
[name_partitions] <- Registry.lookup(@registry, pid) do
352-
name_partitions
418+
[{_, table}] <- Registry.lookup(@registry, pid) do
419+
table
353420
else
354421
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
355422
end
@@ -374,7 +441,7 @@ defmodule PartitionSupervisor do
374441
@doc since: "1.14.0"
375442
@spec which_children(name()) :: [
376443
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
377-
{:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
444+
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
378445
]
379446
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
380447
Supervisor.which_children(name)
@@ -428,22 +495,17 @@ defmodule PartitionSupervisor do
428495

429496
@doc false
430497
def whereis_name({name, key}) when is_atom(name) or is_tuple(name) do
431-
{name, partitions} = name_partitions(name)
498+
table = table(name)
499+
partitions = partitions(table, name)
500+
501+
if partitions == 0 do
502+
raise ArgumentError, "PartitionSupervisor #{inspect(name)} has zero partitions"
503+
end
432504

433505
partition =
434506
if is_integer(key), do: rem(abs(key), partitions), else: :erlang.phash2(key, partitions)
435507

436-
whereis_name(name, partition)
437-
end
438-
439-
defp whereis_name(name, partition) when is_atom(name) do
440-
:ets.lookup_element(name, partition, 2)
441-
end
442-
443-
defp whereis_name(name, partition) when is_pid(name) do
444-
@registry
445-
|> Registry.values({name, partition}, name)
446-
|> List.first(:undefined)
508+
:ets.lookup_element(table, partition, 2)
447509
end
448510

449511
@doc false

lib/elixir/test/elixir/partition_supervisor_test.exs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,65 @@ defmodule PartitionSupervisorTest do
158158
end
159159
end
160160

161+
describe "resize!/1" do
162+
test "resizes the number of children", config do
163+
{:ok, _} =
164+
PartitionSupervisor.start_link(
165+
child_spec: {Agent, fn -> %{} end},
166+
name: config.test,
167+
partitions: 8
168+
)
169+
170+
for range <- [8..0//1, 0..8//1, Enum.shuffle(0..8)], i <- range do
171+
PartitionSupervisor.resize!(config.test, i)
172+
assert PartitionSupervisor.partitions(config.test) == i
173+
174+
assert PartitionSupervisor.count_children(config.test) ==
175+
%{active: i, specs: 8, supervisors: 0, workers: 8}
176+
177+
# Assert that we can still query across all range,
178+
# but they are routed properly, as long as we have
179+
# a single partition.
180+
children =
181+
for partition <- 0..7, i != 0, uniq: true do
182+
GenServer.whereis({:via, PartitionSupervisor, {config.test, partition}})
183+
end
184+
185+
assert length(children) == i
186+
end
187+
end
188+
189+
test "raises on lookup after resizing to zero", config do
190+
{:ok, _} =
191+
PartitionSupervisor.start_link(
192+
child_spec: {Agent, fn -> %{} end},
193+
name: config.test,
194+
partitions: 8
195+
)
196+
197+
assert PartitionSupervisor.resize!(config.test, 0) == 8
198+
199+
assert_raise ArgumentError, ~r"has zero partitions", fn ->
200+
GenServer.whereis({:via, PartitionSupervisor, {config.test, 0}})
201+
end
202+
203+
assert PartitionSupervisor.resize!(config.test, 8) == 0
204+
end
205+
206+
test "raises if trying to increase the number of partitions", config do
207+
{:ok, _} =
208+
PartitionSupervisor.start_link(
209+
child_spec: {Agent, fn -> %{} end},
210+
name: config.test,
211+
partitions: 8
212+
)
213+
214+
assert_raise ArgumentError,
215+
"the number of partitions to resize to must be a number between 0 and 8, got: 9",
216+
fn -> PartitionSupervisor.resize!(config.test, 9) end
217+
end
218+
end
219+
161220
describe "which_children/1" do
162221
test "returns all partitions", config do
163222
{:ok, _} =

0 commit comments

Comments
 (0)