194 lines
8.3 KiB
C++
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
|