Refresh public update status after completed transaction recovery

This commit is contained in:
Codex
2026-10-13 21:46:45 +03:00
parent 653388f0d7
commit 8512d1d10b
3 changed files with 40 additions and 2 deletions
+2 -1
View File
@@ -133,7 +133,8 @@ Writes use adjacent temporary files, file fsync, atomic replacement and director
fsync. Recovery is independent of the runtime package. The public read-only status fsync. Recovery is independent of the runtime package. The public read-only status
summary is `/usr/share/hydra/update-status.json`; private journals are authoritative, summary is `/usr/share/hydra/update-status.json`; private journals are authoritative,
and an abrupt loss of power may leave the summary briefly older than those journals. and an abrupt loss of power may leave the summary briefly older than those journals.
Run recovery as administrator when an update was interrupted. Run recovery as administrator when an update was interrupted; it refreshes the
public summary from authoritative state even when no transaction remains pending.
This is a per-file recoverable transaction, not a simultaneous filesystem snapshot. This is a per-file recoverable transaction, not a simultaneous filesystem snapshot.
A running process can observe a mixture during apply. Do not launch new Hydra A running process can observe a mixture during apply. Do not launch new Hydra
+5 -1
View File
@@ -463,7 +463,11 @@ class Updater:
pending = self.pending() pending = self.pending()
if len(pending) > 1: if len(pending) > 1:
raise UpdateError('Multiple pending journals require manual inspection') raise UpdateError('Multiple pending journals require manual inspection')
return [self.restore(transaction, False) for transaction in pending] results = [self.restore(transaction, False) for transaction in pending]
# A crash can leave a terminal journal durable but its public summary stale.
# Reconcile even when there is no transaction left to restore.
self.publish()
return results
def checked_url(url): def checked_url(url):
+33
View File
@@ -429,3 +429,36 @@ CALL
updater.recover() updater.recover()
self.assertEqual(updater.status()['highest_sequence'], 1) self.assertEqual(updater.status()['highest_sequence'], 1)
self.assertEqual(self.target.read_bytes(), b'old runtime\n') self.assertEqual(self.target.read_bytes(), b'old runtime\n')
def test_fresh_recovery_reconciles_public_status_after_terminal_journal_before_publish(self):
self.package()
updater = self.updater()
from unittest.mock import patch
original_publish = updater.publish
def interrupted_publish():
if not updater.pending():
raise OSError('power lost after terminal journal before public status')
return original_publish()
for operation in ('apply', 'rollback'):
with self.subTest(operation=operation):
with patch.object(updater, 'publish', side_effect=interrupted_publish):
with self.assertRaisesRegex(OSError, 'terminal journal'):
if operation == 'apply':
updater.apply(self.artifacts)
else:
updater.rollback(updater.state()['current']['transaction'])
authoritative = updater.status()
self.assertEqual(authoritative['pending'], [])
stale = json.loads(updater.public_status.read_text())
self.assertNotEqual(stale, authoritative)
result = subprocess.run([sys.executable, '-I', str(ENGINE.parent.parent / 'bin/hydra-update-recover'),
'--root', str(self.root), '--public-key', str(self.pub)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout), [])
self.assertEqual(json.loads(updater.public_status.read_text()), authoritative)
self.assertEqual(self.target.read_bytes(), b'new runtime\n' if operation == 'apply' else b'old runtime\n')
updater.base.chmod(0)
try:
self.assertEqual(updater.status(), authoritative)
finally:
updater.base.chmod(0o700)