Rho.Dyn.Keeper (rho v0.1.0)

Copy Markdown View Source

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 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, 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).

Summary

Functions

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

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

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).

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

Returns a specification to start this module under a supervisor.

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

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

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.

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

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

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

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.

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

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

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

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

Types

boot_report()

@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()
}

Functions

activate(server, n, token)

@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(server)

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

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

boot(server, opts \\ [])

@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(server, gen, mods)

@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(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

discard(server, n)

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

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

first_render(server, screen)

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

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

install_seed(server, gen, mods)

@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(server)

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

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

reserve_proposal(server)

@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(server, n, token)

@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(opts \\ [])

@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(server)

@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(server, pid \\ self())

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

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

unwatch(server, pid)

@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(server, pid, mod)

@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.