Hyper.Node (Hyper v0.1.0)

Copy Markdown View Source

Per-machine supervisor. Exactly one Hyper.Node runs per BEAM node; it owns every microVM scheduled onto this machine.

Children:

  • VM routing lives in Hyper.Cluster.Routing (a cluster-wide CRDT started by Hyper.Cluster, above this supervisor), not here - this node only owns the local processes that run its microVMs.

  • Hyper.Node.ImageStore - a node-local content-addressed blob cache. Started before the VM supervisor so VMs can pull base images on boot.

  • Hyper.Node.VMSupervisor - a local DynamicSupervisor that starts one Hyper.Node.FireVMM per VM. Local on purpose: a firecracker VM is pinned to this machine's kernel/rootfs/cgroup/tap devices and cannot migrate, so we deliberately avoid Horde.DynamicSupervisor (which would try to restart VMs on a surviving node - cold-booting a ghost).

  • Hyper.Node.Users - manages an availability pool of users. Each VM gets its own user id and group id.

  • Hyper.Node.Budget.Supervisor - the node's resource budget: hard memory/disk accounting (Hyper.Node.Budget.Hard) plus the Sys.Mon real-time monitors backing the soft budget (Hyper.Node.Budget.Soft). Lives here, not at the application root, because both are per-node and only meaningful while this node hosts VMs.

  • Hyper.Node.Reaper - a periodic, liveness-aware GC for per-VM host resources (orphaned firecracker cgroups and hyper-rw-* dm volumes) stranded by an unclean death whose vm_id never reboots. Started last so the VM supervisor it consults for liveness is already up.

Summary

Functions

Returns a specification to start this module under a supervisor.

Run argv in a VM hosted on this node, identified by vm_id, via its relay/agent. The node-local half of Hyper.exec/3, which resolves the owning node and :erpc-calls this function there.

Fork the running VM parent_vm_id onto this node: budget-admitted like any placement (try_run/3), erroring with the node's admission verdict (:mem_exhausted, :cpu_saturated, ...) instead of falling back elsewhere. The guest is asked to sync first (best-effort); the snapshot is otherwise crash-consistent, like a power cut.

Publish the running VM vm_id's disk state as a new derived image and return what a remote node needs to boot a fork of it: the new img_id plus the parent's instance type, arch, and boot_args. The slow-fork half of Hyper.Vm.fork/1; the parent keeps running throughout.

Boot a fork of the running parent VM (described by its start Opts) on this node: same image lineage, rootfs snapshotted COW-style from the parent's mutable layer.

Boot an image-backed VM on this node: claim a uid, build the mutable rootfs layer, resolve the kernel, and start the VM supervisor. The uid is freed and the mutable layer torn down automatically when the VM supervisor dies.

Start a microVM on this node.

Tear down an image-backed VM started by start_image_vm/2.

Start a VM here and confirm its budget.

CPU time accrued by vm_id's meter on this node but not yet flushed to the usage table. The node-local half of Hyper.usage/1, which resolves the owning node and :erpc-calls this function there.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

exec(vm_id, argv, opts \\ [])

@spec exec(Hyper.Vm.Id.t(), [String.t()], keyword()) ::
  {:ok, %{stdout: binary(), stderr: binary(), exit_code: integer()}}
  | {:error, term()}

Run argv in a VM hosted on this node, identified by vm_id, via its relay/agent. The node-local half of Hyper.exec/3, which resolves the owning node and :erpc-calls this function there.

fork_vm_local(parent_vm_id)

@spec fork_vm_local(Hyper.Vm.Id.t()) :: {:ok, pid()} | {:error, term()}

Fork the running VM parent_vm_id onto this node: budget-admitted like any placement (try_run/3), erroring with the node's admission verdict (:mem_exhausted, :cpu_saturated, ...) instead of falling back elsewhere. The guest is asked to sync first (best-effort); the snapshot is otherwise crash-consistent, like a power cut.

publish_fork_image(vm_id)

@spec publish_fork_image(Hyper.Vm.Id.t()) ::
  {:ok,
   %{
     img_id: Hyper.Img.id(),
     type: Hyper.Vm.Instance.t(),
     arch: Hyper.Vm.Instance.arch(),
     boot_args: String.t() | nil
   }}
  | {:error, term()}

Publish the running VM vm_id's disk state as a new derived image and return what a remote node needs to boot a fork of it: the new img_id plus the parent's instance type, arch, and boot_args. The slow-fork half of Hyper.Vm.fork/1; the parent keeps running throughout.

start_forked_vm(child_vm_id, parent)

@spec start_forked_vm(Hyper.Vm.Id.t(), Hyper.Node.FireVMM.Opts.t()) ::
  {:ok, pid()} | {:error, term()}

Boot a fork of the running parent VM (described by its start Opts) on this node: same image lineage, rootfs snapshotted COW-style from the parent's mutable layer.

start_image_vm(vm_id, spec)

@spec start_image_vm(Hyper.Vm.Id.t(), Hyper.Vm.Spec.t()) ::
  {:ok, pid()} | {:error, term()}

Boot an image-backed VM on this node: claim a uid, build the mutable rootfs layer, resolve the kernel, and start the VM supervisor. The uid is freed and the mutable layer torn down automatically when the VM supervisor dies.

start_link(opts \\ [])

start_vm(opts)

Start a microVM on this node.

stop_image_vm(pid)

@spec stop_image_vm(pid()) :: :ok

Tear down an image-backed VM started by start_image_vm/2.

test_system()

@spec test_system() :: :ok | {:error, term()}

try_run(spec, start_fun, stop_fun)

@spec try_run(
  Hyper.Vm.Instance.Spec.t(),
  (-> {:ok, pid()} | {:error, term()}),
  (pid() -> :ok)
) ::
  {:ok, pid()} | {:error, term()}

Start a VM here and confirm its budget.

start_fun boots the VM and returns {:ok, vm_pid}; the reservation is held against vm_pid and released when it dies. If the reserve loses a race (the node filled up since the scheduler's snapshot) the just-started VM is torn down via stop_fun and {:error, reason} is returned.

unflushed_usage(vm_id)

@spec unflushed_usage(Hyper.Vm.Id.t()) :: Unit.Time.t()

CPU time accrued by vm_id's meter on this node but not yet flushed to the usage table. The node-local half of Hyper.usage/1, which resolves the owning node and :erpc-calls this function there.