# Concurrency Design

STA Connector uses Elixir/OTP concurrency primitives for building reliable, fault-tolerant file processing pipelines on the BEAM VM.

## BEAM VM Concurrency Model

The BEAM (Bogdan's Erlang Abstract Machine) provides a powerful concurrency model that forms the foundation of all Elixir applications:

### Lightweight Processes

- BEAM processes are **not OS threads** - they are lightweight, isolated units of execution
- Each process has its own heap (~2KB initial), stack, and mailbox
- A single BEAM VM can run **millions** of concurrent processes
- Processes communicate exclusively via **message passing** (no shared memory)
- Memory is garbage collected per-process, avoiding global GC pauses

### Preemptive Scheduling

- BEAM uses **preemptive scheduling** with reduction counting
- Each process gets ~4000 reductions (roughly function calls) before being preempted
- Ensures fair CPU distribution - no single process can monopolize the scheduler
- Soft real-time guarantees for all processes
- Schedulers automatically balance work across available CPU cores

### Isolation and Fault Tolerance

- Process crashes are **isolated** - one process failure doesn't affect others
- "Let it crash" philosophy - supervisors restart failed processes automatically
- No defensive programming needed - focus on the happy path

## Component Architecture

```
+-------------------------------------------------------------------------+
|                        Application Supervisor                            |
|                        (one_for_one strategy)                           |
+-------------------------------------------------------------------------+
|                                                                          |
|  +-------------+  +-------------+  +-------------+  +-----------------+ |
|  |   Config    |  |    Store    |  | STA Client  |  |     Metrics     | |
|  |  GenServer  |  |  (Ecto +    |  |  GenServer  |  |   GenServer     | |
|  |             |  |   SQLite)   |  |  + Pool     |  |   (Telemetry)   | |
|  +------+------+  +------+------+  +------+------+  +--------+--------+ |
|         |                |                |                  |          |
|  +------+----------------+----------------+------------------+--------+ |
|  |                   Shared Dependencies (Registry)                    | |
|  +----------------------------+----------------------------------------+ |
|                               |                                          |
|         +--------------------+|+--------------------+                   |
|         |                    ||                     |                   |
|  +------v------+      +------v------+        +------v------+           |
|  |   Poller    |      |  Outbound   |        |   Admin     |           |
|  | GenServer   |      |   Phoenix   |        |   Phoenix   |           |
|  +------+------+      |   Endpoint  |        |   Endpoint  |           |
|         |             +-------------+        +-------------+           |
|         |                                                               |
|  +------v--------------------------------------------------+           |
|  |              Worker Pool (Task.Supervisor)               |           |
|  |  +----------+ +----------+ +----------+ +----------+    |           |
|  |  | Worker 1 | | Worker 2 | | Worker 3 | | Worker N |    |           |
|  |  |  (Task)  | |  (Task)  | |  (Task)  | |  (Task)  |    |           |
|  |  +----------+ +----------+ +----------+ +----------+    |           |
|  +----------------------------------------------------------+           |
|                                                                          |
+-------------------------------------------------------------------------+
```

## OTP Supervision Trees

Supervision trees provide fault tolerance through hierarchical process management:

### Application Supervisor

```elixir
# lib/sta_connector/application.ex
defmodule STAConnector.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Core infrastructure
      STAConnector.Config,
      STAConnector.Repo,
      STAConnector.Metrics,

      # STA client with connection pool
      {STAConnector.STA.ClientSupervisor, []},

      # Worker pool for file processing
      {Task.Supervisor, name: STAConnector.WorkerPool},

      # Inbound polling (if enabled)
      {STAConnector.Poller, enabled: config(:poller_enabled)},

      # HTTP endpoints
      STAConnector.OutboundEndpoint,
      STAConnector.AdminEndpoint
    ]

    opts = [strategy: :one_for_one, name: STAConnector.Supervisor]
    Supervisor.start_link(children, opts)
  end

  defp config(key), do: Application.get_env(:sta_connector, key)
end
```

### Supervision Strategies

| Strategy | Behavior | Use Case |
|----------|----------|----------|
| `:one_for_one` | Restart only the failed child | Independent processes |
| `:one_for_all` | Restart all children if one fails | Tightly coupled processes |
| `:rest_for_one` | Restart failed child and those started after it | Sequential dependencies |

### Child Specifications

```elixir
# Custom child spec with restart configuration
defmodule STAConnector.Poller do
  use GenServer, restart: :permanent

  def child_spec(opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [opts]},
      restart: :permanent,      # Always restart
      shutdown: 30_000,         # 30s graceful shutdown
      type: :worker
    }
  end
end
```

## GenServers for Stateful Processes

GenServer is the primary abstraction for stateful, long-running processes:

### Configuration Manager

```elixir
# lib/sta_connector/config.ex
defmodule STAConnector.Config do
  use GenServer
  require Logger

  # Client API

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def get(key) do
    GenServer.call(__MODULE__, {:get, key})
  end

  def reload do
    GenServer.call(__MODULE__, :reload)
  end

  def subscribe do
    GenServer.call(__MODULE__, {:subscribe, self()})
  end

  # Server Callbacks

  @impl true
  def init(opts) do
    config_path = Keyword.fetch!(opts, :config_path)

    case load_config(config_path) do
      {:ok, config} ->
        # Watch for file changes
        {:ok, _} = FileSystem.start_link(dirs: [Path.dirname(config_path)])
        FileSystem.subscribe()

        {:ok, %{path: config_path, config: config, subscribers: MapSet.new()}}

      {:error, reason} ->
        {:stop, reason}
    end
  end

  @impl true
  def handle_call({:get, key}, _from, state) do
    value = get_in(state.config, List.wrap(key))
    {:reply, value, state}
  end

  @impl true
  def handle_call(:reload, _from, state) do
    case load_config(state.path) do
      {:ok, new_config} ->
        Logger.info("Configuration reloaded")
        notify_subscribers(state.subscribers, :config_changed)
        {:reply, :ok, %{state | config: new_config}}

      {:error, reason} ->
        Logger.error("Failed to reload config: #{inspect(reason)}")
        {:reply, {:error, reason}, state}
    end
  end

  @impl true
  def handle_call({:subscribe, pid}, _from, state) do
    Process.monitor(pid)
    {:reply, :ok, %{state | subscribers: MapSet.put(state.subscribers, pid)}}
  end

  @impl true
  def handle_info({:file_event, _, _}, state) do
    # Hot-reload on file change
    GenServer.cast(self(), :reload)
    {:noreply, state}
  end

  @impl true
  def handle_info({:DOWN, _, :process, pid, _}, state) do
    {:noreply, %{state | subscribers: MapSet.delete(state.subscribers, pid)}}
  end

  defp load_config(path) do
    with {:ok, content} <- File.read(path),
         {:ok, config} <- YamlElixir.read_from_string(content) do
      {:ok, config}
    end
  end

  defp notify_subscribers(subscribers, message) do
    Enum.each(subscribers, &send(&1, {__MODULE__, message}))
  end
end
```

### Poller GenServer

```elixir
# lib/sta_connector/poller.ex
defmodule STAConnector.Poller do
  use GenServer
  require Logger

  @default_interval :timer.seconds(30)
  @default_batch_size 100

  # Client API

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def pause do
    GenServer.call(__MODULE__, :pause)
  end

  def resume do
    GenServer.call(__MODULE__, :resume)
  end

  def status do
    GenServer.call(__MODULE__, :status)
  end

  # Server Callbacks

  @impl true
  def init(opts) do
    if Keyword.get(opts, :enabled, true) do
      state = %{
        interval: Keyword.get(opts, :interval, @default_interval),
        batch_size: Keyword.get(opts, :batch_size, @default_batch_size),
        systems: Keyword.get(opts, :systems, []),
        paused: false,
        stats: %{polls: 0, files_processed: 0, errors: 0}
      }

      # Schedule first poll
      schedule_poll(state.interval)

      {:ok, state}
    else
      :ignore
    end
  end

  @impl true
  def handle_call(:pause, _from, state) do
    Logger.info("Poller paused")
    {:reply, :ok, %{state | paused: true}}
  end

  @impl true
  def handle_call(:resume, _from, state) do
    Logger.info("Poller resumed")
    schedule_poll(0)  # Poll immediately
    {:reply, :ok, %{state | paused: false}}
  end

  @impl true
  def handle_call(:status, _from, state) do
    {:reply, Map.take(state, [:paused, :stats, :interval]), state}
  end

  @impl true
  def handle_info(:poll, %{paused: true} = state) do
    # Don't poll when paused, but keep scheduling
    schedule_poll(state.interval)
    {:noreply, state}
  end

  @impl true
  def handle_info(:poll, state) do
    state = poll_and_process(state)
    schedule_poll(state.interval)
    {:noreply, state}
  end

  defp schedule_poll(interval) do
    Process.send_after(self(), :poll, interval)
  end

  defp poll_and_process(state) do
    case STAConnector.STA.Client.query_available_files(state.systems, state.batch_size) do
      {:ok, files} ->
        # Spawn tasks for each file using Task.Supervisor
        files
        |> Enum.reject(&STAConnector.Store.processed?(&1.protocol))
        |> Enum.each(fn file ->
          Task.Supervisor.start_child(
            STAConnector.WorkerPool,
            fn -> process_file(file) end
          )
        end)

        update_stats(state, :polls, 1)

      {:error, reason} ->
        Logger.error("Poll failed: #{inspect(reason)}")
        update_stats(state, :errors, 1)
    end
  end

  defp process_file(file) do
    Logger.info("Processing file", protocol: file.protocol, system: file.system)

    with {:ok, content} <- STAConnector.STA.Client.download(file.protocol),
         {:ok, parsed} <- STAConnector.Parser.parse(file.system, content),
         :ok <- STAConnector.Router.route(file.system, parsed),
         :ok <- STAConnector.STA.Client.confirm_receipt(file.protocol),
         :ok <- STAConnector.Store.mark_processed(file.protocol, file.system) do
      Logger.info("File processed successfully", protocol: file.protocol)
      :ok
    else
      {:error, reason} ->
        Logger.error("File processing failed",
          protocol: file.protocol,
          reason: inspect(reason)
        )
        {:error, reason}
    end
  end

  defp update_stats(state, key, increment) do
    update_in(state, [:stats, key], &((&1 || 0) + increment))
  end
end
```

## Phoenix Channels for Real-Time

Phoenix Channels provide real-time, bidirectional communication:

### Admin Dashboard Channel

```elixir
# lib/sta_connector_web/channels/admin_channel.ex
defmodule STAConnectorWeb.AdminChannel do
  use Phoenix.Channel
  require Logger

  @impl true
  def join("admin:dashboard", _params, socket) do
    # Subscribe to metrics updates
    Phoenix.PubSub.subscribe(STAConnector.PubSub, "metrics")

    # Send initial state
    send(self(), :send_initial_state)

    {:ok, socket}
  end

  @impl true
  def handle_info(:send_initial_state, socket) do
    push(socket, "state", %{
      poller: STAConnector.Poller.status(),
      metrics: STAConnector.Metrics.snapshot(),
      config: STAConnector.Config.get([:routes])
    })
    {:noreply, socket}
  end

  @impl true
  def handle_info({:metrics_update, metrics}, socket) do
    push(socket, "metrics", metrics)
    {:noreply, socket}
  end

  @impl true
  def handle_in("poller:pause", _params, socket) do
    :ok = STAConnector.Poller.pause()
    broadcast!(socket, "poller:status", %{paused: true})
    {:reply, :ok, socket}
  end

  @impl true
  def handle_in("poller:resume", _params, socket) do
    :ok = STAConnector.Poller.resume()
    broadcast!(socket, "poller:status", %{paused: false})
    {:reply, :ok, socket}
  end

  @impl true
  def handle_in("config:reload", _params, socket) do
    case STAConnector.Config.reload() do
      :ok ->
        broadcast!(socket, "config:changed", %{})
        {:reply, :ok, socket}

      {:error, reason} ->
        {:reply, {:error, %{reason: inspect(reason)}}, socket}
    end
  end
end
```

### Broadcasting Metrics

```elixir
# lib/sta_connector/metrics.ex
defmodule STAConnector.Metrics do
  use GenServer

  @broadcast_interval :timer.seconds(5)

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def snapshot do
    GenServer.call(__MODULE__, :snapshot)
  end

  def record(event, measurements, metadata \\ %{}) do
    GenServer.cast(__MODULE__, {:record, event, measurements, metadata})
  end

  @impl true
  def init(_opts) do
    # Attach telemetry handlers
    :telemetry.attach_many(
      "sta-metrics",
      [
        [:sta, :request, :stop],
        [:sta, :file, :processed],
        [:sta, :file, :error]
      ],
      &handle_telemetry/4,
      nil
    )

    schedule_broadcast()

    {:ok, %{
      requests: 0,
      files_processed: 0,
      errors: 0,
      latency_sum: 0,
      latency_count: 0
    }}
  end

  @impl true
  def handle_call(:snapshot, _from, state) do
    snapshot = %{
      requests_total: state.requests,
      files_processed_total: state.files_processed,
      errors_total: state.errors,
      avg_latency_ms: safe_div(state.latency_sum, state.latency_count)
    }
    {:reply, snapshot, state}
  end

  @impl true
  def handle_info(:broadcast, state) do
    Phoenix.PubSub.broadcast(
      STAConnector.PubSub,
      "metrics",
      {:metrics_update, snapshot_from_state(state)}
    )
    schedule_broadcast()
    {:noreply, state}
  end

  defp schedule_broadcast do
    Process.send_after(self(), :broadcast, @broadcast_interval)
  end

  defp handle_telemetry([:sta, :request, :stop], measurements, _metadata, _config) do
    record(:request, measurements)
  end

  defp safe_div(_, 0), do: 0
  defp safe_div(a, b), do: a / b

  defp snapshot_from_state(state) do
    %{
      requests_total: state.requests,
      files_processed_total: state.files_processed,
      errors_total: state.errors,
      avg_latency_ms: safe_div(state.latency_sum, state.latency_count)
    }
  end
end
```

## Task and Task.Supervisor for Async Work

### Fire-and-Forget Tasks

```elixir
# Supervised async task - failures are logged but don't crash parent
Task.Supervisor.start_child(STAConnector.WorkerPool, fn ->
  process_file(file)
end)
```

### Async with Results

```elixir
# Process multiple files concurrently with result collection
defmodule STAConnector.BatchProcessor do
  def process_batch(files, timeout \\ 30_000) do
    files
    |> Enum.map(fn file ->
      Task.Supervisor.async_nolink(STAConnector.WorkerPool, fn ->
        process_file(file)
      end)
    end)
    |> Task.yield_many(timeout)
    |> Enum.map(fn
      {task, {:ok, result}} -> {:ok, result}
      {task, {:exit, reason}} -> {:error, {:exit, reason}}
      {task, nil} ->
        Task.shutdown(task, :brutal_kill)
        {:error, :timeout}
    end)
  end
end
```

### Parallel Map with Concurrency Control

```elixir
# Process files with max 10 concurrent operations
defmodule STAConnector.ParallelProcessor do
  def parallel_map(items, fun, max_concurrency \\ 10) do
    items
    |> Task.async_stream(
      fun,
      max_concurrency: max_concurrency,
      timeout: 60_000,
      on_timeout: :kill_task,
      ordered: false
    )
    |> Enum.map(fn
      {:ok, result} -> result
      {:exit, reason} -> {:error, reason}
    end)
  end
end
```

## Ecto Connection Pools

### Database Configuration

```elixir
# config/config.exs
config :sta_connector, STAConnector.Repo,
  database: "sta_connector.db",
  pool_size: 10,
  queue_target: 500,     # Target checkout time in ms
  queue_interval: 1000   # Interval to check queue health
```

### Repo Module

```elixir
# lib/sta_connector/repo.ex
defmodule STAConnector.Repo do
  use Ecto.Repo,
    otp_app: :sta_connector,
    adapter: Ecto.Adapters.SQLite3
end
```

### Connection Pool Behavior

| Parameter | Default | Description |
|-----------|---------|-------------|
| `pool_size` | 10 | Number of connections in the pool |
| `queue_target` | 50ms | Target time for checkout |
| `queue_interval` | 1000ms | Health check interval |

When load exceeds pool capacity:
1. Requests queue for an available connection
2. If queue time exceeds `queue_target`, pool logs warnings
3. DBConnection automatically handles connection failures and reconnects

### Using Transactions

```elixir
# Automatic transaction retry on serialization failures
defmodule STAConnector.Store do
  alias STAConnector.Repo
  alias STAConnector.Schema.ProcessedFile

  def mark_processed(protocol, system) do
    Repo.transaction(fn ->
      %ProcessedFile{}
      |> ProcessedFile.changeset(%{
        protocol: protocol,
        system: system,
        processed_at: DateTime.utc_now()
      })
      |> Repo.insert!()
    end)
  end

  def processed?(protocol) do
    Repo.exists?(
      from p in ProcessedFile,
      where: p.protocol == ^protocol
    )
  end
end
```

## Rate Limiter with GenServer

```elixir
# lib/sta_connector/sta/rate_limiter.ex
defmodule STAConnector.STA.RateLimiter do
  use GenServer

  @max_concurrent 10
  @max_per_minute 120

  defstruct [:concurrent, :minute_bucket, :minute_timer]

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def acquire(timeout \\ 5000) do
    GenServer.call(__MODULE__, :acquire, timeout)
  end

  def release do
    GenServer.cast(__MODULE__, :release)
  end

  def status do
    GenServer.call(__MODULE__, :status)
  end

  @impl true
  def init(_opts) do
    state = %__MODULE__{
      concurrent: 0,
      minute_bucket: @max_per_minute,
      minute_timer: schedule_refill()
    }
    {:ok, state}
  end

  @impl true
  def handle_call(:acquire, from, state) do
    cond do
      state.concurrent >= @max_concurrent ->
        # Queue the request - will be processed when a slot opens
        {:noreply, state, {:continue, {:queue, from}}}

      state.minute_bucket <= 0 ->
        {:reply, {:error, :rate_limited}, state}

      true ->
        new_state = %{state |
          concurrent: state.concurrent + 1,
          minute_bucket: state.minute_bucket - 1
        }
        {:reply, :ok, new_state}
    end
  end

  @impl true
  def handle_call(:status, _from, state) do
    status = %{
      concurrent_used: state.concurrent,
      concurrent_max: @max_concurrent,
      minute_remaining: state.minute_bucket,
      minute_max: @max_per_minute
    }
    {:reply, status, state}
  end

  @impl true
  def handle_cast(:release, state) do
    {:noreply, %{state | concurrent: max(0, state.concurrent - 1)}}
  end

  @impl true
  def handle_info(:refill_bucket, state) do
    new_state = %{state |
      minute_bucket: @max_per_minute,
      minute_timer: schedule_refill()
    }
    {:noreply, new_state}
  end

  defp schedule_refill do
    Process.send_after(self(), :refill_bucket, :timer.minutes(1))
  end
end
```

## Circuit Breaker with GenServer

```elixir
# lib/sta_connector/sta/circuit_breaker.ex
defmodule STAConnector.STA.CircuitBreaker do
  use GenServer
  require Logger

  @failure_threshold 5
  @recovery_timeout :timer.seconds(30)
  @half_open_successes 3

  defmodule State do
    defstruct [
      :status,        # :closed, :open, :half_open
      :failures,
      :successes,
      :last_failure,
      :recovery_timer
    ]
  end

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def execute(fun) do
    case GenServer.call(__MODULE__, :check) do
      :ok ->
        result = fun.()
        GenServer.cast(__MODULE__, {:record, result_status(result)})
        result

      {:error, :circuit_open} = error ->
        error
    end
  end

  def status do
    GenServer.call(__MODULE__, :status)
  end

  def reset do
    GenServer.call(__MODULE__, :reset)
  end

  @impl true
  def init(_opts) do
    state = %State{
      status: :closed,
      failures: 0,
      successes: 0,
      last_failure: nil,
      recovery_timer: nil
    }
    {:ok, state}
  end

  @impl true
  def handle_call(:check, _from, %{status: :open} = state) do
    {:reply, {:error, :circuit_open}, state}
  end

  @impl true
  def handle_call(:check, _from, state) do
    {:reply, :ok, state}
  end

  @impl true
  def handle_call(:status, _from, state) do
    {:reply, state.status, state}
  end

  @impl true
  def handle_call(:reset, _from, state) do
    if state.recovery_timer, do: Process.cancel_timer(state.recovery_timer)
    {:reply, :ok, %State{status: :closed, failures: 0, successes: 0}}
  end

  @impl true
  def handle_cast({:record, :success}, %{status: :half_open} = state) do
    new_successes = state.successes + 1

    if new_successes >= @half_open_successes do
      Logger.info("Circuit breaker closed after recovery")
      {:noreply, %State{status: :closed, failures: 0, successes: 0}}
    else
      {:noreply, %{state | successes: new_successes}}
    end
  end

  @impl true
  def handle_cast({:record, :success}, state) do
    {:noreply, %{state | failures: 0}}
  end

  @impl true
  def handle_cast({:record, :failure}, state) do
    new_failures = state.failures + 1

    if new_failures >= @failure_threshold and state.status == :closed do
      Logger.warning("Circuit breaker opened after #{new_failures} failures")
      timer = Process.send_after(self(), :attempt_recovery, @recovery_timeout)
      {:noreply, %{state |
        status: :open,
        failures: new_failures,
        last_failure: DateTime.utc_now(),
        recovery_timer: timer
      }}
    else
      {:noreply, %{state | failures: new_failures, last_failure: DateTime.utc_now()}}
    end
  end

  @impl true
  def handle_info(:attempt_recovery, state) do
    Logger.info("Circuit breaker entering half-open state")
    {:noreply, %{state | status: :half_open, successes: 0, recovery_timer: nil}}
  end

  defp result_status({:ok, _}), do: :success
  defp result_status(:ok), do: :success
  defp result_status(_), do: :failure
end
```

## Retry with Exponential Backoff

```elixir
# lib/sta_connector/retry.ex
defmodule STAConnector.Retry do
  require Logger

  @default_opts [
    initial_delay: 100,
    max_delay: 30_000,
    multiplier: 2,
    max_retries: 5,
    jitter: 0.25
  ]

  def with_backoff(fun, opts \\ []) do
    opts = Keyword.merge(@default_opts, opts)
    do_retry(fun, opts, 0, opts[:initial_delay])
  end

  defp do_retry(fun, opts, attempt, delay) do
    case fun.() do
      {:ok, _} = success ->
        success

      :ok ->
        :ok

      {:error, reason} = error ->
        if attempt >= opts[:max_retries] do
          Logger.error("Max retries exceeded",
            attempt: attempt,
            reason: inspect(reason)
          )
          error
        else
          if retryable?(reason) do
            jittered_delay = apply_jitter(delay, opts[:jitter])

            Logger.debug("Retrying after error",
              attempt: attempt + 1,
              delay_ms: jittered_delay,
              reason: inspect(reason)
            )

            Process.sleep(jittered_delay)

            next_delay = min(
              round(delay * opts[:multiplier]),
              opts[:max_delay]
            )

            do_retry(fun, opts, attempt + 1, next_delay)
          else
            error
          end
        end
    end
  end

  defp retryable?(%STAConnector.STA.Error{status: status})
    when status >= 500 or status == 429, do: true
  defp retryable?(%STAConnector.STA.ConnectionError{}), do: true
  defp retryable?(:timeout), do: true
  defp retryable?(_), do: false

  defp apply_jitter(delay, jitter_ratio) do
    jitter = round(delay * jitter_ratio * :rand.uniform())
    delay + jitter
  end
end
```

## Graceful Shutdown

```elixir
# lib/sta_connector/application.ex
defmodule STAConnector.Application do
  use Application
  require Logger

  @impl true
  def start(_type, _args) do
    children = [
      # ... children ...
    ]

    opts = [strategy: :one_for_one, name: STAConnector.Supervisor]
    Supervisor.start_link(children, opts)
  end

  @impl true
  def stop(_state) do
    Logger.info("Application shutting down...")

    # Drain the worker pool (wait for in-flight tasks)
    drain_worker_pool(30_000)

    Logger.info("Shutdown complete")
    :ok
  end

  defp drain_worker_pool(timeout) do
    # Get all running tasks
    children = Task.Supervisor.children(STAConnector.WorkerPool)

    if length(children) > 0 do
      Logger.info("Waiting for #{length(children)} tasks to complete...")

      # Wait for tasks with timeout
      Task.Supervisor.await_completion(STAConnector.WorkerPool, timeout)
    end
  end
end
```

## Best Practices

### 1. Process Naming and Registration

```elixir
# Use Registry for dynamic process names
{:ok, _} = Registry.start_link(keys: :unique, name: STAConnector.Registry)

# Start worker with unique name
GenServer.start_link(Worker, args,
  name: {:via, Registry, {STAConnector.Registry, {:worker, id}}}
)

# Lookup worker
case Registry.lookup(STAConnector.Registry, {:worker, id}) do
  [{pid, _}] -> {:ok, pid}
  [] -> {:error, :not_found}
end
```

### 2. Message Passing Patterns

```elixir
# Prefer call for requests that need responses
def get_status(server) do
  GenServer.call(server, :status)
end

# Use cast for fire-and-forget
def increment_counter(server) do
  GenServer.cast(server, :increment)
end

# Use send for internal messages
Process.send_after(self(), :tick, 1000)
```

### 3. Error Handling in GenServer

```elixir
@impl true
def handle_call(:risky_operation, _from, state) do
  case perform_operation(state) do
    {:ok, result} ->
      {:reply, {:ok, result}, state}

    {:error, reason} ->
      # Don't crash - return error to caller
      {:reply, {:error, reason}, state}
  end
rescue
  e ->
    Logger.error("Unexpected error: #{inspect(e)}")
    {:reply, {:error, :internal_error}, state}
end
```

### 4. Telemetry Integration

```elixir
# Instrument operations with telemetry
def process_file(file) do
  start_time = System.monotonic_time()
  metadata = %{protocol: file.protocol, system: file.system}

  :telemetry.execute([:sta, :file, :start], %{}, metadata)

  result = do_process_file(file)

  duration = System.monotonic_time() - start_time
  :telemetry.execute(
    [:sta, :file, :stop],
    %{duration: duration},
    Map.put(metadata, :result, result_type(result))
  )

  result
end
```

## Next Steps

- [Database Schema](/architecture/database) - Data model design
- [Architecture Overview](/architecture/) - System components
- [API Reference](/api/) - REST API documentation
