Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start with initial children #1

Merged
merged 3 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: [
"{mix,.formatter}.exs",
"{config,lib,test,test_app,test_app_with_children}/**/*.{ex,exs}"
]
]
29 changes: 27 additions & 2 deletions lib/pogo/dynamic_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ defmodule Pogo.DynamicSupervisor do

use GenServer, type: :supervisor

@type option :: {:name, Supervised.name()}
@type init_option ::
Supervisor.init_option()
| {:scope, term}
| {:sync_interval, pos_integer()}
| {:children, [Supervisor.child_spec()]}

@sync_interval 5_000

defstruct [:scope, :supervisor, :sync_interval]
Expand All @@ -48,8 +55,14 @@ defmodule Pogo.DynamicSupervisor do
* `sync_interval` - interval in milliseconds, how often the supervisor should
synchronize its local state with the cluster by processing requests to
start and terminate child processes, defaults to `5_000`
* `children` - list of child specifications to automatially start in the cluster

Children specified at startup are handled in the same way as children started
dynamically. The only difference is that when `Pogo.DynamicSupervisor` runs under
a supervision tree and is restarted, it will automatically restart all these
children as well.
"""
@spec start_link(keyword) :: GenServer.on_start()
@spec start_link([option | init_option]) :: GenServer.on_start()
def start_link(opts) do
{name, opts} = Keyword.pop(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
Expand Down Expand Up @@ -93,6 +106,7 @@ defmodule Pogo.DynamicSupervisor do
def init(opts) do
scope = Keyword.fetch!(opts, :scope)
{sync_interval, opts} = Keyword.pop(opts, :sync_interval, @sync_interval)
{children, opts} = Keyword.pop(opts, :children, [])

opts = Keyword.put(opts, :strategy, :one_for_one)

Expand All @@ -109,7 +123,18 @@ defmodule Pogo.DynamicSupervisor do

Process.send_after(self(), :sync, sync_interval)

{:ok, state}
{:ok, state, {:continue, {:start_children, children}}}
end

@impl true
def handle_continue({:start_children, children}, %{scope: scope} = state) do
for child_spec <- children do
child_spec = Supervisor.child_spec(child_spec, [])
:ok = validate_child(child_spec)
make_request(scope, {:start_child, child_spec})
end

{:noreply, state}
end

@impl true
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ defmodule Pogo.MixProject do
{:libring, "~> 1.6.0"},
{:local_cluster, "~> 1.2.1", only: [:test]},
{:test_app, path: "test_app", only: [:test]},
{:test_app_with_children, path: "test_app_with_children", only: [:test]},
{:ex_doc, "~> 0.29", only: [:dev], runtime: false},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
]
Expand Down
69 changes: 56 additions & 13 deletions test/pogo/dynamic_supervisor_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Pogo.DynamicSupervisorTest do
use ExUnit.Case
import AssertAsync

@supervisor TestApp.DistributedSupervisor
@supervisor PogoTest.DistributedSupervisor

setup do
on_exit(fn ->
Expand All @@ -15,7 +15,7 @@ defmodule Pogo.DynamicSupervisorTest do
end

test "starts child on a single node in the cluster" do
[node1, node2] = start_nodes("foo", 2)
[node1, node2] = start_nodes(:test_app, "foo", 2)

child_spec = Pogo.Worker.child_spec(1)

Expand All @@ -31,7 +31,7 @@ defmodule Pogo.DynamicSupervisorTest do
end

test "terminates child running in the cluster" do
[node1, node2] = nodes = start_nodes("foo", 2)
[node1, node2] = nodes = start_nodes(:test_app, "foo", 2)

[child_spec1, _child_spec2, child_spec3] =
for id <- 1..3 do
Expand Down Expand Up @@ -80,7 +80,7 @@ defmodule Pogo.DynamicSupervisorTest do
end

test "keeps track of new pid when child process crashes and gets restarted" do
[node] = start_nodes("foo", 1)
[node] = start_nodes(:test_app, "foo", 1)

child_spec = Pogo.Worker.child_spec(1)
start_child(node, child_spec)
Expand All @@ -99,7 +99,7 @@ defmodule Pogo.DynamicSupervisorTest do
end

test "moves children between nodes when cluster topology changes" do
[node1] = start_nodes("foo", 1)
[node1] = start_nodes(:test_app, "foo", 1)

start_child(node1, Pogo.Worker.child_spec(1))
start_child(node1, Pogo.Worker.child_spec(2))
Expand All @@ -113,7 +113,7 @@ defmodule Pogo.DynamicSupervisorTest do
} = local_children([node1])
end

[node2] = start_nodes("bar", 1)
[node2] = start_nodes(:test_app, "bar", 1)

assert_async do
assert %{
Expand All @@ -135,7 +135,7 @@ defmodule Pogo.DynamicSupervisorTest do
end

test "starts process on another node when the node it was scheduled on goes down" do
[node1, node2, node3] = start_nodes("foo", 3)
[node1, node2, node3] = start_nodes(:test_app, "foo", 3)

start_child(node2, Pogo.Worker.child_spec(1))

Expand All @@ -161,10 +161,53 @@ defmodule Pogo.DynamicSupervisorTest do
end
end

test "starts distributed supervisor with initial list of children" do
[node1, node2, node3] = start_nodes(:test_app_with_children, "foo", 3)

assert_async do
assert %{
^node1 => [{{Pogo.Worker, 2}, _, :worker, _}],
^node2 => [
{{Pogo.Worker, 1}, _, :worker, _},
{{Pogo.Worker, 3}, _, :worker, _}
],
^node3 => []
} = local_children([node1, node2, node3])
end
end

test "initial list of children is automatically started after supervisor crash" do
[node] = start_nodes(:test_app_with_children, "foo", 1)

assert_async do
assert %{
^node => [
{{Pogo.Worker, 1}, _, :worker, _},
{{Pogo.Worker, 2}, _, :worker, _},
{{Pogo.Worker, 3}, _, :worker, _}
]
} = local_children([node])
end

# kill supervisor
pid = :rpc.call(node, Process, :whereis, [PogoTest.DistributedSupervisor])
:rpc.call(node, Process, :exit, [pid, :brutal_kill])

assert_async do
assert %{
^node => [
{{Pogo.Worker, 1}, _, :worker, _},
{{Pogo.Worker, 2}, _, :worker, _},
{{Pogo.Worker, 3}, _, :worker, _}
]
} = local_children([node])
end
end

@tag chaos: true
test "chaos" do
# start nodes
nodes = start_nodes("foo", 10)
nodes = start_nodes(:test_app, "foo", 10)

# start children
specs = for id <- 1..60, do: Pogo.Worker.child_spec(id)
Expand Down Expand Up @@ -192,7 +235,7 @@ defmodule Pogo.DynamicSupervisorTest do
:timer.sleep(1000)

# start some more nodes
nodes = nodes ++ start_nodes("bar", 10)
nodes = nodes ++ start_nodes(:test_app, "bar", 10)

:timer.sleep(1000)

Expand Down Expand Up @@ -270,7 +313,7 @@ defmodule Pogo.DynamicSupervisorTest do

describe "which_children/1" do
test "returns children running on the node when called with :local" do
[node1, node2] = nodes = start_nodes("foo", 2)
[node1, node2] = nodes = start_nodes(:test_app, "foo", 2)

start_child(node1, Pogo.Worker.child_spec(1))
start_child(node1, Pogo.Worker.child_spec(2))
Expand Down Expand Up @@ -299,7 +342,7 @@ defmodule Pogo.DynamicSupervisorTest do
end

test "returns all children running in cluster when called with :global" do
[node1, node2] = start_nodes("foo", 2)
[node1, node2] = start_nodes(:test_app, "foo", 2)

start_child(node1, Pogo.Worker.child_spec(1))
start_child(node1, Pogo.Worker.child_spec(2))
Expand Down Expand Up @@ -334,9 +377,9 @@ defmodule Pogo.DynamicSupervisorTest do
end
end

defp start_nodes(prefix, n) do
defp start_nodes(app, prefix, n) do
LocalCluster.start_nodes(prefix, n,
applications: [:test_app],
applications: [app],
files: ["test/support/pogo/worker.ex"]
)
end
Expand Down
4 changes: 0 additions & 4 deletions test_app/.formatter.exs

This file was deleted.

26 changes: 0 additions & 26 deletions test_app/.gitignore

This file was deleted.

21 changes: 0 additions & 21 deletions test_app/README.md

This file was deleted.

2 changes: 0 additions & 2 deletions test_app/lib/test_app.ex

This file was deleted.

2 changes: 1 addition & 1 deletion test_app/lib/test_app/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule TestApp.Application do
def start(_type, _args) do
children = [
{Pogo.DynamicSupervisor,
name: TestApp.DistributedSupervisor, scope: :test, sync_interval: 100}
name: PogoTest.DistributedSupervisor, scope: :test, sync_interval: 100}
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
27 changes: 27 additions & 0 deletions test_app_with_children/lib/test_app_with_children/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule TestAppWithChildren.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false

use Application

@impl true
def start(_type, _args) do
children = [
{Pogo.DynamicSupervisor,
name: PogoTest.DistributedSupervisor,
scope: :test,
sync_interval: 100,
children: [
Pogo.Worker.child_spec(1),
Pogo.Worker.child_spec(2),
{Pogo.Worker, 3}
]}
]

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: TestAppWithChildren.Supervisor]
Supervisor.start_link(children, opts)
end
end
29 changes: 29 additions & 0 deletions test_app_with_children/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule TestAppWithChildren.MixProject do
use Mix.Project

def project do
[
app: :test_app_with_children,
version: "0.1.0",
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {TestAppWithChildren.Application, []}
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end