Skip to content

Roadmap questions #71

Description

@spunkedy

@markovejnovic we talked on Reddit.

I figured having a diagram and thoughts here might be a little easier to communicate some of the ideas. I was thinking through ways of possibly separating out some of the layers to make them portable as an interface to be able to do things like:

  • create extensions like vfkit / apple containers for the runtime
  • scale out without having to need postgres as a hard dependency ( thinking through auto scaling groups etc )

If you are open to something like this, I can flush it out much more. In a PR

Proposal: split hyper_control / hyper_agent, behind a pluggable registry interface

Problem

:hyper is one Mix package today. Every node — whether it's fielding gRPC calls and
scheduling, or just running Firecracker VMs — runs identical code and links ecto_sql +
postgrex with live DB credentials, because agent-side code calls straight into
Hyper.Img.Db.* (Ecto/Postgres) rather than through an interface.

Concretely, three call sites do this today:

Call site What it does File
Hyper.Node.Img.Server.resolve_layers/1Db.Image.resolve_chain/1 VM boot: img_id → ordered layer/blob chain lib/hyper/node/img/server.ex
Hyper.Node.Img.with_image_lease/3Db.Lease.bump/3, Db.Lease.release/1 Per-VM lease heartbeat while a VM lives lib/hyper/node/img.ex
Hyper.Metering.Usage.record/1 Periodic CPU-usage row insert, called from FireVMM.Meter lib/hyper/metering/usage.ex

Everything else that's Postgres-hardwired (Hyper.Img.create/2, create_derived/3, all of
Hyper.Img.Db.Gc) is write/ingest or singleton-GC work that belongs to the control side
regardless of any split.

Notably, the equivalent problem on the bytes side is already solved:
Hyper.Node.Layer.Repo (lib/hyper/node/layer/repo.ex) only ever resolves
layer_id -> path, and its moduledoc already says it may be "backed by anything you like:
a plain filesystem, an NFS drive." There's no Ecto in it. The gap is purely on the
metadata side.

Proposed split

Three Mix packages:

  • hyper_control — scheduler, gRPC API, Postgres (Hyper.Img.Db.Repo + migrations),
    GC. 1–3 nodes, HA quorum.
  • hyper_agent — VM execution: Node.VMSupervisor/FireVMM, Budget, Reaper,
    Users, the setuid suidhelper. N nodes.
  • hyper_proto — shared contracts: Vm.Id/Vm.Spec, Unit.*, generated gRPC +
    Firecracker bindings. No Ecto, no scheduler, no Postgres.

Control and agents stay joined on the same Distributed Erlang mesh they use today —
Horde's CRDT gossip (Hyper.Cluster.Routing, Hyper.Cluster.Budget) keeps working
unmodified; Hyper.Cluster.Gc moves into hyper_control since it needs Postgres.

Module map

Module / area Today Proposed package Port / adapter
Hyper.Img.Db.Repo + migrations lib/hyper/img/db/repo.ex, priv/repo/ hyper_control
Hyper.Cluster.Scheduler lib/hyper/cluster/scheduler.ex hyper_control
Hyper.Img, Hyper.Img.Db.Gc lib/hyper/img.ex, lib/hyper/img/db/gc*.ex hyper_control
Hyper.Grpc.Server (external API) lib/hyper/grpc/*.ex hyper_control
Hyper.Node.Img / Img.Server — chain resolve, lease bump/release lib/hyper/node/img.ex, img/server.ex hyper_agent Hyper.Img.Registry — Postgres or Remote adapter
Hyper.Metering.Usage (called from FireVMM.Meter) lib/hyper/metering/usage.ex hyper_agent Hyper.Img.Registry.record_usage/1
Hyper.Node.Layer.Repo — blob path lookup lib/hyper/node/layer/repo.ex hyper_agent Layer.Repo — local/NFS or blob-proxy adapter
Hyper.Node.* — VMSupervisor, FireVMM, Users, Budget, Reaper lib/hyper/node/** hyper_agent
suidhelper, guest-agent native/** hyper_agent
Hyper.Cluster.Routing / Budget (CRDTs) lib/hyper/cluster/{routing,budget}.ex started by both
Vm.Id/Vm.Spec, Unit.*, generated bindings lib/hyper/vm/{id,spec}.ex, lib/unit/**, proto/** hyper_proto

The interface layer

Two ports, each with a direct (colocated) adapter and a remote (funneled through
hyper_control) adapter — chosen independently of each other:

defmodule Hyper.Img.Registry do
  @moduledoc "Port: agent-side access to image lineage and lease/usage state."

  @callback resolve_chain(Hyper.Img.id()) :: [layer()]
  @callback lease_bump(Hyper.Img.id(), Hyper.Vm.Id.t(), Unit.Time.t()) ::
              {:ok, term()} | {:error, term()}
  @callback lease_release(Hyper.Vm.Id.t()) :: :ok
  @callback record_usage(Hyper.Metering.Usage.attrs()) :: :ok | {:error, term()}
end
Port Adapter Backing Extra hop? Best for
Hyper.Img.Registry (new) Registry.Postgres This node's own Ecto pool — Db.Image, Db.Lease, Metering.Usage No Trusted subnet — today's behavior, unchanged
Hyper.Img.Registry (new) Registry.Remote Call to hyper_control over the existing libcluster mesh Yes — RTT on VM boot + lease heartbeat NAT'd / cross-region agents with no direct DB reachability
Hyper.Node.Layer.Repo (exists today) local / NFS mount File.stat/1 against Cfg.Dirs.layer_dir() No Already shipped — works as-is
Hyper.Node.Layer.Repo (exists today) blob-store proxy Streamed / range-fetched from hyper_control or an object store Yes — per-layer fetch on first mount Cross-region deploys with no shared POSIX filesystem

Both remote adapters can start as plain Distributed Erlang calls, since agents already join
the same cluster as hyper_control for Horde's CRDTs — no new wire protocol needed until an
agent has to leave that mesh entirely, at which point the call becomes a gRPC RPC on
hyper.proto instead.

Trade-offs

Pros

  • Execution nodes never ship Postgres drivers or DB credentials — smaller trust surface per VM host.
  • Scheduler, GC, and the gRPC API redeploy independently of hundreds of live-VM agents.
  • Bounded connection-pool pressure: control nodes' pools, not N-agents × pool_size.
  • Reaches topologies today's design can't: NAT'd, cross-region, or edge agents with no DB line-of-sight.
  • Agent-side tests run against a fake port — no Postgres needed in CI for that code path.

Cons

  • New latency + failure mode on VM boot: resolve_chain becomes a network call, not a local query.
  • Lease heartbeat now depends on control-plane reachability, adding a partition-vs-DB-down failure mode to classify.
  • Control plane must be its own HA target, or it's a new bottleneck — today "HA" is just "Postgres is HA."
  • Loses free transactionality: Ecto.Multi writes need a purpose-built transactional RPC when remote.
  • More to operate: an internal RPC surface with its own versioning, auth boundary, and retry policy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    debtThere is existing debt that needs cleaningdesign-proposedThe design for this ticket is in the idea stagedocumentationImprovements or additions to documentation

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions