#include "simulation.hpp" #include #include namespace ota::rollback { namespace { bool compatible(const Data &data) { return data.schema == 1; } bool can_restore(const Storage &storage) { return storage.restore_authorized && storage.backup_valid && storage.backup && compatible(*storage.backup); } bool eligible(const Storage &storage) { return storage.window_open && storage.target_eligible; } void finish(Coordinator &coordinator, Storage &storage, bool healthy = true) { for (int steps = 0; !terminal(storage.phase); ++steps) { if (steps >= 16) throw std::runtime_error("Recovery did not terminate"); coordinator.advance(healthy); } } }// namespace Coordinator::Coordinator(Storage &storage) : storage_(storage) {} void Coordinator::start(const std::string &reason) { if (storage_.phase != Phase::Idle) throw std::logic_error("Recovery already started; resume saved state"); if (reason.empty() || storage_.failed_release.empty() || storage_.previous_release.empty() || storage_.failed_release == storage_.previous_release) throw std::logic_error("Recovery requires distinct release identities and a failure reason"); storage_.failure_reason = reason; storage_.blocked_releases.insert(storage_.failed_release); storage_.report_pending = true; storage_.phase = Phase::Assess; } void Coordinator::stop(const std::string &reason) { storage_.recovery_reason = reason; storage_.phase = Phase::SeparateRecovery; } void Coordinator::advance(bool previous_healthy) { switch (storage_.phase) { case Phase::Idle: throw std::logic_error("Save recovery intent before taking action"); case Phase::Assess: if (!eligible(storage_)) stop("previous release unavailable or rollback window closed"); else if (storage_.attempts_left <= 0) stop("recovery boot budget exhausted"); else if (compatible(storage_.data)) storage_.phase = Phase::SelectPrevious; else if (can_restore(storage_)) storage_.phase = Phase::RestoreData; else stop("incompatible data; no authorized valid restore"); break; case Phase::RestoreData: { if (!eligible(storage_) || storage_.attempts_left <= 0 || !can_restore(storage_)) { stop("restore prerequisites no longer hold"); break; } // Record IDs are unique. Count live records absent from the backup. const auto &saved = storage_.backup->records; storage_.records_lost = static_cast(std::count_if( storage_.data.records.begin(), storage_.data.records.end(), [&](int record) { return std::find(saved.begin(), saved.end(), record) == saved.end(); })); storage_.data = *storage_.backup; ++storage_.restores; // Data replacement and this progress transition are one assumed durable // operation in this model. Real restores need their own recovery protocol. storage_.phase = Phase::SelectPrevious; break; } case Phase::SelectPrevious: if (!eligible(storage_) || !compatible(storage_.data)) { stop("previous release or data no longer eligible"); break; } if (storage_.attempts_left <= 0) { stop("recovery boot budget exhausted"); break; } // Abstract boot control saves the consumed attempt before handoff. --storage_.attempts_left; ++storage_.boots_requested; storage_.running = Slot::A; storage_.phase = Phase::CheckPrevious; break; case Phase::CheckPrevious: if (!eligible(storage_) || !compatible(storage_.data) || storage_.running != Slot::A) stop("recovery prerequisites changed before checks"); else if (!previous_healthy) stop("previous release failed local checks"); else { storage_.recovery_reason = "previous release passed local checks"; storage_.phase = Phase::Recovered; } break; case Phase::Recovered: case Phase::SeparateRecovery: break; } } void Coordinator::after_reset() { // A reset before completed checks requires another accounted boot. Recreating // the Linux coordinator alone does not imply a device reset or new attempt. if (storage_.phase == Phase::CheckPrevious) storage_.phase = Phase::SelectPrevious; } bool terminal(Phase phase) { return phase == Phase::Recovered || phase == Phase::SeparateRecovery; } bool can_offer(const Storage &storage, const std::string &release) { // This models the local block and exclusion only, not full update admission. return (storage.phase == Phase::Idle || storage.phase == Phase::Recovered) && storage.blocked_releases.count(release) == 0; } const char *name(Phase phase) { switch (phase) { case Phase::Idle: return "idle"; case Phase::Assess: return "assess"; case Phase::RestoreData: return "restore-data"; case Phase::SelectPrevious: return "select-previous"; case Phase::CheckPrevious: return "check-previous"; case Phase::Recovered: return "recovered"; case Phase::SeparateRecovery: return "separate-recovery"; } throw std::logic_error("Unknown recovery phase"); } const std::vector &scenarios() { static const std::vector values{ "success", "incompatible-data", "backup-restore", "restart-during-recovery", "recovery-failure", "blocked-release", "attempts-exhausted", "window-closed", "target-unavailable"}; return values; } Storage run_scenario(const std::string &scenario) { if (std::find(scenarios().begin(), scenarios().end(), scenario) == scenarios().end()) throw std::invalid_argument("Unknown scenario: " + scenario); Storage storage; if (scenario == "incompatible-data" || scenario == "backup-restore") storage.data.schema = 2; if (scenario == "backup-restore") { storage.backup = Data{1, {1, 2}}; storage.backup_valid = true; storage.restore_authorized = true; } if (scenario == "window-closed") storage.window_open = false; if (scenario == "target-unavailable") storage.target_eligible = false; { Coordinator coordinator(storage); coordinator.start("B failed required local checks"); if (scenario == "restart-during-recovery" || scenario == "attempts-exhausted") { coordinator.advance();// Assess. coordinator.advance();// First A boot; checks not completed. } else { finish(coordinator, storage, scenario != "recovery-failure"); } } if (scenario == "restart-during-recovery" || scenario == "attempts-exhausted") { Coordinator restarted(storage); restarted.after_reset(); if (scenario == "attempts-exhausted") { restarted.advance();// Second A boot also fails to reach completed checks. restarted.after_reset(); } finish(restarted, storage); } if (scenario == "blocked-release") { Coordinator restarted(storage); restarted.after_reset(); if (can_offer(storage, storage.failed_release)) throw std::runtime_error("Failed release was offered again"); } return storage; } }// namespace ota::rollback