use std::{
collections::VecDeque,
time::{Duration, Instant},
};
use tokio::sync::{mpsc::UnboundedReceiver, watch};
use tracing::info;
use zakura_chain::block::Height;
use crate::service::{
finalized_state::{FinalizedState, NextVctBlock},
queued_blocks::QueuedCheckpointVerified,
write::{VctRootRepairState, VctRootRepairStatus},
};
const VCT_ROOT_RETRY_WAIT: Duration = Duration::from_millis(500);
const VCT_AWAIT_SUCCESSOR_WAIT: Duration = Duration::from_millis(20);
const VCT_ROOT_STALL_WARN_AFTER: Duration = Duration::from_secs(30);
pub(super) struct VctWriteManager {
lookahead: VecDeque<QueuedCheckpointVerified>,
retry: Option<QueuedCheckpointVerified>,
stall: Option<(Height, Instant)>,
stall_logged: bool,
root_repair_sender: watch::Sender<VctRootRepairStatus>,
root_repair_status: VctRootRepairStatus,
}
impl Default for VctWriteManager {
fn default() -> Self {
let (root_repair_sender, _root_repair_receiver) =
watch::channel(VctRootRepairStatus::default());
Self::new(root_repair_sender)
}
}
impl VctWriteManager {
pub(super) fn new(root_repair_sender: watch::Sender<VctRootRepairStatus>) -> Self {
Self {
lookahead: VecDeque::new(),
retry: None,
stall: None,
stall_logged: false,
root_repair_sender,
root_repair_status: VctRootRepairStatus::default(),
}
}
pub(super) fn take_ready(&mut self) -> Option<QueuedCheckpointVerified> {
self.retry.take().or_else(|| self.lookahead.pop_front())
}
pub(super) fn reset(&mut self, finalized_state: &mut FinalizedState) {
self.lookahead.clear();
finalized_state.clear_vct_prevalidated_next();
self.publish_root_repair_idle();
}
pub(super) fn fill_successor(
&mut self,
receiver: &mut UnboundedReceiver<QueuedCheckpointVerified>,
current: &QueuedCheckpointVerified,
) {
loop {
let front_links = self
.lookahead
.front()
.map(|next| next.0.block.header.previous_block_hash == current.0.hash);
match front_links {
Some(true) => break,
Some(false) => {
let dropped = self
.lookahead
.pop_front()
.expect("the front entry was just inspected");
tracing::debug!(
current_height = ?current.0.height,
current_hash = ?current.0.hash,
dropped_height = ?dropped.0.height,
dropped_hash = ?dropped.0.hash,
"dropping a buffered block that does not extend the block being \
committed. Assuming a parent block failed, and dropping this block",
);
}
None => match receiver.try_recv() {
Ok(next) => self.lookahead.push_back(next),
Err(_) => break,
},
}
}
}
pub(super) fn next_vct_block(&self) -> Option<NextVctBlock> {
self.lookahead
.front()
.map(|next| NextVctBlock::from_block(next.0.block.clone(), next.0.auth_data_root))
}
pub(super) fn on_commit_success(&mut self) {
if self.stall.is_some() {
if self.stall_logged {
info!(
stalled_height = ?self.stall.map(|(h, _)| h),
"VCT: checkpoint commit recovered; the stalled height now has a verifiable supplied root"
);
metrics::gauge!("state.vct.root.stalled.height").set(0.0);
}
self.stall = None;
self.stall_logged = false;
}
self.publish_root_repair_idle();
}
pub(super) fn on_retryable_error(
&mut self,
height: Height,
root_unavailable: bool,
had_root_candidate: bool,
block: QueuedCheckpointVerified,
) -> Duration {
metrics::counter!("state.vct.root.retry.count").increment(1);
if root_unavailable {
self.publish_root_repair_needed(height, had_root_candidate);
}
let new_stall = match self.stall {
Some((stuck, _)) if stuck == height => false,
_ => {
self.stall = Some((height, Instant::now()));
self.stall_logged = false;
true
}
};
if !self.stall_logged
&& self
.stall
.is_some_and(|(_, since)| since.elapsed() >= VCT_ROOT_STALL_WARN_AFTER)
{
tracing::error!(
?height,
root_unavailable,
stalled_for = ?VCT_ROOT_STALL_WARN_AFTER,
"VCT: checkpoint commit stalled waiting for a verifiable supplied root \
or successor witness; the node will not recompute against the frozen frontier"
);
metrics::gauge!("state.vct.root.stalled.height").set(f64::from(height.0));
self.stall_logged = true;
} else if new_stall {
tracing::warn!(
?height,
block_height = ?block.0.height,
block_hash = ?block.0.hash,
root_unavailable,
"VCT: supplied root not yet verifiable; retrying checkpoint commit in place"
);
} else {
tracing::trace!(
?height,
block_height = ?block.0.height,
block_hash = ?block.0.hash,
root_unavailable,
"VCT: supplied root still not verifiable; retrying checkpoint commit in place"
);
}
self.retry = Some(block);
if root_unavailable {
VCT_ROOT_RETRY_WAIT
} else {
VCT_AWAIT_SUCCESSOR_WAIT
}
}
fn publish_root_repair_needed(&mut self, height: Height, had_root_candidate: bool) {
let same_height =
self.root_repair_status.state == VctRootRepairState::Unavailable { height };
if same_height && !had_root_candidate {
return;
}
self.root_repair_status = VctRootRepairStatus {
state: VctRootRepairState::Unavailable { height },
generation: self.root_repair_status.generation.saturating_add(1),
};
let _ = self.root_repair_sender.send(self.root_repair_status);
metrics::counter!("state.vct.root.repair.requested").increment(1);
}
fn publish_root_repair_idle(&mut self) {
if self.root_repair_status.state == VctRootRepairState::Idle {
return;
}
self.root_repair_status = VctRootRepairStatus {
state: VctRootRepairState::Idle,
generation: self.root_repair_status.generation,
};
let _ = self.root_repair_sender.send(self.root_repair_status);
}
}
#[cfg(test)]
mod tests;