Refresh public update status after completed transaction recovery
This commit is contained in:
+2
-1
@@ -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
|
||||
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.
|
||||
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.
|
||||
A running process can observe a mixture during apply. Do not launch new Hydra
|
||||
|
||||
@@ -463,7 +463,11 @@ class Updater:
|
||||
pending = self.pending()
|
||||
if len(pending) > 1:
|
||||
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):
|
||||
|
||||
@@ -429,3 +429,36 @@ CALL
|
||||
updater.recover()
|
||||
self.assertEqual(updater.status()['highest_sequence'], 1)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user