Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-repair/src/view.rs

//! **What the verbs read**, transport-neutral: the twin's `Mirror.LogHead`
//! ([`TwinHead`]), the main's `Bootstrap.Status` ([`MainStatus`]) and the box's
//! answer to `Bootstrap.Restore` ([`Restored`]). Each host turns its own
//! generated message into these with [`wire!`](crate::wire) — one expansion per
//! generated `pb`, never a hand-written copy.

use serde::{Deserialize, Serialize};

/// The twin's `Mirror.LogHead`, the fields a repair decides on.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TwinHead {
    pub first_seq: u64,
    pub head_seq: u64,
    pub bytes: u64,
    /// `false`: the box answering is a primary, not a twin.
    pub twin: bool,
    pub applied_seq: u64,
    pub primary_head_seq: u64,
    pub caught_up: bool,
    /// `following` / `unreachable` / `stalled` / `diverged` / `snapshot-needed` / `idle`.
    pub state: String,
    pub why: String,
    pub last_answer_unix_ms: i64,
    pub answer_ago_ms: u64,
    pub log_id: String,
    pub last_level_unix_ms: i64,
    /// The twin's kept base: its log starts at `snapshot_at + 1`. 0 = no base.
    pub snapshot_at: u64,
    pub snapshot_bytes: u64,
}

/// The main's `Bootstrap.Status` (credential free), the fields a repair decides on.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MainStatus {
    pub window_open: bool,
    pub bootstrapped: bool,
    pub bootstrapped_principal: String,
    pub principals: u64,
    pub keys: u64,
    pub detail: String,
    pub serving_git: bool,
    pub readiness: String,
    /// `primary` / `twin` / `standby` / … as the box says it.
    pub role: String,
    /// A recharged main that awaits its twin names it here.
    pub recharge_from: String,
    pub restoring: bool,
    pub log_head_seq: u64,
    pub restore_partial_seq: u64,
    pub restore_partial_bytes: u64,
    pub restore_snapshot_resumable: bool,
    pub restore_snapshot_id: String,
    pub restore_snapshot_items: u64,
    pub restore_snapshot_bytes: u64,
    pub restore_snapshot_whole: bool,
    /// The box's /data as it measures it; 0 = not said.
    pub data_total_bytes: u64,
}

impl MainStatus {
    /// **Fresh**: no identity, the bootstrap window open — what a re-laid main is.
    pub fn is_fresh(&self) -> bool {
        self.window_open && self.principals == 0 && self.keys == 0
    }
}

/// `Bootstrap.Restore`'s answer.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Restored {
    pub head_seq: u64,
    pub applied: u64,
    pub lost: u64,
    pub restart_required: bool,
    pub detail: String,
}

/// A generated `LogHeadResponse`, read as a [`TwinHead`].
pub trait AsTwinHead {
    fn twin_head(&self) -> TwinHead;
}

/// A generated `BootstrapStatusResponse`, read as a [`MainStatus`].
pub trait AsMainStatus {
    fn main_status(&self) -> MainStatus;
}

/// A generated `RestoreResponse`, read as [`Restored`].
pub trait AsRestored {
    fn restored(&self) -> Restored;
}

/// **One piece of the twin's snapshot or log** (`LogChunk`), as the pump needs it.
pub trait Chunk: Send + Sync + 'static {
    fn seq(&self) -> u64;
    fn kind(&self) -> u32;
    /// The last piece of its entry (or snapshot item).
    fn last(&self) -> bool;
    /// The byte of the entry this piece starts at; `None` from an older twin.
    fn offset(&self) -> Option<u64>;
    /// The bytes of blob in THIS piece.
    fn piece_len(&self) -> u64;
    fn snapshot_id(&self) -> &str;
    fn snapshot_item(&self) -> Option<u64>;
    /// **Announce the replay on the piece**: `restore_head` always (the box refuses to
    /// finish below it), `restore_bytes` when the pump was told a size, and the twin's
    /// `log_id` when the piece carries none.
    fn stamp(&mut self, restore_head: u64, restore_bytes: Option<u64>, log_id: &str);
}

