okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5bdc0ee
main
1"""A pending cell-completion wake must survive the idle swap of a stale kernel. 2 3The incident behind this suite: a session ran a long background cell on a 4kernel left stale by a daemon restart. At the turn's end a park nobody kept 5asked for a kernel, so the runtime started the swap; the arriving replacement 6was dropped with no notice, the old kernel (still holding the cell's pending 7completion wake) was stopped, and the model was never told anything. The wake 8must instead be admitted before the swap, ride it, and carry the upgrade 9notice; a swap that does end a running cell must journal it interrupted and 10say so. 11""" 12 13import json 14import os 15from pathlib import Path 16import time 17import unittest 18 19import harness 20from harness import Provider ,exclusive ,latest_user ,python ,text 21from kernel_support import MARKER ,ModelCells ,editable_daemon 22 23 24# exclusive: boots an editable python bundle and restarts the daemon 25@exclusive 26class SwapWakeTests (ModelCells ,unittest .TestCase ): 27bundle :Path 28 29def setUp (self ): 30self .cells = [] 31 32def script (request ): 33user = latest_user (request ) 34if "background python cells finished" in user : 35return text ("wake seen" ) 36if user .endswith ("hold" ): 37return text ("holding" ,delay = 4.0 ) 38if request ["input" ][- 1 ].get ("role" )== "user" and self .cells : 39return python (self .cells .pop (0 )) 40return text ("ok" ) 41 42def prepare (app ): 43launcher ,self .bundle = editable_daemon (app .root ) 44app .env ["ALBEDO_DAEMON" ]= str (launcher ) 45app .env ["ALBEDO_CELL_BACKGROUND_SECONDS" ]= "0.5" 46 47self .provider = Provider (script ) 48self .addCleanup (self .provider .close ) 49self .app = harness .Albedo (self .provider ,protocol = "responses" ,prepare = prepare ) 50self .app .__enter__ () 51self .addCleanup (self .app .__exit__ ,None ,None ,None ) 52self .session = self .app .session () 53 54def kernel (self ): 55with self .app .api (f"/sessions/ { self . session } ?tail=0" )as response : 56return json .load (response )["kernel" ] 57 58def gate (self ): 59gate = self .app .workspace / "cell-gate" 60os .mkfifo (gate ) 61self .addCleanup (harness .release_fifo ,gate ) 62return gate 63 64def held_cell (self ,* ,protocol_skew = False ): 65"""Cell code whose backgrounded job runs until the gate is released: 66the cell finishes only when the test decides, like the incident's 67build, and the kernel's loop stays free to answer the daemon.""" 68gate = self .gate () 69program = f"open( { str ( gate )!r } , 'rb').read(1)" 70prefix = ( 71"import albedo_link, os, sys\nalbedo_link.PROTOCOL = 2\n" 72if protocol_skew 73else "import os, sys\n" 74 ) 75code = ( 76f" { prefix } survivor = 41\n" 77f"j = run(sys.executable, '-c', { program !r } )\n" 78"await j\n('gate done',)" 79 ) 80return gate ,code 81 82def wait_for_wake (self ): 83deadline = time .monotonic ()+ 40 84while time .monotonic ()< deadline : 85for record in reversed (self .provider .requests ): 86if "background python cells finished" in latest_user (record ["request" ]): 87return latest_user (record ["request" ]) 88time .sleep (0.1 ) 89self .fail ("the cell completion wake never reached the model" ) 90 91def test_a_pending_cell_wake_survives_the_idle_kernel_swap (self ): 92gate ,code = self .held_cell () 93first ,_ = self .cell (code ) 94self .assertEqual (first ["status" ],"backgrounded" ,first ) 95 96with (self .bundle / "albedo_bundle.py" ).open ("a" )as source : 97source .write (MARKER ) 98self .app .restart () 99 100# One slow turn on the still-stale kernel (its live job holds the 101# swap). The gate is released mid-turn, so the background cell 102# finishes and its wake retries while the session is busy, exactly as 103# in the incident; the spurious park used to fire when this turn 104# ended, starting a swap that killed the wake with the old kernel. 105self .app .prompt (self .session ,"hold" ).close () 106time .sleep (0.8 ) 107harness .wait_until ( 108lambda :harness .release_fifo (gate ),10 ,"the held job never opened its gate" 109 ) 110self .app .idle (self .session ) 111 112wake = self .wait_for_wake () 113self .app .idle (self .session ) 114# The wake was admitted before the swap, so it rides the swapped 115# kernel's turn together with the upgrade notice. 116self .assertIn ("The python kernel was upgraded to the new python bundle" ,wake ) 117self .assertRegex (wake ,r"Restored: [^.]*\bsurvivor\b" ) 118 119swapped ,_ = self .cell ( 120"import albedo_bundle, os\n" 121"(survivor, getattr(albedo_bundle, 'MARKER', None), os.getpid())" 122 ) 123self .assertEqual (swapped .get ("status" ),"ok" ,swapped ) 124survivor ,marker ,_pid = swapped ["value" ].strip ("()" ).split (", " ) 125self .assertEqual ((survivor ,marker ), ("41" ,"'new bundle'" ),swapped ) 126self .assertFalse (self .kernel ()["stale" ]) 127 128def test_a_swap_past_a_running_cell_ends_it_as_interrupted (self ): 129# A kernel on another protocol is swapped regardless of live work, so 130# the swap ends the running cell: it must be journaled interrupted and 131# named, not left `started` with its effects unknown. (The explicit 132# upgrade refuses while a cell runs: "a cell is still running".) 133gate ,code = self .held_cell (protocol_skew = True ) 134first ,_ = self .cell (code ) 135self .assertEqual (first ["status" ],"backgrounded" ,first ) 136cell_id = first ["cell_id" ] 137 138self .app .restart () 139ended ,prompt = self .cell (f"print(await cells.info( { cell_id !r } ))" ) 140self .assertIn ("'status': 'interrupted'" ,ended ["output" ],ended ) 141self .assertIn ( 142"The python kernel was upgraded to the current kernel protocol" ,prompt 143 ) 144self .assertIn (f"Cells ended by the swap: { cell_id } " ,prompt ) 145 146 147if __name__ == "__main__" : 148unittest .main ()