PhoenixGenApi.NodeSelector (PhoenixGenApi v2.11.0)

Copy Markdown View Source

Provides node selection strategies for distributed request execution.

This module implements various strategies for selecting target nodes from a list of available nodes. The selection strategy determines how requests are distributed across nodes in a cluster.

Supported Selection Strategies

:random

Selects a random node from the available nodes list. This provides simple load balancing with no guarantees about distribution fairness.

:hash

Uses consistent hashing based on the request ID to select a node. The same request ID will always map to the same node, which is useful for caching and stateful operations.

### {:hash, hash_key} Uses consistent hashing based on a specific field from the request. The hash_key can reference a field in request.args or a field directly on the request struct (like user_id or device_id). This ensures requests with the same hash_key value always go to the same node.

:round_robin

Distributes requests evenly across all nodes in a circular fashion. Uses an atomic counter for true global round-robin distribution across all processes.

Dynamic Node Resolution

Instead of a static list of nodes, you can provide a module-function-args tuple that will be called at runtime to get the current list of nodes:

nodes: {MyApp.NodeRegistry, :get_active_nodes, []}

This allows for dynamic node discovery and automatic adaptation to cluster changes.

Node Selection for Retry/Fallback

The get_nodes/2 function returns a list of nodes suitable for the retry strategy:

  • :random and :hash return a single-node list (the selected node)
  • :round_robin returns all nodes (for fallback to other nodes on failure)

Fault Tolerance

  • Nodes are validated before selection
  • Empty node lists return {:error, :no_nodes_available}
  • Invalid node formats are filtered out
  • Failed node resolution is logged with details

Examples

# Random selection
config = %FunConfig{
  nodes: ["node1@host", "node2@host", "node3@host"],
  choose_node_mode: :random
}
{:ok, node} = NodeSelector.get_node(config, request)

# Hash by request ID (consistent)
config = %FunConfig{
  nodes: ["node1@host", "node2@host"],
  choose_node_mode: :hash
}
{:ok, node} = NodeSelector.get_node(config, request)

# Hash by custom field
request = %Request{
  request_id: "req_123",
  user_id: "user_456",
  args: %{"session_id" => "sess_789"}
}

config = %FunConfig{
  nodes: ["node1@host", "node2@host"],
  choose_node_mode: {:hash, "user_id"}
}
{:ok, node} = NodeSelector.get_node(config, request)

# Round-robin (global, atomic counter)
config = %FunConfig{
  nodes: ["node1@host", "node2@host", "node3@host"],
  choose_node_mode: :round_robin
}
{:ok, node1} = NodeSelector.get_node(config, request)
{:ok, node2} = NodeSelector.get_node(config, request)
{:ok, node3} = NodeSelector.get_node(config, request)
{:ok, node1_again} = NodeSelector.get_node(config, request)  # wraps around

Notes

  • Round-robin uses an atomic counter for true global distribution (no process dictionary)
  • Hash functions use :erlang.phash2/2 for deterministic hashing
  • Dynamic node resolution happens on every call, allowing real-time cluster updates
  • If a hash_key is not found in the request, falls back to random selection
  • Returns {:ok, node} on success or {:error, reason} on failure

Summary

Functions

Calculates a retry backoff delay based on the attempt number.

Validates that the choose_node_mode is a recognized strategy.

Selects a single target node based on the configuration and request.

Selects a list of target nodes based on the configuration and request.

Resets the round-robin counter.

Resolves dynamic node configuration to a concrete node list.

Resolves nodes and returns the raw node list regardless of configuration type.

Functions

calculate_backoff(attempt, opts \\ [])

@spec calculate_backoff(
  pos_integer(),
  keyword()
) :: non_neg_integer()

Calculates a retry backoff delay based on the attempt number.

Uses exponential backoff with jitter to prevent thundering herd problems.

Parameters

  • attempt - The current attempt number (1-based)
  • opts - Options keyword list:
    • :base_ms - Base delay in milliseconds (default: 100)
    • :max_ms - Maximum delay in milliseconds (default: 5000)
    • :jitter - Whether to add random jitter (default: true)

