# `PhoenixGenApi.NodeSelector`
[🔗](https://github.com/ohhi-vn/phoenix_gen_api/blob/main/lib/phoenix_gen_api/node_selector.ex#L1)

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

# `calculate_backoff`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
