# `Ankusa.Reload`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/reload.ex#L1)

Changing a running instance's config without a restart: the part of a config
reload that core owns.

What a reload *is* (re-reading a file, resolving secrets, deciding which keys
changed and whether they may) belongs to whoever loads the config; for the
image that is `AnkusaServer.Reload`. This module is the seam between that and
the instance:

  * `register/2` / `run/2` — the loader registers a reload function for an
    instance, and `POST /v1/config/reload` (`Ankusa.Admin.Router`) calls it.
    An instance nobody registered one for answers `{:error, :unsupported}`.
  * `apply_config/3` — what the loader calls once it holds a validated
    `%Ankusa.Config{}`: the *reloadable set* is taken from it, everything else
    stays as it is, and the result is applied atomically.

## The reloadable set

| Field | How it takes effect |
| --- | --- |
| `rate_limits` (all but `scope`) | read on every request |
| `dispatch.concurrency`, `dispatch.sink_concurrency`, `dispatch.tenants` | `Ankusa.Dispatch.Pipeline.reconfigure/1`: the next scheduling step |
| `metrics.detail` | the instance's metric definitions are rebuilt (`Ankusa.Metrics` restarts: its counters start from zero) |
| `admin.gauge_interval_ms` | the gauge poller restarts with the new period |
| `routes.seed` | the difference is applied through `Ankusa.Routes`: a changed or new seed route is replaced, a route that left the seed is deleted; routes created through the API are left alone |
| static `sources` | `Ankusa.SourceStore.Static.with_sources/2`: the next `fetch/2` sees them |

Every other field of the running config is kept, whatever `next` says; the
loader refuses a reload that changes one (`{:error, {:not_reloadable, keys}}`).

## Atomicity and audit

The merged config is validated with the checks the instance boots under
before anything is touched, so a reload that would not boot is refused with
nothing applied. One audit row (`config.reload`, `detail.keys`) brackets the
application; its `result` is `ok` or `failed`. A reload the trail cannot record
is not made (`:audit_backlog`, `:store_unavailable`). The seed routes are
applied one by one through `Ankusa.Routes` (each its own audited change), so a
seed route that conflicts with a route made through the API fails the reload
part-way: the routes already applied stay, the config is not replaced, and the
same reload can be run again.

# `error`

```elixir
@type error() ::
  {:not_reloadable, [String.t()]}
  | {:invalid_config, String.t()}
  | :unsupported
  | :audit_backlog
  | :store_unavailable
```

# `summary`

```elixir
@type summary() :: %{
  keys: [String.t()],
  sources: [String.t()],
  kept: [%{path: String.t(), reason: String.t()}]
}
```

What a reload did: the dotted keys that changed, the source ids whose spec changed, secrets that kept their old value.

# `apply_config`

```elixir
@spec apply_config(atom(), Ankusa.Config.t(), keyword()) ::
  {:ok, %{sources: [String.t()]}} | {:error, error()}
```

Apply the reloadable fields of `next` to the running `instance`.

Options: `:audit` (the `Ankusa.Audit.Context`, default `Context.system/0`) and
`:keys` (the dotted config keys the loader found changed, recorded in the audit
row). Returns `{:ok, %{sources: ids}}` with the source ids whose spec changed,
or `{:error, {:invalid_config, message}}` and nothing applied when the merged
config would not boot (or a seed route is refused part-way, see above).

# `register`

```elixir
@spec register(atom(), (Ankusa.Audit.Context.t() -&gt;
                    {:ok, summary()} | {:error, error()})) :: :ok
```

Register `fun` as `instance`'s reload: it takes the `Ankusa.Audit.Context` of
whoever asked and returns `{:ok, summary}` or `{:error, error}`.

# `run`

```elixir
@spec run(atom(), Ankusa.Audit.Context.t()) :: {:ok, summary()} | {:error, error()}
```

Run `instance`'s registered reload on behalf of `context`.

---

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