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
|
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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user