Skip to content

Commit 19dd8ea

Browse files
committed
feat(img): register mutable layers by vm_id; expose active_vm_ids/0
Adds a per-node Registry (Hyper.Node.Img.MutableRegistry) keyed by vm_id and names each Img.Mutable through it, giving a one-mutable-per-vm invariant and a cheap, non-blocking list of live mutable vm_ids for the Reaper to consult.
1 parent a3c7757 commit 19dd8ea

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

lib/hyper/node/img.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ defmodule Hyper.Node.Img do
2020
alias Hyper.Node.Img.ThinPool
2121

2222
@registry Hyper.Node.Img.Registry
23+
@mutable_registry Hyper.Node.Img.MutableRegistry
2324
@server_sup Hyper.Node.Img.Supervisor
2425
@mutable_sup Hyper.Node.Img.MutableSupervisor
2526

@@ -31,6 +32,7 @@ defmodule Hyper.Node.Img do
3132
def init(_opts) do
3233
children = [
3334
{Registry, keys: :unique, name: @registry},
35+
{Registry, keys: :unique, name: @mutable_registry},
3436
ThinPool,
3537
{DynamicSupervisor, strategy: :one_for_one, name: @server_sup},
3638
{DynamicSupervisor, strategy: :one_for_one, name: @mutable_sup}
@@ -42,6 +44,9 @@ defmodule Hyper.Node.Img do
4244
@doc false
4345
def registry, do: @registry
4446

47+
@doc false
48+
def mutable_registry, do: @mutable_registry
49+
4550
@doc "Activate `img_id` on this node: start (or reuse) its image server."
4651
@spec activate(Hyper.Img.id()) :: {:ok, pid()} | {:error, term()}
4752
def activate(img_id) do

lib/hyper/node/img/mutable.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ defmodule Hyper.Node.Img.Mutable do
4747
end
4848

4949
@spec start_link(Opts.t()) :: GenServer.on_start()
50-
def start_link(%Opts{} = opts), do: GenServer.start_link(__MODULE__, opts)
50+
def start_link(%Opts{vm_id: vm_id} = opts),
51+
do: GenServer.start_link(__MODULE__, opts, name: via(vm_id))
5152

5253
@spec blk_path(GenServer.server()) :: Path.t()
5354
def blk_path(server), do: GenServer.call(server, :blk_path)
@@ -146,6 +147,14 @@ defmodule Hyper.Node.Img.Mutable do
146147
@spec dm_name(Hyper.Vm.Id.t()) :: String.t()
147148
def dm_name(vm_id), do: "hyper-rw-#{sanitize(vm_id)}"
148149

150+
@doc "vm_ids of every live mutable layer on this node."
151+
@spec active_vm_ids() :: [Hyper.Vm.Id.t()]
152+
def active_vm_ids do
153+
Registry.select(Img.mutable_registry(), [{{:"$1", :_, :_}, [], [:"$1"]}])
154+
end
155+
156+
defp via(vm_id), do: {:via, Registry, {Img.mutable_registry(), vm_id}}
157+
149158
defp sanitize(id), do: String.replace(id, ~r/[^A-Za-z0-9._-]/, "_")
150159

151160
@spec drop(State.t(), pid()) :: State.t()
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
defmodule Hyper.Node.Img.MutableTest do
2+
use ExUnit.Case, async: false
3+
4+
alias Hyper.Node.Img
5+
alias Hyper.Node.Img.Mutable
6+
7+
setup do
8+
# The full app does not start in the test env, so stand up just the registry
9+
# the mutable layers register into. Reuse the real name so we exercise the
10+
# exact lookup `active_vm_ids/0` performs.
11+
name = Img.mutable_registry()
12+
13+
unless Process.whereis(name) do
14+
start_supervised!({Registry, keys: :unique, name: name})
15+
end
16+
17+
:ok
18+
end
19+
20+
defp register_live(vm_id) do
21+
test = self()
22+
23+
{:ok, pid} =
24+
Task.start_link(fn ->
25+
{:ok, _} = Registry.register(Img.mutable_registry(), vm_id, nil)
26+
send(test, {:registered, vm_id})
27+
Process.sleep(:infinity)
28+
end)
29+
30+
assert_receive {:registered, ^vm_id}
31+
pid
32+
end
33+
34+
test "active_vm_ids lists the vm_ids of every live mutable layer" do
35+
register_live("vm-a")
36+
register_live("vm-b")
37+
38+
assert Enum.sort(Mutable.active_vm_ids()) == ["vm-a", "vm-b"]
39+
end
40+
41+
test "active_vm_ids drops a vm_id once its mutable layer dies" do
42+
pid = register_live("vm-gone")
43+
ref = Process.monitor(pid)
44+
Process.unlink(pid)
45+
Process.exit(pid, :kill)
46+
assert_receive {:DOWN, ^ref, :process, ^pid, _}
47+
48+
# Registry removes the entry on the registered process's :DOWN. Poll briefly
49+
# so the test is robust to that async unregister without a fixed sleep.
50+
assert eventually(fn -> Mutable.active_vm_ids() == [] end)
51+
end
52+
53+
defp eventually(fun, attempts \\ 50) do
54+
cond do
55+
fun.() ->
56+
true
57+
58+
attempts == 0 ->
59+
false
60+
61+
true ->
62+
Process.sleep(2)
63+
eventually(fun, attempts - 1)
64+
end
65+
end
66+
end

0 commit comments

Comments
 (0)