use super::{
RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind,
RestoreApplyOperationKindCounts, RestoreApplyOperationState,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(in crate::restore) struct RestoreApplyJournalReport {
pub report_version: u16,
pub backup_id: String,
pub outcome: RestoreApplyReportOutcome,
pub attention_required: bool,
pub ready: bool,
pub complete: bool,
pub blocked_reasons: Vec<String>,
pub operation_count: usize,
pub operation_counts: RestoreApplyOperationKindCounts,
pub progress: RestoreApplyProgressSummary,
pub pending_summary: RestoreApplyPendingSummary,
pub pending_operations: usize,
pub ready_operations: usize,
pub blocked_operations: usize,
pub completed_operations: usize,
pub failed_operations: usize,
pub next_transition: Option<RestoreApplyReportOperation>,
pub pending: Vec<RestoreApplyReportOperation>,
pub failed: Vec<RestoreApplyReportOperation>,
pub blocked: Vec<RestoreApplyReportOperation>,
}
impl RestoreApplyJournalReport {
#[must_use]
pub(in crate::restore) fn from_journal(journal: &RestoreApplyJournal) -> Self {
let complete = journal.is_complete();
let outcome = RestoreApplyReportOutcome::from_journal(journal, complete);
let pending = report_operations_with_state(journal, RestoreApplyOperationState::Pending);
let failed = report_operations_with_state(journal, RestoreApplyOperationState::Failed);
let blocked = report_operations_with_state(journal, RestoreApplyOperationState::Blocked);
Self {
report_version: 1,
backup_id: journal.backup_id.clone(),
outcome: outcome.clone(),
attention_required: outcome.attention_required(),
ready: journal.ready,
complete,
blocked_reasons: journal.blocked_reasons.clone(),
operation_count: journal.operation_count,
operation_counts: journal.operation_kind_counts(),
progress: RestoreApplyProgressSummary::from_journal(journal),
pending_summary: RestoreApplyPendingSummary::from_journal(journal),
pending_operations: journal.pending_operations,
ready_operations: journal.ready_operations,
blocked_operations: journal.blocked_operations,
completed_operations: journal.completed_operations,
failed_operations: journal.failed_operations,
next_transition: journal
.next_transition_operation()
.map(RestoreApplyReportOperation::from_journal_operation),
pending,
failed,
blocked,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RestoreApplyPendingSummary {
pub pending_operations: usize,
pub pending_operation_available: bool,
pub pending_sequence: Option<usize>,
pub pending_operation: Option<RestoreApplyOperationKind>,
pub pending_updated_at: Option<String>,
pub pending_updated_at_known: bool,
}
impl RestoreApplyPendingSummary {
#[must_use]
pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
let pending = journal
.operations
.iter()
.filter(|operation| operation.state == RestoreApplyOperationState::Pending)
.min_by_key(|operation| operation.sequence);
let pending_updated_at = pending.and_then(|operation| operation.state_updated_at.clone());
let pending_updated_at_known = pending_updated_at
.as_deref()
.is_some_and(known_state_update_marker);
Self {
pending_operations: journal.pending_operations,
pending_operation_available: pending.is_some(),
pending_sequence: pending.map(|operation| operation.sequence),
pending_operation: pending.map(|operation| operation.operation.clone()),
pending_updated_at,
pending_updated_at_known,
}
}
}
fn known_state_update_marker(value: &str) -> bool {
!value.trim().is_empty() && value != "unknown"
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RestoreApplyProgressSummary {
pub operation_count: usize,
pub completed_operations: usize,
pub remaining_operations: usize,
pub transitionable_operations: usize,
pub attention_operations: usize,
pub completion_basis_points: usize,
}
impl RestoreApplyProgressSummary {
#[must_use]
pub const fn from_journal(journal: &RestoreApplyJournal) -> Self {
let remaining_operations = journal
.operation_count
.saturating_sub(journal.completed_operations);
let transitionable_operations = journal.ready_operations + journal.pending_operations;
let attention_operations =
journal.pending_operations + journal.blocked_operations + journal.failed_operations;
let completion_basis_points =
completion_basis_points(journal.completed_operations, journal.operation_count);
Self {
operation_count: journal.operation_count,
completed_operations: journal.completed_operations,
remaining_operations,
transitionable_operations,
attention_operations,
completion_basis_points,
}
}
}
const fn completion_basis_points(completed_operations: usize, operation_count: usize) -> usize {
if operation_count == 0 {
return 0;
}
completed_operations.saturating_mul(10_000) / operation_count
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RestoreApplyReportOutcome {
Empty,
Complete,
Failed,
Blocked,
Pending,
InProgress,
}
impl RestoreApplyReportOutcome {
const fn from_journal(journal: &RestoreApplyJournal, complete: bool) -> Self {
if journal.operation_count == 0 {
return Self::Empty;
}
if complete {
return Self::Complete;
}
if journal.failed_operations > 0 {
return Self::Failed;
}
if !journal.ready || journal.blocked_operations > 0 {
return Self::Blocked;
}
if journal.pending_operations > 0 {
return Self::Pending;
}
Self::InProgress
}
const fn attention_required(&self) -> bool {
matches!(self, Self::Failed | Self::Blocked | Self::Pending)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RestoreApplyReportOperation {
pub sequence: usize,
pub operation: RestoreApplyOperationKind,
pub state: RestoreApplyOperationState,
pub member_order: usize,
pub role: String,
pub source_canister: String,
pub target_canister: String,
pub state_updated_at: Option<String>,
pub reasons: Vec<String>,
}
impl RestoreApplyReportOperation {
fn from_journal_operation(operation: &RestoreApplyJournalOperation) -> Self {
Self {
sequence: operation.sequence,
operation: operation.operation.clone(),
state: operation.state.clone(),
member_order: operation.member_order,
role: operation.role.clone(),
source_canister: operation.source_canister.clone(),
target_canister: operation.target_canister.clone(),
state_updated_at: operation.state_updated_at.clone(),
reasons: operation.blocking_reasons.clone(),
}
}
}
fn report_operations_with_state(
journal: &RestoreApplyJournal,
state: RestoreApplyOperationState,
) -> Vec<RestoreApplyReportOperation> {
journal
.operations
.iter()
.filter(|operation| operation.state == state)
.map(RestoreApplyReportOperation::from_journal_operation)
.collect()
}