use crate::stateful::{
actor::{
core::mailbox::Message,
processor::{FinalizeStatus, Processor},
},
Application,
};
use commonware_actor::mailbox as actor_mailbox;
use commonware_consensus::{
marshal::{
ancestry::BlockProvider,
core::{Mailbox as MarshalMailbox, Variant},
},
types::Height,
Heightable,
};
use commonware_cryptography::certificate::Scheme;
use commonware_macros::select_loop;
use commonware_runtime::{Clock, ContextCell, Metrics, Spawner};
use commonware_utils::{channel::fallible::OneshotExt, Acknowledgement};
use futures::{
future::{ready, Either},
FutureExt,
};
use rand_core::Rng;
use std::sync::mpsc::TryRecvError;
use tracing::{debug, info_span, Instrument as _};
enum Step<M, P> {
Message(M),
Prune(P),
}
pub(super) struct Processing<E, A, S, V>
where
E: Rng + Spawner + Metrics + Clock,
A: Application<E>,
S: Scheme,
V: Variant<ApplicationBlock = A::Block>,
{
pub(super) context: ContextCell<E>,
pub(super) mailbox: actor_mailbox::Receiver<Message<E, A>>,
pub(super) input_provider: A::InputProvider,
pub(super) marshal: MarshalMailbox<S, V>,
pub(super) processor: Processor<E, A>,
pub(super) skip_finalized_until: Option<Height>,
}
impl<E, A, S, V> Processing<E, A, S, V>
where
E: Rng + Spawner + Metrics + Clock,
A: Application<E>,
S: Scheme,
V: Variant<ApplicationBlock = A::Block>,
MarshalMailbox<S, V>: BlockProvider<Block = A::Block>,
{
pub async fn start(mut self) {
let mut pending_prune = None;
select_loop! {
self.context,
on_start => {
let next = match self.mailbox.try_recv() {
Ok(message) => Either::Left(ready(Some(Step::Message(message)))),
Err(TryRecvError::Empty) => match pending_prune.take() {
Some(prune) => Either::Left(ready(Some(Step::Prune(prune)))),
None => Either::Right(self.mailbox.recv().map(|m| m.map(Step::Message))),
},
Err(TryRecvError::Disconnected) => {
debug!("mailbox closed, stopping processing");
return;
}
};
},
on_stopped => {
debug!("shutdown signal received, stopping processing");
},
Some(step) = next else {
debug!("mailbox closed, stopping processing");
break;
} => match step {
Step::Message(Message::Propose {
span,
context,
ancestry,
response,
}) => {
let process = info_span!(parent: &span, "stateful.actor.propose");
self.processor
.propose(
self.context.as_present(),
self.marshal.clone(),
context,
ancestry,
&mut self.input_provider,
response,
)
.instrument(process)
.await;
}
Step::Message(Message::Verify {
span,
context,
ancestry,
response,
}) => {
let process = info_span!(parent: &span, "stateful.actor.verify");
self.processor
.verify(
self.context.as_present(),
self.marshal.clone(),
context,
ancestry,
response,
)
.instrument(process)
.await;
}
Step::Message(Message::Finalized {
span,
block,
acknowledgement,
}) => {
let process = info_span!(parent: &span, "stateful.actor.finalized");
let prune = async {
if skip_finalized_block(&mut self.skip_finalized_until, block.height()) {
self.processor
.notify_finalized(self.context.as_present(), block.as_ref())
.await;
acknowledgement.acknowledge();
return None;
}
let (status, prune) = self
.processor
.finalize(&self.context, block.as_ref())
.await;
if let FinalizeStatus::Persisted { height } = status {
debug!(height = height.get(), "persisted finalized database batch");
}
acknowledgement.acknowledge();
prune
}
.instrument(process)
.await;
if let Some(prune) = prune {
pending_prune = Some(prune);
}
}
Step::Message(Message::SubscribeDatabases { response }) => {
response.send_lossy(self.processor.databases().clone());
}
Step::Prune(prune) => {
prune
.run(self.processor.databases_mut(), &self.marshal)
.await;
}
},
}
}
}
fn skip_finalized_block(skip_until: &mut Option<Height>, height: Height) -> bool {
let Some(target) = *skip_until else {
return false;
};
if height > target {
*skip_until = None;
return false;
}
if height == target {
*skip_until = None;
}
true
}
#[cfg(test)]
mod tests {
use super::skip_finalized_block;
use commonware_consensus::types::Height;
#[test]
fn skip_finalized_block_skips_through_target_height() {
let mut skip_until = Some(Height::new(3));
assert!(skip_finalized_block(&mut skip_until, Height::new(1)));
assert_eq!(skip_until, Some(Height::new(3)));
assert!(skip_finalized_block(&mut skip_until, Height::new(3)));
assert_eq!(skip_until, None);
assert!(!skip_finalized_block(&mut skip_until, Height::new(4)));
}
#[test]
fn skip_finalized_block_clears_stale_target() {
let mut skip_until = Some(Height::new(3));
assert!(!skip_finalized_block(&mut skip_until, Height::new(4)));
assert_eq!(skip_until, None);
}
}