fix(indeehub): bound transient restore readiness retries

This commit is contained in:
archipelago
2026-10-08 02:13:53 -04:00
parent c58d1180e7
commit 260e13273d
3 changed files with 55 additions and 5 deletions
@@ -495,3 +495,15 @@ refusal. Bounded private failure diagnostics now preserve the controller's reaso
without exposing stderr in the public RPC response. The previously recorded without exposing stderr in the public RPC response. The previously recorded
2,025 Rust tests predate these final helper changes; a matching executable and 2,025 Rust tests predate these final helper changes; a matching executable and
final combined receipt remain required. final combined receipt remain required.
The matching c58d1180 VM build passed with unchanged inputs. A second actual
RPC operation, `7d2ea2ae-7b42-4a2c-9095-f0ef2ab52048`, successfully drained all
seven recovered-image originals (including the relay's owned lineage admission)
and completed backup. Private diagnostics identified a **10-second pg_isready
probe TimeoutExpired**, not a schema mismatch. Pre-target recovery again reached
Restored with cleanup complete. The readiness loop now treats probe timeout as
not ready only within its existing 90-second overall budget, caps each attempt
and sleep by remaining time, and rejects success after the deadline. No mutation
or pg_restore timeout is retried. **58 pure controller tests pass**, including
ready-after-timeout, expired-deadline cleanup and late-success refusal. Another
matching executable/full transaction and final combined suite remain pending.
+9 -5
View File
@@ -704,12 +704,16 @@ class Controller:
# TCP readiness waits for the final server, avoiding interrupted restore. # TCP readiness waits for the final server, avoiding interrupted restore.
deadline=time.monotonic()+90 deadline=time.monotonic()+90
while True: while True:
remaining=deadline-time.monotonic()
require(remaining>0,'Backup restore database did not become ready')
try: try:
self.run(['podman','exec',identifier,'pg_isready','-h','127.0.0.1','-U','indeedhub','-d','indeedhub'],timeout=10) self.run(['podman','exec',identifier,'pg_isready','-h','127.0.0.1','-U','indeedhub','-d','indeedhub'],timeout=min(10,remaining))
break except (subprocess.CalledProcessError,subprocess.TimeoutExpired):
except subprocess.CalledProcessError: remaining=deadline-time.monotonic()
require(time.monotonic()<deadline,'Backup restore database did not become ready') require(remaining>0,'Backup restore database did not become ready')
time.sleep(0.5) time.sleep(min(0.5,remaining));continue
require(time.monotonic()<deadline,'Backup restore database did not become ready')
break
with (self.root/'backup'/'database.dump').open('rb') as source: with (self.root/'backup'/'database.dump').open('rb') as source:
self.run(['podman','exec','-i',identifier,'pg_restore','--exit-on-error','--no-owner','--no-acl', self.run(['podman','exec','-i',identifier,'pg_restore','--exit-on-error','--no-owner','--no-acl',
'-U','indeedhub','-d','indeedhub'],timeout=1800,input_file=source) '-U','indeedhub','-d','indeedhub'],timeout=1800,input_file=source)
@@ -575,4 +575,38 @@ console.log('process identity cases passed');'''
c.save_failure('acquire',RuntimeError('diagnostic'*1000));path=c.root/'last-failure.private.json';record=json.loads(path.read_text()) c.save_failure('acquire',RuntimeError('diagnostic'*1000));path=c.root/'last-failure.private.json';record=json.loads(path.read_text())
self.assertEqual(record['operation_id'],self.operation);self.assertEqual(record['action'],'acquire');self.assertEqual(record['error_type'],'RuntimeError');self.assertEqual(len(record['message']),4096) self.assertEqual(record['operation_id'],self.operation);self.assertEqual(record['action'],'acquire');self.assertEqual(record['error_type'],'RuntimeError');self.assertEqual(len(record['message']),4096)
self.assertEqual(path.stat().st_mode&0o777,0o600);self.assertEqual(c.path.read_bytes(),before) self.assertEqual(path.stat().st_mode&0o777,0o600);self.assertEqual(c.path.read_bytes(),before)
def database_readiness_fixture(self, failures):
c=self.completed_backup();c.record.pop('backup_restore_verified');attempts=[]
c.inspect=lambda identifier:{'Mounts':[]}
c.cleanup_restore_fixture=lambda:c.record.pop('restore_fixture',None)
c.database_commitments=lambda identifier:c.record['database_before']
def run(argv,**kwargs):
if argv[:2]==['podman','create']:return ('d'*64).encode()
if argv[:2]==['podman','start']:return b''
if argv[:2]==['podman','exec'] and argv[3]=='pg_isready':
attempts.append(argv)
if failures:raise failures.pop(0)
return b''
if argv[:3]==['podman','exec','-i']:return b''
raise AssertionError(argv)
c.run=run
return c,attempts
def test_database_readiness_probe_timeout_retries_within_existing_deadline(self):
from unittest.mock import patch
c,attempts=self.database_readiness_fixture([module.subprocess.TimeoutExpired(['pg_isready'],10),module.subprocess.CalledProcessError(1,['pg_isready'])])
with patch.object(module.time,'monotonic',side_effect=[0,1,2,3,4,5,6]),patch.object(module.time,'sleep'):
c.verify_database_backup()
self.assertEqual(len(attempts),3);self.assertEqual(c.record['backup_restore_verified'],c.backup_restore_terms())
def test_database_readiness_probe_timeout_never_extends_overall_deadline(self):
from unittest.mock import patch
c,attempts=self.database_readiness_fixture([module.subprocess.TimeoutExpired(['pg_isready'],10)])
with patch.object(module.time,'monotonic',side_effect=[0,1,91]),patch.object(module.time,'sleep'):
with self.assertRaisesRegex(RuntimeError,'did not become ready'):c.verify_database_backup()
self.assertEqual(len(attempts),1);self.assertNotIn('backup_restore_verified',c.record);self.assertNotIn('restore_fixture',c.record)
def test_database_readiness_rejects_success_after_deadline(self):
from unittest.mock import patch
c,attempts=self.database_readiness_fixture([])
with patch.object(module.time,'monotonic',side_effect=[0,89,91]),patch.object(module.time,'sleep'):
with self.assertRaisesRegex(RuntimeError,'did not become ready'):c.verify_database_backup()
self.assertEqual(len(attempts),1);self.assertNotIn('backup_restore_verified',c.record);self.assertNotIn('restore_fixture',c.record)
if __name__=='__main__':unittest.main() if __name__=='__main__':unittest.main()