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();
}
}
}
};
}