Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions test/hyper/metering/usage_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,50 @@ defmodule Hyper.Metering.UsageTest do
cpu_time: Time.zero()
})
end

describe "Hyper.usage/1 across a VM's lifetime" do
test "a stopped-but-metered VM reports its recorded lifetime total" do
vm_id = Hyper.Vm.Id.generate()
record!(vm_id, 0, 60, Time.ms(1_500))
record!(vm_id, 60, 120, Time.ms(500))

# Not registered anywhere: whereis/1 is nil, so the not-yet-flushed
# remainder is zero and usage/1 returns the flushed windows alone.
assert {:ok, total} = Hyper.usage(vm_id)
assert Time.as_ms(total) == 2_000
end

test "an entirely unknown VM is :not_found" do
# Never metered and not running anywhere: both halves of the lookup miss.
assert Hyper.usage(Hyper.Vm.Id.generate()) == {:error, :not_found}
end

test "usage/1 resolves a pid handle to its vm_id and delegates" do
vm_id = Hyper.Vm.Id.generate()
record!(vm_id, 0, 60, Time.ms(750))
:ok = Hyper.Cluster.Routing.register_self({vm_id, :supervisor})
await_local(vm_id)

# id/1 reverse-resolves self() -> vm_id; usage/1 then delegates to the
# vm_id clause. whereis/1 resolves this node, so unflushed_on/2 does erpc
# into Meter.unflushed/1; no meter is registered, so it swallows the
# GenServer :noproc exit and returns zero. The flushed total is returned.
assert {:ok, total} = Hyper.usage(self())
assert Time.as_ms(total) == 750
end
end

# Horde materialises a local registration into its replica asynchronously; poll
# until this node owns the routing entry before routing against it.
defp await_local(vm_id, tries \\ 200)
defp await_local(vm_id, 0), do: flunk("routing entry for #{vm_id} never materialised")

defp await_local(vm_id, tries) do
if Hyper.whereis(vm_id) == node() do
:ok
else
Process.sleep(5)
await_local(vm_id, tries - 1)
end
end
end
48 changes: 40 additions & 8 deletions test/hyper_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,27 @@ defmodule HyperTest do
# owning node's Hyper.Node.exec/3. The final hop into a live guest/relay is
# covered by the Hyper.Node.FireVMM.Agent suite and the live E2E.

test "exec/3 with an unregistered vm_id returns {:error, :not_found}" do
# A vm_id shaped like a real one but never registered in the routing table.
assert Hyper.exec("vaaaaaaaaaaaaaaaa", ["/bin/true"]) == {:error, :not_found}
end
# Every cluster entry point that resolves a VM to a node must short-circuit to
# {:error, :not_found} when the target is unresolvable, rather than misroute or
# crash. `:__self__` resolves to this test's pid at runtime: a live pid that was
# never registered as a {vm_id, :supervisor}, so id/1 reverse-resolves to nil.
for {name, {fun, args}} <- [
{"exec/3 by an unregistered vm_id", {:exec, ["vaaaaaaaaaaaaaaaa", ["/bin/true"]]}},
{"exec/3 by a non-supervisor pid", {:exec, [:__self__, ["/bin/true"]]}},
{"usage/1 by a non-supervisor pid", {:usage, [:__self__]}},
{"fork_vm/1 by an unregistered vm_id", {:fork_vm, ["vaaaaaaaaaaaaaaaa"]}}
] do
test "#{name} refuses with {:error, :not_found}" do
{fun, args} = unquote(Macro.escape({fun, args}))

args =
Enum.map(args, fn
:__self__ -> self()
other -> other
end)

test "exec/3 with a pid that is not a VM supervisor returns {:error, :not_found}" do
# self/0 is a live pid but was never registered under {vm_id, :supervisor},
# so id/1 resolves to nil and exec/3 refuses rather than misrouting.
assert Hyper.exec(self(), ["/bin/true"]) == {:error, :not_found}
assert apply(Hyper, fun, args) == {:error, :not_found}
end
end

test "exec/3 routes a resolvable VM into the owning node's Hyper.Node.exec/3, by vm_id and by pid" do
Expand All @@ -55,6 +67,14 @@ defmodule HyperTest do
end
end

test "id/1 answers nil when the VM's owning node is unreachable" do
# id/1 erpc's to node(pid). When that node can never be reached, the {:erpc,
# :noconnection} transport failure must degrade to nil (the VM died with its
# host, so "unknown" is truthful) rather than crash the caller.
unreachable = fabricate_pid_on(:"ghost@127.0.0.1")
assert Hyper.id(unreachable) == nil
end

# Horde materialises a registration into the local replica asynchronously, so
# poll whereis/1 until the entry is visible before routing against it.
defp await_route(vm_id, tries \\ 200)
Expand All @@ -70,4 +90,16 @@ defmodule HyperTest do
:ok
end
end

# A pid term whose node/1 is `node_name`, built without connecting to it. The
# external NEW_PID_EXT (tag 88) form: node as a SMALL_ATOM_UTF8_EXT (tag 119,
# 8-bit length), then id/serial/creation words. Lets us exercise the
# unreachable-owning-node path with no peer node, epmd, or distribution.
defp fabricate_pid_on(node_name) do
node_bin = :erlang.atom_to_binary(node_name, :utf8)

:erlang.binary_to_term(
<<131, 88, 119, byte_size(node_bin)::8, node_bin::binary, 1::32, 0::32, 1::32>>
)
end
end
Loading