Skip to content

Commit 176e67e

Browse files
committed
test(e2e): slow-fork fallback publishes then surfaces cluster verdict
Also pins the dead-handle contract: fork/fast_fork of an unknown VM is :not_found and is never misrouted into the capacity fallback.
1 parent 580424a commit 176e67e

1 file changed

Lines changed: 61 additions & 1 deletion

File tree

test/e2e/fork_test.exs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ defmodule Hyper.E2e.ForkTest do
1111
- a VM created from the derived image composes that chain (dm-snapshot over
1212
the published COW store) and boots with the parent's pre-publish state —
1313
including the parent's writes, excluding the child's;
14-
- all forked writable volumes are reclaimed on stop (no dm leak).
14+
- all forked writable volumes are reclaimed on stop (no dm leak);
15+
- `Hyper.Vm.fork/1`'s slow-fork fallback: when a real node-admission refusal
16+
stops `fast_fork/1`, `fork/1` publishes exactly one derived image before
17+
re-placing cluster-wide, and the cluster-wide capacity verdict surfaces
18+
unmasked when no other node can take the child;
19+
- `fast_fork/1` and `fork/1` on a dead VM handle are `{:error, :not_found}`,
20+
never misclassified as a capacity refusal.
1521
1622
Runs only under `--only integration` / `--include integration` on a host
1723
provisioned per docs/cookbook/install.md (CI: the `integration` job).
@@ -27,6 +33,17 @@ defmodule Hyper.E2e.ForkTest do
2733
# limits, which shared GHA egress IPs routinely exhaust.
2834
@image System.get_env("HYPER_E2E_IMAGE", "public.ecr.aws/docker/library/alpine:3.19")
2935

36+
alias Hyper.Img.Db.{Image, Repo}
37+
38+
# Boot :micro fillers until cluster placement refuses, so the next
39+
# fast_fork must hit a node-admission refusal.
40+
defp saturate(img_id) do
41+
case Hyper.create_vm(%Hyper.Vm.Spec{img_id: img_id, type: :micro}) do
42+
{:ok, vm} -> [vm | saturate(img_id)]
43+
{:error, reason} when reason in [:no_capacity, :exhausted] -> []
44+
end
45+
end
46+
3047
test "fast_fork isolates writes; published delta composes and boots" do
3148
assert {:ok, img_id} = Hyper.Img.OciLoader.load(@image)
3249

@@ -108,4 +125,47 @@ defmodule Hyper.E2e.ForkTest do
108125

109126
assert cousin_sees_childfile != 0, "child's writes leaked into the published delta"
110127
end
128+
129+
test "fork/1 falls back to publish-and-reschedule when the node refuses admission" do
130+
assert {:ok, img_id} = Hyper.Img.OciLoader.load(@image)
131+
132+
assert {:ok, parent} = Hyper.create_vm(%Hyper.Vm.Spec{img_id: img_id, type: :micro})
133+
on_exit(fn -> Hyper.Node.stop_image_vm(parent) end)
134+
135+
# Unique disk content so the published delta (and thus the derived image
136+
# id) cannot collide with any image published by an earlier test.
137+
assert {:ok, %{exit_code: 0}} =
138+
await_exec(
139+
parent,
140+
["/bin/sh", "-c", "head -c 32 /dev/urandom > /marker && sync"],
141+
:timer.minutes(3)
142+
)
143+
144+
fillers = saturate(img_id)
145+
on_exit(fn -> Enum.each(fillers, &Hyper.Node.stop_image_vm/1) end)
146+
147+
images_before = Repo.aggregate(Image, :count)
148+
149+
# Single-node cluster: fast_fork must refuse with a capacity verdict,
150+
# fork/1 must then publish the parent's disk state (the observable slow-
151+
# fork side effect) and re-place cluster-wide — where the same saturated
152+
# node is the only candidate, so the cluster verdict surfaces unmasked.
153+
# fork/1's doc names both cluster-wide verdicts; either proves the point.
154+
assert {:error, reason} = Hyper.Vm.fork(parent)
155+
156+
assert reason in [:no_capacity, :exhausted],
157+
"expected the cluster-wide capacity verdict, got #{inspect(reason)}"
158+
159+
assert Repo.aggregate(Image, :count) == images_before + 1,
160+
"slow fork must publish exactly one derived image before re-placing"
161+
end
162+
163+
test "forking a dead VM handle is :not_found, with no fallback attempted" do
164+
dead = spawn(fn -> :ok end)
165+
ref = Process.monitor(dead)
166+
assert_receive {:DOWN, ^ref, :process, ^dead, _}
167+
168+
assert Hyper.Vm.fast_fork(dead) == {:error, :not_found}
169+
assert Hyper.Vm.fork(dead) == {:error, :not_found}
170+
end
111171
end

0 commit comments

Comments
 (0)