okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
159a58a
main
1"""Catch orphaned cleanup workers with deterministic gates inside unlink and VACUUM. 2 3The real CLI and daemon E2E tests cannot pause these operations. These tests 4run the production helper, replacing only the operation's scheduling with an 5acknowledged gate. EOF or death must stop that one process and release its lock. 6""" 7 8from contextlib import closing 9import json 10import os 11from pathlib import Path 12import sqlite3 13import subprocess 14import sys 15import tempfile 16import unittest 17 18sys .path .insert (0 ,str (Path (__file__ ).resolve ().parents [1 ])) 19from storage_helper_support import maintenance_command 20 21OWNER = """ 22import json, signal, subprocess, sys 23helper = subprocess.Popen(sys.argv[1:], 24stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True) 25helper.stdin.write(json.dumps({'paths': []}) + '\\n') 26helper.stdin.flush() 27assert json.loads(helper.stdout.readline()) == {'ready': True} 28print(helper.pid, flush=True) 29signal.pause() 30""" 31 32 33class MaintenanceLifetimeTests (unittest .TestCase ): 34def assert_released (self ,home ): 35with sqlite3 .connect (home / "daemon.lock" ,timeout = 0 )as db : 36db .execute ("BEGIN EXCLUSIVE" ) 37 38def assert_owned (self ,home ): 39with closing (sqlite3 .connect (home / "daemon.lock" ,timeout = 0 ))as db : 40with self .assertRaises (sqlite3 .OperationalError )as refused : 41db .execute ("BEGIN EXCLUSIVE" ) 42self .assertIn ( 43refused .exception .sqlite_errorcode , 44 (sqlite3 .SQLITE_BUSY ,sqlite3 .SQLITE_LOCKED ), 45 ) 46 47def close_process (self ,process ): 48if process .poll ()is None : 49process .kill () 50process .wait (timeout = 10 ) 51for pipe in (process .stdin ,process .stdout ,process .stderr ): 52if pipe and not pipe .closed : 53pipe .close () 54 55def test_death_or_eof_during_delete_and_vacuum_releases_ownership (self ): 56for operation in ("delete" ,"vacuum" ): 57for death in ("helper killed" ,"owner EOF" ): 58with ( 59self .subTest (operation = operation ,death = death ), 60tempfile .TemporaryDirectory ()as directory , 61 ): 62home = Path (directory ) 63candidate = home / "candidate" 64candidate .write_bytes (b"approved" ) 65with sqlite3 .connect (home / "albedo.sqlite" )as db : 66db .execute ("CREATE TABLE reclaim(data BLOB)" ) 67db .execute ("INSERT INTO reclaim VALUES(zeroblob(262144))" ) 68db .execute ("DELETE FROM reclaim" ) 69before = (home / "albedo.sqlite" ).read_bytes () 70worker = subprocess .Popen ( 71maintenance_command (home ,operation = operation ), 72stdin = subprocess .PIPE , 73stdout = subprocess .PIPE , 74stderr = subprocess .PIPE , 75text = True , 76 ) 77self .addCleanup (self .close_process ,worker ) 78assert worker .stdin is not None and worker .stdout is not None 79worker .stdin .write (json .dumps ({"paths" : [str (candidate )]})+ "\n" ) 80worker .stdin .flush () 81self .assertEqual ( 82json .loads (worker .stdout .readline ()), {"ready" :True } 83 ) 84worker .stdin .write ( 85json .dumps ( 86 { 87"apply" :True , 88"vacuum" :operation == "vacuum" , 89"before" :len (before ), 90"free_pages" :1 , 91 } 92 ) 93+ "\n" 94 ) 95worker .stdin .flush () 96self .assertEqual ( 97json .loads (worker .stdout .readline ()), {"working" :operation } 98 ) 99self .assert_owned (home ) 100if death == "helper killed" : 101worker .kill () 102else : 103worker .stdin .close () 104self .assertNotEqual (worker .wait (timeout = 10 ),0 ) 105self .assert_released (home ) 106if operation == "delete" : 107self .assertEqual (candidate .read_bytes (),b"approved" ) 108else : 109self .assertFalse (candidate .exists ()) 110self .assertEqual ((home / "albedo.sqlite" ).read_bytes (),before ) 111self .close_process (worker ) 112 113@ unittest . skipUnless (hasattr( os , "pidfd_open" ), "requires Linux pidfds" ) 114def test_owner_killed_before_authorization_releases_helper (self ): 115import select 116 117with tempfile .TemporaryDirectory ()as directory : 118home = Path (directory ) 119owner = subprocess .Popen ( 120 [sys .executable ,"-c" ,OWNER ,* maintenance_command (home )], 121stdout = subprocess .PIPE , 122stderr = subprocess .PIPE , 123text = True , 124 ) 125self .addCleanup (self .close_process ,owner ) 126assert owner .stdout is not None 127helper_pid = int (owner .stdout .readline ()) 128descriptor = os .pidfd_open (helper_pid ) 129try : 130owner .kill () 131owner .wait (timeout = 10 ) 132readable ,_ ,_ = select .select ([descriptor ], [], [],10 ) 133self .assertEqual (readable , [descriptor ],"helper survived its owner" ) 134self .assert_released (home ) 135finally : 136os .close (descriptor ) 137self .close_process (owner ) 138 139 140if __name__ == "__main__" : 141unittest .main ()