# `Rho.Dyn.Keeper`
[🔗](https://github.com/GenericJam/rho/blob/v0.1.0/lib/rho/dyn/keeper.ex#L1)

The Core process that decides which Dyn generation runs (activation,
probation, revert and safe mode, steps 3–4 in `Rho.Dyn`). It owns the
registry table (`Rho.Dyn.Registry`) and is the only writer of the
`current` pointer.

**Activation** (`activate/3`, approval required) loads the generation,
flips the pointer, then swaps the registry: a crash between the two
leaves the old pointer, or the new pointer and a registry the next start
rebuilds from it. The new generation is on **probation** until it has
run 60 s without a Dyn crash (`quiet`) and a later app start reached
stable (`restarted`); then it is proven. An approval token is accepted
once (see `Rho.Dyn.Approval`).

**Crashes.** Dyn processes are watched (`watch/3`: tool calls through
`Rho.ToolRunner` and `Rho.Dyn.Source`, screens through the wrapper the
compiler adds to their entry function, `Rho.Dyn.Screens`). An abnormal
exit (crash, kill, timeout) is written to the
log and sent to subscribers as `{:rho_dyn, %{type: :crash, ...}}`, its
`reason` a string that describes the exit without running any of it
(`Rho.Untrusted.exit_text/2`).
3 crashes of the current generation within 60 s while it is on probation
revert it to its parent (`%{type: :reverted, crashes: [...]}`, the crash
reports the agent fixes from): the parent is loaded first (generation 0,
no Dyn at all, if the parent can't be), then the pointer flips. If the
pointer can't be written, `:revert_failed` is reported and the crashes
stay counted, so the next one tries again. A proven generation's crashes
are only reported.

**Launches** (`boot/2`, once per app start). `boot.json` counts the
launches since the last one that reached stable (`mark_stable/1`). In the
app a launch is stable once its first frame was drawn (the host's
`c:Rho.Dyn.Screens.after_first_render/1`) and then either
`:stable_delay_ms` passed or the app went to the background (the user
left it, `c:Rho.Dyn.Screens.left_app?/1`: a quick close is not a failed
launch; a crash before the first frame or within the delay is). If the
previous launch never got there and
the current generation is still unproven, it is reverted before anything
loads (boot probation). Two such launches in a row: **safe mode**, the
Core boots without any Dyn module and `Rho.Dyn.safe_mode?/0`
says so; activations and reverts then only move the pointer, for the
next launch. A generation that can't be loaded (say its rebuild for a new
app version fails its selftests) is reported as `:load_failed`, the
launch runs without it, and it goes back on probation. The pointer only
ever names a generation that was active once (`:probation`, `:proven`,
or `:candidate` / `:reverted` caught between the pointer and manifest
writes); anything else falls back to generation 0.

**Rebuilds** for a new Core (its binaries were built by another runtime,
`Rho.Dyn.Compiler.stale?/1`) don't hold up the launch: the
app never waits for Dyn code. `boot/2` returns at once with the
generation current but nothing of it in the registry (no Dyn tools,
screens or theme; `Rho.Dyn.Registry.rebuilding/1` says so), a
linked worker rebuilds and selftests it, and the Keeper then stores and
registers it and sends `%{type: :loaded}` (or reports `:load_failed`).
Proposals wait for it (`{:error, :rebuilding}`, `await_rebuild/1`), and
so does the launch's **stable** mark: a rebuild that takes the app down
counts as a failed launch, exactly as when it ran before the first frame,
so boot probation and safe mode still catch it.

**Core updates** (the host's own release path, say mob_deliver) share that launch: the
`:on_stable` MFA runs when a launch reaches stable (it ends a delivered
update's probation). `boot/2` records the update a launch runs (`core:`,
in `boot.json`); when mob_deliver has rolled back exactly that update
(`core_rolled_back:`), the failed launch doesn't count against the Dyn
generation: no boot revert and no step towards safe mode for it.

**Old generations** stay loaded while anything may run them: the current
one, its parent (instant revert), the pending candidate, and any
generation with a live watched process; the rest are unloaded with
`Rho.Dyn.Compiler.unload/1`, retried every `:gc_retry_ms`.

**Proposals** per launch are capped (`:max_proposals`, and none while
the atom table is over 80 % full): every proposal creates atoms (module
names `Dyn.G<n>.*`, parsed identifiers) and atoms are never
freed.

Options (defaults from `config :rho, :dyn`, `Rho.Dyn.Config`): `:name`
(also the registry table's name), `:dir`, `:approval`
(`Rho.Dyn.Approval.Biometric`), `:probation_ms` (60 000),
`:crash_limit` (3), `:crash_window_ms` (60 000), `:stable_delay_ms`
(10 000), `:gc_retry_ms` (30 000), `:max_proposals` (50), `:on_stable`
(`{module, function, args}` run in its own process when a launch
reaches stable, or `nil`), and the compile / selftest limits
(`:compile_timeout_ms`, `:compile_max_heap_mb`, `:selftest_timeout_ms`,
`:selftest_max_heap_mb`); a background rebuild's compile, and loading a
generation that isn't loaded (a revert to an older one), get
`:rebuild_timeout_ms` (300 000: it compiles the whole generation, which
can take minutes on a slow phone).

# `boot_report`

```elixir
@type boot_report() :: %{
  mode: :normal | :safe,
  generation: non_neg_integer(),
  status: Rho.Dyn.Generation.status(),
  reverted: nil | %{from: pos_integer(), to: non_neg_integer()},
  failed_launches: non_neg_integer(),
  load_ms: non_neg_integer(),
  rebuilding: boolean()
}
```

# `activate`

```elixir
@spec activate(GenServer.server(), pos_integer(), Rho.Dyn.Approval.token()) ::
  {:ok, Rho.Dyn.Generation.t()} | {:error, term()}
```

`Rho.Dyn.activate/3` on this Keeper: candidate `n` goes on probation, with an approval token.

# `await_rebuild`

```elixir
@spec await_rebuild(GenServer.server()) :: :ok
```

Returns once this launch's background rebuild (see the moduledoc) is
over, at once if none runs.

# `boot`

```elixir
@spec boot(GenServer.server(), keyword()) :: boot_report()
```

The launch's Dyn boot: launch markers, boot probation, safe mode, loading
the current generation. Options: `core:` the delivered Core update
(mob_deliver manifest id) this launch runs, `nil` for the build's own
code, recorded with the launch; `core_rolled_back:` the update
mob_deliver rolled back during this launch, if any. When it is the one
the last counted launch ran, that launch failed on the Core and doesn't
count against the Dyn generation (see the moduledoc).

# `candidate`

```elixir
@spec candidate(GenServer.server(), Rho.Dyn.Generation.t(), [module()]) ::
  :ok | {:error, :superseded}
```

Records a proposal that passed (see `Rho.Dyn.propose/2`); supersedes the previous one.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `discard`

```elixir
@spec discard(GenServer.server(), pos_integer()) :: :ok | {:error, :not_pending}
```

Drops the pending candidate `n` (unloads it; no approval needed).

# `first_render`

```elixir
@spec first_render(GenServer.server(), module() | nil) :: :ok
```

The `:after_first_render` hook (mob appends the screen): stable after `:stable_delay_ms`.

# `install_seed`

```elixir
@spec install_seed(GenServer.server(), Rho.Dyn.Generation.t(), [module()]) ::
  :ok | {:error, :safe_mode | :proposal_pending | :stale | term()}
```

Makes `gen` (built from the seed, `Rho.Dyn.seed/3`; its modules
`mods` loaded) current and proven at once, without approval: the seed
ships with the Core. Only on top of the current generation, in normal
mode and with no proposal pending; records it as the install's seed.

# `mark_stable`

```elixir
@spec mark_stable(GenServer.server()) :: :ok
```

This launch reached stable: launch markers reset; may end a generation's probation.

# `reserve_proposal`

```elixir
@spec reserve_proposal(GenServer.server()) ::
  :ok | {:error, :proposal_limit | :rebuilding}
```

Counts a proposal against this launch's cap; `{:error, :proposal_limit}`
once it's reached, `{:error, :rebuilding}` during a background rebuild.

# `revert_to`

```elixir
@spec revert_to(GenServer.server(), non_neg_integer(), Rho.Dyn.Approval.token()) ::
  {:ok, Rho.Dyn.Generation.t()} | {:error, term()}
```

`Rho.Dyn.revert_to/3` on this Keeper: generation `n` becomes current, with an approval token.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Started by `Rho.Supervisor` when Dyn is on (its `:dyn` option holds
`opts`). Options are merged over `config :rho, :dyn` and the defaults
(see the moduledoc); `:name` defaults to `Rho.Dyn.Keeper`.

# `status`

```elixir
@spec status(GenServer.server()) :: %{
  generation: non_neg_integer(),
  status: Rho.Dyn.Generation.status(),
  parent: non_neg_integer() | nil,
  mode: :booting | :normal | :safe,
  pending: pos_integer() | nil
}
```

The current generation, its status and parent, the mode and the pending candidate.

# `subscribe`

```elixir
@spec subscribe(GenServer.server(), pid()) :: :ok
```

Sends `pid` `{:rho_dyn, event}` for every crash, revert, activation, ... until it exits.

# `unwatch`

```elixir
@spec unwatch(GenServer.server(), pid()) :: :ok
```

Stops watching `pid`: its exit is no longer a crash (an orderly stop). A no-op without a Keeper.

# `watch`

```elixir
@spec watch(GenServer.server(), pid(), module()) :: :ok
```

Watches `pid`, which runs Dyn module `mod`: its abnormal exit counts as
a crash of `mod`'s generation. A no-op without a Keeper.

---

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