Returns

  • Delay in milliseconds before the next retry

Examples

iex> NodeSelector.calculate_backoff(1)
# Returns ~100ms (with jitter)

iex> NodeSelector.calculate_backoff(3)
# Returns ~400ms (with jitter)

iex> NodeSelector.calculate_backoff(5, max_ms: 2000)
# Returns capped at ~2000ms (with jitter)

choose_node_valid?(fun_config)

@spec choose_node_valid?(PhoenixGenApi.Structs.FunConfig.t()) :: boolean()

Validates that the choose_node_mode is a recognized strategy.

Returns

  • true if the mode is valid
  • false otherwise

get_node(config, request)

@spec get_node(PhoenixGenApi.Structs.FunConfig.t(), PhoenixGenApi.Structs.Request.t()) ::
  {:ok, node :: atom() | String.t()} | {:error, term()}

Selects a single target node based on the configuration and request.

This function examines the choose_node_mode in the configuration and applies the appropriate node selection strategy. If the nodes field is a tuple, it will be called as a function to dynamically resolve the node list.

Parameters

  • config - A FunConfig struct containing:

    • nodes - Either a list of node names or a {module, function, args} tuple
    • choose_node_mode - The selection strategy (:random, :hash, {:hash, key}, :round_robin)
  • request - A Request struct containing the request details

Returns

  • {:ok, node} - The selected node
  • {:error, reason} - Selection failed

Examples

config = %FunConfig{
  nodes: ["node1@host", "node2@host"],
  choose_node_mode: :random
}

request = %Request{
  request_id: "req_123",
  request_type: "get_user",
  user_id: "user_456",
  args: %{"user_id" => "user_456"}
}

{:ok, node} = NodeSelector.get_node(config, request)

get_nodes(config, request)

@spec get_nodes(
  PhoenixGenApi.Structs.FunConfig.t(),
  PhoenixGenApi.Structs.Request.t()
) ::
  {:ok, [atom() | String.t()]} | {:error, term()}

Selects a list of target nodes based on the configuration and request.

The returned list is ordered by preference for fallback/retry purposes:

  • :random - Returns a shuffled list of all nodes (selected node first)
  • :hash / {:hash, key} - Returns all nodes with the hashed node first
  • :round_robin - Returns all nodes starting from the round-robin position

This is useful for the executor's fallback mechanism where if the primary node fails, it tries the remaining nodes in order.

Parameters

  • config - A FunConfig struct
  • request - A Request struct

Returns

  • {:ok, [node, ...]} - Ordered list of nodes (primary first)
  • {:error, reason} - Selection failed

Examples

config = %FunConfig{
  nodes: ["node1@host", "node2@host", "node3@host"],
  choose_node_mode: :random
}

{:ok, [primary | fallbacks]} = NodeSelector.get_nodes(config, request)

reset_round_robin()

@spec reset_round_robin() :: :ok

Resets the round-robin counter.

This is primarily useful for testing. In production, the counter should be allowed to increment naturally.

resolve_nodes(config)

@spec resolve_nodes(PhoenixGenApi.Structs.FunConfig.t()) ::
  {:ok, PhoenixGenApi.Structs.FunConfig.t()} | {:error, term()}

Resolves dynamic node configuration to a concrete node list.

If nodes is an MFA tuple {module, function, args}, calls the function to get the node list at runtime. If nodes is already a list, returns the config unchanged. If nodes is :local, returns the config unchanged.

Parameters

  • config - A FunConfig struct

Returns

  • {:ok, %FunConfig{}} - Config with resolved nodes
  • {:error, reason} - Resolution failed

resolve_nodes_list(config)

@spec resolve_nodes_list(PhoenixGenApi.Structs.FunConfig.t()) ::
  {:ok, [atom() | String.t()]} | {:error, term()}

Resolves nodes and returns the raw node list regardless of configuration type.

Unlike resolve_nodes/1 which returns the full config, this returns just the list of nodes. Useful for getting all available nodes for retry strategies.

Parameters

  • config - A FunConfig struct

Returns

  • {:ok, [node]} - List of resolved nodes
  • {:error, reason} - Resolution failed