Current section
Files
Jump to
Current section
Files
lib/feature_supervisor.ex
defmodule FeatureSupervisor do
@moduledoc """
A small wrapper for `Supervisor` that dynamically starts/terminates children based on feature flags.
## Example
defmodule MyApp.Application do
use Application
@mix_env Mix.env()
def start(_type, _args) do
children = [
# a regular child
Child1,
# supposed to be disabled in tests
FeatureSupervisor.child_spec(Child2, enabled?: @mix_env != :test),
# supposed to run only when the feature is enabled
FeatureSupervisor.child_spec({Child3, name: Child3},
enabled?: &feature_enabled?/1,
feature_id: "my-feature"
)
]
FeatureSupervisor.start_link(children, strategy: :one_for_one, sync_interval: 1000)
end
defp feature_enabled?(spec) do
MyApp.Features.enabled?(spec.feature_id)
end
end
"""
alias FeatureSupervisor.Manager
@type child ::
Supervisor.child_spec()
| {module(), term()}
| module()
| (old_erlang_child_spec :: :supervisor.child_spec())
@type option :: {:sync_interval, non_neg_integer()}
@doc """
Wraps the `Supervisor.start_link/2` function.
Children will be split into two groups:
* "static" - those without the `:enabled?` field or with `enabled?: true`
* "dynamic" - those with the `:enabled?` field set to a function
Children with `enabled?: false` will be excluded.
"static" children will be started as normal.
"dynamic" children (if any) will be started separately via `Supervisor.start_child/2`.
If a "dynamic" child is expected to be started/terminated later via a feature flag
you should provide the `sync_interval` option. The child's `:restart` option must be set to `:permanent`.
"""
@spec start_link([child()], [Supervisor.option() | Supervisor.init_option() | option()]) ::
{:ok, pid} | {:error, {:already_started, pid} | {:shutdown, term} | term}
def start_link(children, options) when is_list(children) do
children
|> group_children()
|> maybe_add_manager_spec(options)
|> Supervisor.start_link(options)
end
@doc """
Same as `Supervisor.child_spec/2` but allows any key in the overrides.
"""
@spec child_spec(child(), Keyword.t()) :: map()
def child_spec(child, overrides) do
child
|> Supervisor.child_spec([])
|> Map.merge(Map.new(overrides))
end
@doc false
@spec group_children([child()]) :: {static :: [map()], dynamic :: [map()]}
def group_children(children) do
children
|> Enum.reverse()
|> Enum.reduce({[], []}, fn spec, {static, dynamic} = acc ->
case child_spec(spec, []) do
%{enabled?: fun} = spec when is_function(fun, 1) -> {static, [spec | dynamic]}
%{enabled?: false} -> acc
spec -> {[spec | static], dynamic}
end
end)
end
defp maybe_add_manager_spec({static, []}, _), do: static
defp maybe_add_manager_spec({static, dynamic}, options) do
static ++
[
Manager.child_spec(
children: dynamic,
sync_interval: Keyword.get(options, :sync_interval)
)
]
end
end