#!/usr/bin/env python3
"""Cancel only fake find workers; no real cache deletions or Perforce calls."""
import os
from pathlib import Path
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import unittest
HELPER = Path(__file__).resolve().parents[1] / 'Server/Unix/p4/common/bin/sdp_cache_cleanup.sh'
BASH = os.environ.get('SDP_TEST_BASH', shutil.which('bash'))
class CacheCancellation(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix='sdp-cancel-test-')
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
(self.root / 'bin').mkdir()
fake = self.root / 'bin/find'
fake.write_text('#!' + sys.executable + '\n' + '''import os, signal, sys, time
from pathlib import Path
root = Path(os.environ['TEST_ROOT'])
if os.environ.get('IGNORE_TERM'):
signal.signal(signal.SIGTERM, signal.SIG_IGN)
sys.stdout.write('...'); sys.stdout.flush()
(root / ('started.' + str(os.getpid()))).write_text(str(os.getpid()))
if not os.environ.get('FINISH_NOW'):
while not (root/'proceed').exists(): time.sleep(.01)
(root / ('continued.' + str(os.getpid()))).write_text('unexpected after cancellation')
sys.exit(int(os.environ.get('FIND_EXIT', '0')))
''')
fake.chmod(0o755)
self.script = self.root / 'run.sh'
self.script.write_text('''#!/bin/bash
source "$HELPER"
trap 'echo original-term >> "$TEST_ROOT/traps"; exit 143' TERM
trap 'echo original-int >> "$TEST_ROOT/traps"; exit 130' INT
trap 'echo original-hup >> "$TEST_ROOT/traps"; exit 129' HUP
trap 'echo "exit:$? removed:$SDP_CACHE_REMOVED" >> "$TEST_ROOT/traps"' EXIT
sdp_add_field() { echo "$1=$2" >> "$TEST_ROOT/fields"; }
sdp_set_error() { echo "$1" >> "$TEST_ROOT/error"; }
sdp_cache_cleanup_dirs 7 no "$TEST_ROOT/depot1" "$TEST_ROOT/depot2" "$TEST_ROOT/depot3"
result=$?
echo "result:$result removed:$SDP_CACHE_REMOVED" >> "$TEST_ROOT/traps"
if [[ ${CHECK_RESTORED:-0} == 1 ]]; then kill -TERM $$; fi
exit "$result"
''')
self.env = dict(os.environ, TEST_ROOT=str(self.root), HELPER=str(HELPER), P4TMP=str(self.root),
PATH=str(self.root/'bin')+':'+os.environ['PATH'], SDP_CACHE_CLEAN_WORKERS='2')
def test_signals_stop_actual_find_children_preserve_counts_and_caller_traps(self):
for sig, code, name, ignore in [(signal.SIGTERM,143,'term',False), (signal.SIGINT,130,'int',True), (signal.SIGHUP,129,'hup',False)]:
with self.subTest(signal=sig):
for path in self.root.glob('started.*'): path.unlink()
for name_file in ['traps','fields','error','proceed']:
(self.root/name_file).unlink(missing_ok=True)
env = dict(self.env)
if ignore: env['IGNORE_TERM'] = '1'
process = subprocess.Popen([BASH,str(self.script)],env=env,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
try:
deadline = time.monotonic()+5
while len(list(self.root.glob('started.*'))) < 2 and time.monotonic()<deadline:
time.sleep(.01)
pids = [int(path.read_text()) for path in self.root.glob('started.*')]
self.assertEqual(len(pids),2)
process.send_signal(sig)
out,err = process.communicate(timeout=5)
self.assertEqual(process.returncode,code,out+err)
self.assertIn('original-'+name,(self.root/'traps').read_text())
self.assertIn('removed:6',(self.root/'traps').read_text())
self.assertIn('files_removed=6',(self.root/'fields').read_text())
self.assertIn('interrupted',(self.root/'error').read_text())
self.assertEqual(list(self.root.glob('cache-clean.*')),[])
for pid in pids:
with self.assertRaises(ProcessLookupError): os.kill(pid,0)
(self.root/'proceed').touch()
self.assertEqual(list(self.root.glob('continued.*')),[])
finally:
if process.poll() is None: process.kill(); process.communicate()
def test_success_restores_original_signal_and_exit_handlers(self):
result = subprocess.run([BASH,str(self.script)],env=dict(self.env,FINISH_NOW='1',CHECK_RESTORED='1'),capture_output=True,text=True,timeout=5)
self.assertEqual(result.returncode,143,result.stderr)
text=(self.root/'traps').read_text()
self.assertIn('result:0 removed:9',text)
self.assertIn('original-term',text)
self.assertIn('exit:143',text)
self.assertEqual(list(self.root.glob('cache-clean.*')),[])
def test_worker_failure_counts_partial_work_and_reaps_remaining_workers(self):
result = subprocess.run([BASH,str(self.script)],env=dict(self.env,FINISH_NOW='1',FIND_EXIT='7'),capture_output=True,text=True,timeout=5)
self.assertEqual(result.returncode,1,result.stderr)
self.assertIn('result:1 removed:9',(self.root/'traps').read_text())
self.assertEqual(len(list(self.root.glob('continued.*'))),3)
self.assertEqual(list(self.root.glob('cache-clean.*')),[])
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 |