okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(runtime): decouple core storage from work9dd3a56

main
5.1 KiB160 linesraw
1//// A kernel or its owner going away must still end every process the kernel
2//// started, races beyond normal E2E teardown. Split from supervision_test so
3//// the two run side by side.
4
5import albedo/daemon/store
6import albedo/harness/extensions/python/kernel as python
7import gleam/erlang/process
8import gleam/int
9import gleeunit/should
10import harness/kernel_fixture
11
12/// Cells may not spawn processes themselves, but a library they call may:
13/// this defines one, `spawn`, compiled outside any cell.
14const library_spawn =
15  "library = {}\nexec(compile('import subprocess\\ndef spawn(*args, **kwargs):\\n    return subprocess.Popen(*args, **kwargs)', 'fixture_library.py', 'exec'), library)\nspawn = library['spawn']\n"
16
17pub fn owner_death_still_ends_reported_job_groups_test() -> Nil {
18  let assert Ok(store) = store.start(":memory:", "")
19  let assert Ok(kernel) = kernel_fixture.local(store, "/tmp")
20  let assert Ok(started) =
21    python.execute(
22      kernel,
23      "a",
24      "late = run('sleep', '30', timeout=300)\nimport asyncio\nawait asyncio.sleep(0.3)\nlate.group.pgid",
25      10_000,
26    )
27  let assert Ok(pgid) = int.parse(started.value)
28  // Kill the kernel from inside a cell: no cleanup callback can run, so the
29  // supervisor's own record of the job group is the only remaining owner.
30  let assert Error(python.Lost) =
31    python.execute(
32      kernel,
33      "b",
34      "import os, signal\nos.kill(os.getpid(), signal.SIGKILL)",
35      5000,
36    )
37  let assert Ok(probe) = kernel_fixture.local(store, "/tmp")
38  let assert Ok(checked) =
39    python.execute(
40      probe,
41      "c",
42      "import os\nstate = 'alive'\ntry:\n    os.killpg("
43        <> int.to_string(pgid)
44        <> ", 0)\nexcept ProcessLookupError:\n    state = 'gone'\nstate",
45      5000,
46    )
47  checked.value |> should.equal("'gone'")
48  let assert Ok(_) = python.stop(probe)
49  store.close(store)
50}
51
52pub fn kernel_death_ends_unregistered_children_in_its_own_group_test() -> Nil {
53  let assert Ok(store) = store.start(":memory:", "")
54  let assert Ok(kernel) = kernel_fixture.local(store, "/tmp")
55  let assert Ok(started) =
56    python.execute(kernel, "start", library_spawn <> "import subprocess
57child = spawn(['sleep', '30'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
58child.pid", 5000)
59  let assert Ok(pid) = int.parse(started.value)
60  let assert Error(python.Lost) =
61    python.execute(
62      kernel,
63      "die",
64      "import os, signal
65os.kill(os.getpid(), signal.SIGKILL)",
66      5000,
67    )
68  let assert Ok(probe) = kernel_fixture.local(store, "/tmp")
69  let assert Ok(checked) = python.execute(probe, "check", "import os
70state = 'alive'
71try:
72    os.kill(" <> int.to_string(pid) <> ", 0)
73except ProcessLookupError:
74    state = 'gone'
75state", 5000)
76  checked.value |> should.equal("'gone'")
77  let assert Ok(_) = python.stop(probe)
78  store.close(store)
79}
80
81pub fn shutdown_keeps_ownership_of_late_job_registrations_test() -> Nil {
82  let assert Ok(store) = store.start(":memory:", "")
83  let assert Ok(kernel) = kernel_fixture.local(store, "/tmp")
84  let assert Ok(path) =
85    python.execute(
86      kernel,
87      "start",
88      library_spawn <> "import __main__ as kernel, os, subprocess, tempfile
89from pathlib import Path
90fd, report = tempfile.mkstemp(prefix='albedo-owned-shutdown-')
91os.close(fd)
92def late():
93    child = spawn(['sleep', '30'], start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
94    Path(report).write_text(str(child.pid))
95    kernel.send({'type': 'job_start', 'id': 'late-fixture', 'pgid': child.pid})
96    os._exit(0)
97kernel.CLEANUP.append(late)
98report",
99      5000,
100    )
101  let assert Ok(_) = python.stop(kernel)
102  let assert Ok(probe) = kernel_fixture.local(store, "/tmp")
103  let assert Ok(checked) = python.execute(probe, "check", "import os
104from pathlib import Path
105report = Path(" <> path.value <> ")
106pid = int(report.read_text())
107report.unlink()
108state = 'alive'
109try:
110    os.kill(pid, 0)
111except ProcessLookupError:
112    state = 'gone'
113state", 5000)
114  checked.value |> should.equal("'gone'")
115  let assert Ok(_) = python.stop(probe)
116  store.close(store)
117}
118
119pub fn owner_loss_terminates_a_kernel_held_in_native_code_test() -> Nil {
120  let assert Ok(store) = store.start(":memory:", "")
121  let assert Ok(kernel) = kernel_fixture.local(store, "/tmp")
122  let assert Ok(started) =
123    python.execute(
124      kernel,
125      "pid",
126      "import os
127os.getpid()",
128      5000,
129    )
130  let assert Ok(pid) = int.parse(started.value)
131  let reply = process.new_subject()
132  let _ =
133    process.spawn_unlinked(fn() {
134      process.send(
135        reply,
136        python.execute(
137          kernel,
138          "native",
139          "import ctypes
140ctypes.PyDLL(None).sleep(30)",
141          60_000,
142        ),
143      )
144    })
145  process.sleep(100)
146  store.close(store)
147  process.receive(reply, 8000) |> should.equal(Ok(Error(python.Lost)))
148  let assert Ok(other) = store.start(":memory:", "")
149  let assert Ok(probe) = kernel_fixture.local(other, "/tmp")
150  let assert Ok(checked) = python.execute(probe, "check", "import os
151state = 'alive'
152try:
153    os.kill(" <> int.to_string(pid) <> ", 0)
154except ProcessLookupError:
155    state = 'gone'
156state", 5000)
157  checked.value |> should.equal("'gone'")
158  let assert Ok(_) = python.stop(probe)
159  store.close(other)
160}