"""Startup handoff ordering; filesystem is real, service manager is explicit double.""" import contextlib import os from pathlib import Path import tempfile import unittest SCRIPTS = Path(__file__).resolve().parents[1] class StopAfterReload(BaseException): pass class AdoptionScheduling(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) self.ns = {'__name__': 'schedule_fixture'} for name in ('prepare', 'runtime', 'adopt', 'worker'): exec(compile((SCRIPTS / f'public_update_{name}.py').read_text(), name, 'exec'), self.ns) units = self.root / 'units'; units.mkdir() transactions = self.root / 'transactions'; transactions.mkdir() self.tx = transactions / ('a' * 32); self.tx.mkdir() dropin = units / 'archipelago.service.d/70-public-application-update.conf' self.ns.update(TRANSACTIONS=transactions, UNIT_DIR=units, BOOT_WANTS=units/'basic.target.wants', DROPIN=dropin, OWNER_DROPINS=(dropin,), MAINTENANCE_UNITS=()) self.ns['trusted_open'] = lambda path, directory=False: os.open(path, os.O_RDONLY | os.O_NOFOLLOW | (os.O_DIRECTORY if directory else 0)) self.ns['current'] = lambda: (None, None) self.starting = True self.events = [] self.ns['command'] = self.command def command(self, *args, **kwargs): self.events.append(args[1:]) if args[1:] == ('daemon-reload',): self.assertFalse(self.starting, 'reload loses EXTEND_TIMEOUT_USEC on a starting file-backed unit') if args[1:] == ('start', 'archipelago-public-recovery-' + self.tx.name + '.service'): arm = (self.ns['UNIT_DIR'] / args[-1]).read_text() self.assertNotIn('Before=', arm, 'live arm must not wait behind the starting manager') self.assertIn(' arm ', arm) if args[1:3] == ('start', '--no-block'): recovery = 'archipelago-public-recovery-' + self.tx.name + '.service' self.assertIn(' recover-boot ', (self.ns['UNIT_DIR'] / recovery).read_text()) self.assertIn('Before=basic.target archipelago.service', (self.ns['UNIT_DIR'] / recovery).read_text()) self.assertTrue((self.ns['BOOT_WANTS'] / recovery).is_symlink()) self.assertEqual((self.ns['TRANSACTIONS'] / 'current').read_text(), self.tx.name) return '' def schedule(self): return self.ns['schedule_transaction'](self.tx, {'id': self.tx.name, 'adoption': True, 'runtime_requested': True, 'initial': 'signed-pair'}, '# immutable fixture worker\n') def test_admission_arms_durable_boot_recovery_without_reloading_starting_manager(self): state = self.schedule() self.assertEqual(state['phase'], 'queued') self.assertEqual(len(self.events), 2) def test_worker_stops_starting_manager_before_first_reload(self): state = self.schedule() self.ns['current'] = lambda: (self.tx, state) self.ns['owner_lock'] = contextlib.nullcontext for name in ('require_legacy_service', 'custody_frontend_parent'): self.ns[name] = lambda: None self.ns['manager_adoptable'] = lambda: self.starting self.ns['pair_identity'] = lambda *args: 'signed-pair' self.ns['check_preimages'] = lambda *args: None self.ns['require_space'] = lambda *args: None def stop(): self.events.append(('stop-manager',)) self.starting = False self.ns['stop_manager'] = stop def paused(*args): self.assertEqual(self.events[-2:], [('stop-manager',), ('daemon-reload',)]) raise StopAfterReload() self.ns['runtime_pause_units'] = paused with self.assertRaises(StopAfterReload): self.ns['run_worker'](self.tx.name) if __name__ == '__main__': unittest.main()