Files
2026-10-25 08:31:08 -04:00

194 lines
8.3 KiB
C++

#include "simulation.hpp"
#include <algorithm>
#include <stdexcept>
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::size_t>(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<std::string> &scenarios() {
static const std::vector<std::string> 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