/// **The views and [`Chunk`] for one generated `gunnar.v1` module.** Every host has its
/// own `pb` (gunnar-proto, gunnar-ui-server's `crate::pb`, the wasm guest's), all
/// compiled from the same `.proto`; this is the one reader of all of them.
///
/// ```ignore
/// gunnar_repair::wire!(crate::pb);
/// ```
#[macro_export]
macro_rules! wire {
    ($($pb:ident)::+) => {
        impl $crate::AsTwinHead for $($pb)::+::LogHeadResponse {
            fn twin_head(&self) -> $crate::TwinHead {
                $crate::TwinHead {
                    first_seq: self.first_seq,
                    head_seq: self.head_seq,
                    bytes: self.bytes,
                    twin: self.twin,
                    applied_seq: self.applied_seq,
                    primary_head_seq: self.primary_head_seq,
                    caught_up: self.caught_up,
                    state: self.state.clone(),
                    why: self.why.clone(),
                    last_answer_unix_ms: self.last_answer_unix_ms,
                    answer_ago_ms: self.answer_ago_ms,
                    log_id: self.log_id.clone(),
                    last_level_unix_ms: self.last_level_unix_ms,
                    snapshot_at: self.snapshot_at,
                    snapshot_bytes: self.snapshot_bytes,
                }
            }
        }
        impl $crate::AsMainStatus for $($pb)::+::BootstrapStatusResponse {
            fn main_status(&self) -> $crate::MainStatus {
                $crate::MainStatus {
                    window_open: self.window_open,
                    bootstrapped: self.bootstrapped,
                    bootstrapped_principal: self.bootstrapped_principal.clone(),
                    principals: self.principals,
                    keys: self.keys,
                    detail: self.detail.clone(),
                    serving_git: self.serving_git,
                    readiness: self.readiness.clone(),
                    role: self.role.clone(),
                    recharge_from: self.recharge_from.trim().to_owned(),
                    restoring: self.restoring,
                    log_head_seq: self.log_head_seq,
                    restore_partial_seq: self.restore_partial_seq,
                    restore_partial_bytes: self.restore_partial_bytes,
                    restore_snapshot_resumable: self.restore_snapshot_resumable,
                    restore_snapshot_id: self.restore_snapshot_id.clone(),
                    restore_snapshot_items: self.restore_snapshot_items,
                    restore_snapshot_bytes: self.restore_snapshot_bytes,
                    restore_snapshot_whole: self.restore_snapshot_whole,
                    data_total_bytes: self.facts.as_ref().and_then(|f| f.data_total_bytes).unwrap_or(0),
                }
            }
        }
        impl $crate::AsRestored for $($pb)::+::RestoreResponse {
            fn restored(&self) -> $crate::Restored {
                $crate::Restored {
                    head_seq: self.head_seq,
                    applied: self.applied,
                    lost: self.lost,
                    restart_required: self.restart_required,
                    detail: self.detail.clone(),
                }
            }
        }
        impl $crate::Chunk for $($pb)::+::LogChunk {
            fn seq(&self) -> u64 {
                self.seq
            }
            fn kind(&self) -> u32 {
                self.kind
            }
            fn last(&self) -> bool {
                self.last
            }
            fn offset(&self) -> Option<u64> {
                self.offset
            }
            fn piece_len(&self) -> u64 {
                self.blob.len() as u64
            }
            fn snapshot_id(&self) -> &str {
                &self.snapshot_id
            }
            fn snapshot_item(&self) -> Option<u64> {
                self.snapshot_item
            }
            fn stamp(&mut self, restore_head: u64, restore_bytes: Option<u64>, log_id: &str) {
                self.restore_head = restore_head;
                if let Some(bytes) = restore_bytes {
                    self.restore_bytes = bytes;
                }
                if self.log_id.trim().is_empty() {
                    self.log_id = log_id.to_owned();
                }
            }
        }
    };
}