okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
9dd3a56
main
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 { 18let assert Ok ( store ) = store . start ( ":memory:" , "" ) 19let assert Ok ( kernel ) = kernel_fixture . local ( store , "/tmp" ) 20let assert Ok ( started ) = 21python . execute ( 22kernel , 23"a" , 24"late = run('sleep', '30', timeout=300)\nimport asyncio\nawait asyncio.sleep(0.3)\nlate.group.pgid" , 2510_000 , 26) 27let 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. 30let assert Error ( python . Lost ) = 31python . execute ( 32kernel , 33"b" , 34"import os, signal\nos.kill(os.getpid(), signal.SIGKILL)" , 355000 , 36) 37let assert Ok ( probe ) = kernel_fixture . local ( store , "/tmp" ) 38let assert Ok ( checked ) = 39python . execute ( 40probe , 41"c" , 42"import os\nstate = 'alive'\ntry:\n os.killpg(" 43<> int . to_string ( pgid ) 44<> ", 0)\nexcept ProcessLookupError:\n state = 'gone'\nstate" , 455000 , 46) 47checked . value |> should . equal ( "'gone'" ) 48let assert Ok ( _ ) = python . stop ( probe ) 49store . close ( store ) 50} 51 52pub fn kernel_death_ends_unregistered_children_in_its_own_group_test () -> Nil { 53let assert Ok ( store ) = store . start ( ":memory:" , "" ) 54let assert Ok ( kernel ) = kernel_fixture . local ( store , "/tmp" ) 55let assert Ok ( started ) = 56python . execute ( kernel , "start" , library_spawn <> "import subprocess 57child = spawn(['sleep', '30'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 58child.pid" , 5000 ) 59let assert Ok ( pid ) = int . parse ( started . value ) 60let assert Error ( python . Lost ) = 61python . execute ( 62kernel , 63"die" , 64"import os, signal 65os.kill(os.getpid(), signal.SIGKILL)" , 665000 , 67) 68let assert Ok ( probe ) = kernel_fixture . local ( store , "/tmp" ) 69let assert Ok ( checked ) = python . execute ( probe , "check" , "import os 70state = 'alive' 71try: 72os.kill(" <> int . to_string ( pid ) <> ", 0) 73except ProcessLookupError: 74state = 'gone' 75state" , 5000 ) 76checked . value |> should . equal ( "'gone'" ) 77let assert Ok ( _ ) = python . stop ( probe ) 78store . close ( store ) 79} 80 81pub fn shutdown_keeps_ownership_of_late_job_registrations_test () -> Nil { 82let assert Ok ( store ) = store . start ( ":memory:" , "" ) 83let assert Ok ( kernel ) = kernel_fixture . local ( store , "/tmp" ) 84let assert Ok ( path ) = 85python . execute ( 86kernel , 87"start" , 88library_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(): 93child = spawn(['sleep', '30'], start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 94Path(report).write_text(str(child.pid)) 95kernel.send({'type': 'job_start', 'id': 'late-fixture', 'pgid': child.pid}) 96os._exit(0) 97kernel.CLEANUP.append(late) 98report" , 995000 , 100) 101let assert Ok ( _ ) = python . stop ( kernel ) 102let assert Ok ( probe ) = kernel_fixture . local ( store , "/tmp" ) 103let 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: 110os.kill(pid, 0) 111except ProcessLookupError: 112state = 'gone' 113state" , 5000 ) 114checked . value |> should . equal ( "'gone'" ) 115let assert Ok ( _ ) = python . stop ( probe ) 116store . close ( store ) 117} 118 119pub fn owner_loss_terminates_a_kernel_held_in_native_code_test () -> Nil { 120let assert Ok ( store ) = store . start ( ":memory:" , "" ) 121let assert Ok ( kernel ) = kernel_fixture . local ( store , "/tmp" ) 122let assert Ok ( started ) = 123python . execute ( 124kernel , 125"pid" , 126"import os 127os.getpid()" , 1285000 , 129) 130let assert Ok ( pid ) = int . parse ( started . value ) 131let reply = process . new_subject () 132let _ = 133process . spawn_unlinked ( fn () { 134process . send ( 135reply , 136python . execute ( 137kernel , 138"native" , 139"import ctypes 140ctypes.PyDLL(None).sleep(30)" , 14160_000 , 142), 143) 144}) 145process . sleep ( 100 ) 146store . close ( store ) 147process . receive ( reply , 8000 ) |> should . equal ( Ok ( Error ( python . Lost ))) 148let assert Ok ( other ) = store . start ( ":memory:" , "" ) 149let assert Ok ( probe ) = kernel_fixture . local ( other , "/tmp" ) 150let assert Ok ( checked ) = python . execute ( probe , "check" , "import os 151state = 'alive' 152try: 153os.kill(" <> int . to_string ( pid ) <> ", 0) 154except ProcessLookupError: 155state = 'gone' 156state" , 5000 ) 157checked . value |> should . equal ( "'gone'" ) 158let assert Ok ( _ ) = python . stop ( probe ) 159store . close ( other ) 160}