[DPE-10650] S3 restore followup - #96
Conversation
aeb4969 to
8ce1fc1
Compare
…helper wait_until_loaded and wait_until_resynced carried the same tenacity loop; _wait_until(condition, timeout_s, failure) owns it now and each wait passes its predicate (is_loaded / is_replica_synced). Behaviour unchanged. PR #79 review (skourta r3794505615). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resume_failover is best-effort on every restore-failure path and Sentinel persists SENTINEL SET to its own conf, so a sentinel that was unreachable at that moment came back at the suppressed 24h down-after and stayed there -- no automatic failover from it -- until the next sentinel.conf re-render. Each unit now re-checks its own sentinel on update-status when no restore is in progress (SentinelManager.is_failover_suppressed / resume_local_failover, BackupEvents._reconcile_failover_suppression) and resets it. The TLS expiry test's exec mock now returns the (stdout, stderr) tuple the workload contract promises. PR #79 review (skourta r3621161292). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ainly Manual testing found the restore nearly silent in juju debug-log. Every rejected restore action now logs restore.rejected with the reason; the workflow logs the unit's role, each step start / restore.step_done, the leader's restore.advance A -> B and restore.completed; the manager logs verify, download start (bytes) / finish, stop, keep-copy, start, rollback and cleanup; a failure logs restore.step_failed (traceback), restore.failed unit/kind and the leader's restore.ended status. 'Teardown' was confusing (nothing is torn down): _restore_teardown -> _fail_restore (the failing unit records its marker), _clear_failed_restore -> _finish_failed_restore (leader ends the restore, un-wedges, sets the terminal status), _leader_restore_teardown_needed -> _leader_must_fail_restore. failed_restore_kind filters the empty kinds inside the comprehension instead of a trailing discard. PR #79 review (skourta r3794362707, r3794484866, r3794249766, review body). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A one-unit app takes a different path from the 3-unit scenarios: the only participant is both juju leader and valkey primary, there is no replica RESYNC, the barrier has one member, and the RESTORE -> RESYNC -> COMPLETED cascade rides purely on the leader's own app-databag relation-changed self-delivery. test_restore_single_unit redeploys 1 unit, does write -> backup -> mutate -> restore -> original back, then writes again to prove the lone primary is not write-frozen after the restart (rendered min-replicas-to-write 1 relaxed by the post-restore reconcile). deploy_and_relate_s3 grows a num_units parameter for it. Passed locally on lxd/VM (7m25s incl. the redeploy). PR #79 review (skourta r3794577942). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8ce1fc1 to
83fa75b
Compare
| if not (backup_id := event.params.get("backup-id", "")): | ||
| event.set_results({"error": "Must provide backup-id to restore."}) | ||
| event.fail("Must provide backup-id to restore.") | ||
| if not backup_id: |
There was a problem hiding this comment.
Consider adding this in the _restore_blocking_reason. This and the other checks
There was a problem hiding this comment.
Done. _restore_blocking_reason now takes backup_id and owns all the gates. 00e0b3d.
| if self.charm.unit.is_leader(): | ||
| self._advance_if_leader() | ||
|
|
||
| def _reconcile_failover_suppression(self, _: ops.UpdateStatusEvent) -> None: |
There was a problem hiding this comment.
This can be moved to the sentinel manager.
There was a problem hiding this comment.
Moved: SentinelManager.reconcile_failover_suppression() now does the check 62676ab.
…manager The check-and-reset of a sentinel left at the restore-suppressed down-after is sentinel business logic, so it belongs in SentinelManager, not in the backup event layer: reconcile_failover_suppression() reads is_failover_suppressed, logs restore.suppression_leak and calls resume_local_failover, tolerating an unreachable sentinel the same best-effort way resume_failover already does. The update-status handler keeps only the "when" -- unit_server.is_active and not cluster.is_restore_in_progress are restore-domain state -- so it is now a guard plus one manager call. PR #96 review (skourta r3819217492). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_restore_blocking_reason now takes the backup-id and owns every gate, so _on_restore_action is a single `if reason := self._restore_blocking_reason( backup_id)` plus the audit trail. The two backup-id gates live in a sibling _unusable_backup_reason helper: fusing them inline put the guard at mccabe 12 and ruff C901 rejects anything over 10, so it is split the way _unstable_primary_reason already is. Gate order is unchanged -- the S3 round-trip stays last, after the cheap state checks. A listing failure now fails the action with "Could not list backups: S3 request failed: <Code>" instead of splitting the safe error between results.error and a generic fail message; still the error code only, the detail stays in the unit log. PR #96 review (skourta r3813059183). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
reneradoi
left a comment
There was a problem hiding this comment.
No concerns in general, I only have a design remark for the added logging: All the log messages are bloating the event flow, in some cases we even add private methods only for avoiding duplicating log messages.
Example: BackupEvents._record_restore_step() consists of only two lines: the log line and a call to the manager function. It makes to code much harder to read and I would prefer if we could be more concise. Could you please try to add the log output to the manager methods where possible, and leave them out of the event handler? This can be supported by not logging the unit name as part of the log message, which is also duplicate information (already logged by Juju for each line).
If this could be applied all over the events, it would increase readability a lot.
Follow-up to #79 (S3 restore) addressing the last review round (skourta, 2026-08-17).
Only the four commits on top of #79 are new; base becomes
9/edgeonce #79 lands.resume_failoverisbest-effort on failure paths and Sentinel persists
SENTINEL SETto its own conf, so asentinel unreachable at that moment came back with the 24 h
down-afterand no automaticfailover until the next config re-render. Each unit now re-checks its own sentinel on
update-status(outside a restore) and resets it.restore.*log lines for every actionrejection, step, barrier advance, completion, download, rollback and failure
(
juju debug-log | grep restore.shows the whole run); the failure helpers are renamedto say what they do (
_fail_restore/_finish_failed_restore); comment/nit cleanups._wait_until(condition, timeout, failure)replaces the duplicated tenacity loops in
wait_until_loaded/wait_until_resynced.test_restore_single_unit(1-unit app:leader == primary, no resync, cascade driven only by the leader's own app-databag
relation-changed); also asserts the unit is writable after the restart.
No databag/schema changes; behaviour change is limited to the sentinel self-heal.