#!/usr/bin/env python3
"""Real-process Maintenance log leases; files stay inside disposable directories."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
import unittest
SDP = Path(__file__).resolve().parents[1]
CHILD = r'''
import json, os, signal, sys, time
from pathlib import Path
sys.path[:0] = [sys.argv[1], sys.argv[2]]
import sdputils
import sdp_structured_log as logger
root = Path(os.environ['LOGS'])
name, mode = sys.argv[3:5]
(root / (name + '.requested')).touch()
while not (root / 'start').exists(): time.sleep(.01)
if mode == 'handled-term':
def terminate(signum, frame):
print(name + '-terminated', flush=True)
raise SystemExit(128 + signum)
signal.signal(signal.SIGTERM, terminate)
with logger.run('concurrent.py', instance='fixture'), sdputils.maintenance_run('concurrent.py', 'fixture'):
print(name + '-start', flush=True)
fd = sys.stdout.logfile.fileno()
(root / (name + '.ready')).write_text(json.dumps({'inode': os.fstat(fd).st_ino, 'links': os.fstat(fd).st_nlink}))
while not (root / (name + '.go')).exists(): time.sleep(.01)
print(name + '-finish', flush=True)
(root / (name + '.end')).write_text(json.dumps({'inode': os.fstat(fd).st_ino, 'links': os.fstat(fd).st_nlink}))
if mode == 'failure': raise RuntimeError(name + '-specific-failure')
'''
class MaintenanceLogConcurrency(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix='sdp-log-lease-test-')
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.children = []
self.addCleanup(self.reap)
def reap(self):
for child in self.children:
if child.poll() is None:
child.kill()
child.communicate(timeout=5)
def wait_file(self, path):
deadline = time.monotonic() + 10
while not path.exists():
if time.monotonic() >= deadline:
self.fail('Timed out waiting for ' + str(path))
time.sleep(.01)
def start(self, name, mode='success', keep='1', simultaneous=False):
env = dict(os.environ, LOGS=str(self.root), SDP_RUN_LOG_MAX_BYTES='0', SDP_LOG_FORMAT='jsonl')
if keep is None:
env.pop('SDP_RUN_LOG_KEEP', None)
else:
env['SDP_RUN_LOG_KEEP'] = keep
child = subprocess.Popen([sys.executable, '-c', CHILD, str(SDP / 'Maintenance'),
str(SDP / 'Server/Unix/p4/common/bin'), name, mode],
env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
self.children.append(child)
self.wait_file(self.root / (name + '.requested'))
if not simultaneous:
(self.root / 'start').touch()
self.wait_file(self.root / (name + '.ready'))
return child
def finish(self, name, child, code=0):
(self.root / (name + '.go')).touch()
out, err = child.communicate(timeout=10)
self.assertEqual(child.returncode, code, out + err)
return out, err
def logs(self):
return {p.name: p.read_text() for p in self.root.iterdir()
if p.name == 'concurrent.log' or p.name.startswith('concurrent.log.run-')}
def test_overlapping_runs_keep_active_inode_and_all_diagnostics_at_one_and_default_retention(self):
for keep in ('1', None):
with self.subTest(keep=keep):
for path in self.root.iterdir():
path.unlink()
owner = self.start('owner', keep=keep)
inode = json.loads((self.root / 'owner.ready').read_text())['inode']
# Exceeds the default seven archives while the first run stays active.
for index in range(9):
name = 'peer' + str(index)
peer = self.start(name, keep=keep)
self.finish(name, peer)
self.assertEqual((self.root / 'concurrent.log').stat().st_ino, inode)
self.assertEqual(len(self.logs()), 1)
self.finish('owner', owner)
self.assertEqual(json.loads((self.root / 'owner.end').read_text())['links'], 1)
after = self.start('after', keep=keep)
self.finish('after', after)
archived, = (content for name, content in self.logs().items() if '.run-' in name)
for name in ['owner'] + ['peer' + str(i) for i in range(9)]:
self.assertIn(name + '-start', archived)
self.assertIn(name + '-finish', archived)
events = [json.loads(line) for line in (self.root / 'concurrent.jsonl').read_text().splitlines()]
self.assertEqual(len(events), 11)
self.assertTrue(all(row['status'] == 'success' for row in events))
self.assertEqual(len({row['run_id'] for row in events}), 11)
if keep == '1':
latest = self.start('latest', keep=keep)
self.finish('latest', latest)
self.assertEqual(len(self.logs()), 2)
self.assertNotIn('owner-start', '\n'.join(self.logs().values()))
def test_simultaneous_preparation_opens_one_shared_inode(self):
peers = [(str(index), self.start(str(index), simultaneous=True)) for index in range(8)]
(self.root / 'start').touch()
for name, _ in peers:
self.wait_file(self.root / (name + '.ready'))
inodes = {json.loads((self.root / (name + '.ready')).read_text())['inode'] for name, _ in peers}
self.assertEqual(len(inodes), 1)
for name, child in peers:
self.finish(name, child)
self.assertEqual(len(self.logs()), 1)
text = (self.root / 'concurrent.log').read_text()
for name, _ in peers:
self.assertIn(name + '-finish', text)
def test_failure_and_handled_or_abrupt_term_release_leases_and_preserve_evidence(self):
for mode, code in [('failure', 1), ('handled-term', 143), ('abrupt-term', -signal.SIGTERM)]:
with self.subTest(mode=mode):
for path in self.root.iterdir():
path.unlink()
first = self.start('first', mode)
peer = self.start('peer')
if mode == 'failure':
self.finish('first', first, code)
else:
first.send_signal(signal.SIGTERM)
first.communicate(timeout=10)
self.assertEqual(first.returncode, code)
self.finish('peer', peer)
after = self.start('after')
self.finish('after', after)
archived, = (content for name, content in self.logs().items() if '.run-' in name)
self.assertIn('first-start', archived)
self.assertIn('peer-finish', archived)
if mode == 'failure':
self.assertIn('first-specific-failure', archived)
if mode == 'handled-term':
self.assertIn('first-terminated', archived)
self.assertIn('failed (exit 143)', archived)
if __name__ == '__main__':
unittest.main()
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 34004 | Russell C. Jackson (Rusty) |
Improve SDP backup/recovery, Maintenance, logging and cloud operation. Add owned checkpoint locks, recovery-aware retention, preserved database switches, bounded cleanup and coordinated AWS/Azure snapshots. Fix service-user password initialization, edge staging, standby promotion state parsing, proxy cache setup, typed depot verification and historical-server configuration/error handling. Remove unused OTP programs; keep Windows removed, server.locks cleanup and the journal-count condition unchanged. Add no global shell strict modes. Update operational documentation and add reproducible mkdirs.sh/mkdirs.cfg container assurance. Validate Ubuntu 26.04 and UBI 8.10/9.8/10.2 across both Python runtimes: 44 stages; 428 offline tests per runtime/OS, zero offline skips; 19 deployed-archive comparisons per combination. Three six-node topology runs pass all 30 recovery/failure scenarios. P4D 2023.1 through 2026.1 on UBI 8/9 pass all 14 cells and 98 scenarios. Static, documentation and provenance checks pass. Containers do not certify every fleet kernel, patch build or integration. Tested source manifest SHA-256: e1a23e17b8ced8c1148d9a250589977c6f6e98606bf2a81dc3ff5d7c909e3695 Deployed comparison archive SHA-256: 1bd23217356ee3808e040d39aea5be9519bda7c8f2b4924656a70aff8ad7293a |