Packages

SuperWorker is a powerful Elixir library for working with supervisors and background jobs. It provides a much simpler approach than traditional supervisors. This library is currently under development and is unstable, so it is not recommended for production use.

Current section

Files

Jump to
super_worker lib supervisor validator.ex
Raw

lib/supervisor/validator.ex

defmodule SuperWorker.Supervisor.Validator do
@moduledoc """
Utility functions for SuperWorker.Supervisor.
This module provides common utility functions used across the supervisor
implementation, including:
- Option normalization and validation
- API call helpers
- Hashing and partitioning utilities
- Process information helpers
"""
@sup_params [:id, :number_of_partitions, :link, :report_to, :children]
alias SuperWorker.Supervisor
require Logger
# ============================================================================
# Type Definitions
# ============================================================================
@type api_result :: {:ok, any()} | {:error, any()}
@type timeout_ms :: non_neg_integer() | :infinity
def validate_and_convert(options) do
with {:ok, opts} <- normalize_options(options, @sup_params),
{:ok, opts} <- default_sup_opts(opts),
{:ok, opts} <- generic_default_options(opts),
{:ok, opts} <- validate_options(opts),
{:ok, sup} <- to_struct(opts) do
{:ok, sup}
end
end
@spec normalize_options([keyword() | atom()], [atom()]) :: api_result()
def normalize_options(opts, allowed_params) when is_list(opts) and is_list(allowed_params) do
do_normalize_opts(opts, allowed_params, %{}, [])
end
@spec generic_default_options(map()) :: map()
defp generic_default_options(opts) when is_map(opts) do
{:ok, Map.put_new(opts, :owner, self())}
end
@doc """
Checks that the value of a key in options passes a validation function.
## Examples
iex> check_type(%{count: 5}, :count, &is_integer/1)
{:ok, %{count: 5}}
iex> check_type(%{count: "five"}, :count, &is_integer/1)
{:error, :invalid_type}
"""
@spec check_type(map(), atom(), (any() -> boolean())) :: api_result()
def check_type(opts, key, validator_fun)
when is_map(opts) and is_atom(key) and is_function(validator_fun, 1) do
case Map.get(opts, key) do
nil ->
Logger.warning("SuperWorker, Utils, option #{inspect(key)} not found")
{:error, :invalid_type}
value ->
if validator_fun.(value) do
{:ok, opts}
else
Logger.warning("SuperWorker, Utils, option #{inspect(key)} has invalid type")
{:error, :invalid_type}
end
end
end
@doc """
Converts a keyword option shorthand to a full keyword tuple.
Used for converting shorthand like `:group` to `{:type, :group}`.
"""
@spec get_keyword(atom()) :: {:type, atom()} | {:error, :invalid_options}
def get_keyword(:group), do: {:type, :group}
def get_keyword(:chain), do: {:type, :chain}
def get_keyword(:standalone), do: {:type, :standalone}
def get_keyword(_), do: {:error, :invalid_options}
# ============================================================================
# Hashing and Partitioning
# ============================================================================
@doc """
Computes a hash-based order for partitioning data.
Uses Erlang's phash2 for consistent hashing across the cluster.
## Examples
iex> order = get_hash_order("my_data", 10)
iex> order >= 0 and order < 10
true
"""
@spec get_hash_order(term(), pos_integer()) :: non_neg_integer()
def get_hash_order(term, num_partitions)
when is_integer(num_partitions) and num_partitions > 0 do
:erlang.phash2(term, num_partitions)
end
# ============================================================================
# System Information
# ============================================================================
@doc """
Returns the number of online schedulers in the system.
This is typically used as the default number of partitions.
"""
@spec get_default_schedulers() :: pos_integer()
def get_default_schedulers do
System.schedulers_online()
end
@doc """
Counts the number of messages in a process's message queue.
Returns 0 if the process is not alive.
"""
@spec count_msgs(pid()) :: non_neg_integer()
def count_msgs(pid) when is_pid(pid) do
case Process.info(pid, :message_queue_len) do
{:message_queue_len, n} when is_integer(n) -> n
nil -> 0
end
end
# ============================================================================
# Private Functions
# ============================================================================
# Recursively processes options list, separating valid from invalid options
@spec do_normalize_opts(
[keyword() | atom()],
[atom()],
map(),
[atom() | {atom(), any()}]
) :: api_result()
defp do_normalize_opts([], _allowed_params, valid, invalid) do
if Enum.empty?(invalid) do
{:ok, valid}
else
{:error, {:invalid_options, Enum.reverse(invalid)}}
end
end
# Handle keyword tuple option
defp do_normalize_opts([{key, value} | rest], allowed_params, valid, invalid)
when is_atom(key) do
if key in allowed_params do
do_normalize_opts(rest, allowed_params, Map.put(valid, key, value), invalid)
else
do_normalize_opts(rest, allowed_params, valid, [key | invalid])
end
end
# Handle atom shorthand option (like :group, :chain)
defp do_normalize_opts([short_opt | rest], allowed_params, valid, invalid)
when is_atom(short_opt) do
case get_keyword(short_opt) do
{:type, value} ->
if :type in allowed_params do
do_normalize_opts(rest, allowed_params, Map.put(valid, :type, value), invalid)
else
do_normalize_opts(rest, allowed_params, valid, [short_opt | invalid])
end
{:error, _} ->
if short_opt in allowed_params do
# It's a valid boolean flag
do_normalize_opts(rest, allowed_params, Map.put(valid, short_opt, true), invalid)
else
do_normalize_opts(rest, allowed_params, valid, [short_opt | invalid])
end
end
end
# Handle any other unexpected option format
defp do_normalize_opts([opt | rest], allowed_params, valid, invalid) do
do_normalize_opts(rest, allowed_params, valid, [opt | invalid])
end
# Set the default options if not provided.
# TO-DO: Merge with generic_default_sup_opts/1.
defp default_sup_opts(opts) do
opts =
if Map.has_key?(opts, :number_of_partitions) do
opts
else
Map.put(opts, :number_of_partitions, :erlang.system_info(:schedulers_online))
end
opts =
if Map.has_key?(opts, :link) do
opts
else
Map.put(opts, :link, true)
end
opts = Map.put(opts, :master, opts.id)
{:ok, opts}
end
# Validate the type & value of options.
defp validate_options(options) do
with {:ok, opts} <- check_type(options, :id, &is_atom/1),
{:ok, opts} <- check_type(opts, :number_of_partitions, &is_integer/1),
{:ok, opts} <- check_type(opts, :number_of_partitions, &(&1 > 0)),
{:ok, opts} <- check_type(opts, :owner, &is_pid/1),
{:ok, opts} <- check_type(opts, :link, &is_boolean/1) do
{:ok, opts}
else
{:error, reason} = error ->
Logger.error("SuperWorker, Supervisor, error in validating options: #{inspect(reason)}")
error
end
end
defp to_struct(map_options) when is_map(map_options) do
fields =
%Supervisor{id: nil}
|> Map.from_struct()
|> Map.keys()
result =
Enum.reduce(fields, %Supervisor{id: nil}, fn field, acc ->
if Map.has_key?(map_options, field) do
%{acc | field => Map.get(map_options, field)}
else
acc
end
end)
{:ok, result}
end
end