1use std::{
2 collections::BTreeMap,
3 fmt::{Debug, Display},
4 future::Future,
5 path::PathBuf,
6 sync::Arc,
7 time::{Duration, Instant},
8};
9
10use anyhow::{Context, ensure};
11use async_lock::RwLock;
12use derivative::Derivative;
13use espresso_types::{
14 NodeState, Transaction, ValidatedState,
15 v0::traits::{EventConsumer as PersistenceEventConsumer, SequencerPersistence},
16};
17use futures::{
18 future::join_all,
19 stream::{BoxStream, Stream, StreamExt},
20};
21use hotshot_new_protocol::{
22 coordinator::Coordinator,
23 leader_trace::{CsvLeaderTracer, LeaderTracerHandle},
24 network::{Cliquenet, NetworkError},
25};
26use hotshot_orchestrator::client::OrchestratorClient;
27use hotshot_types::{
28 HotShotConfig, PeerConfig, ValidatorConfig,
29 consensus::ConsensusMetricsValue,
30 constants::EXTERNAL_EVENT_CHANNEL_SIZE,
31 data::{Leaf2, ViewNumber},
32 drb::drb_difficulty_selector,
33 epoch_membership::EpochMembershipCoordinator,
34 initializer::{HotShotInitializer, load_start_epoch_info},
35 message::UpgradeLock,
36 network::NetworkConfig,
37 new_protocol::CoordinatorEvent,
38 traits::{
39 election::Membership,
40 metrics::{Counter, Gauge, Histogram, Metrics},
41 },
42 upgrade_config::UpgradeConfig,
43};
44use parking_lot::Mutex;
45use request_response::RequestResponseConfig;
46use tokio::{
47 select, spawn,
48 sync::{
49 broadcast,
50 mpsc::{channel, unbounded_channel},
51 watch,
52 },
53 task::JoinHandle,
54};
55use tracing::{Instrument, Level, info};
56use url::Url;
57use versions::{NEW_PROTOCOL_VERSION, Version};
58
59use crate::{
60 SeqTypes, SequencerApiVersion,
61 catchup::ParallelStateCatchup,
62 consensus_handle::ConsensusHandle,
63 external_event_handler::ExternalEventHandler,
64 proposal_fetcher::ProposalFetcherConfig,
65 request_response::{
66 RequestResponseProtocol,
67 data_source::{DataSource, Storage as RequestResponseStorage},
68 network::Sender as RequestResponseSender,
69 recipient_source::RecipientSource,
70 },
71 startup_catchup::bootstrap_epoch_window,
72 state_signature::{self, DecidedLeaf, StateSignatureMemStorage, StateSigner},
73};
74
75const REQUEST_RESPONSE_CHANNEL_CAPACITY: usize = 256;
79
80const OUTBOUND_MESSAGE_CHANNEL_CAPACITY: usize = 128;
82
83#[derive(Derivative, Clone)]
85#[derivative(Debug(bound = ""))]
86pub struct SequencerContext<P: SequencerPersistence> {
87 #[derivative(Debug = "ignore")]
88 consensus_handle: Arc<ConsensusHandle<SeqTypes, Arc<P>>>,
89
90 #[derivative(Debug = "ignore")]
91 persistence: Arc<P>,
92
93 #[derivative(Debug = "ignore")]
95 #[allow(dead_code)]
96 pub request_response_protocol: RequestResponseProtocol<P>,
97
98 state_signatures: Arc<RwLock<StateSignatureMemStorage>>,
100
101 #[derivative(Debug = "ignore")]
104 wait_for_orchestrator: Option<(Arc<OrchestratorClient>, PeerConfig<SeqTypes>)>,
105
106 tasks: TaskList,
108
109 detached: bool,
110
111 node_state: NodeState,
112
113 network_config: NetworkConfig<SeqTypes>,
114
115 #[derivative(Debug = "ignore")]
116 validator_config: ValidatorConfig<SeqTypes>,
117}
118
119impl<P> SequencerContext<P>
120where
121 P: SequencerPersistence,
122{
123 #[tracing::instrument(skip_all, fields(node_id = initializer.instance_state().node_id))]
124 #[allow(clippy::too_many_arguments)]
125 pub async fn init<F>(
126 network_config: NetworkConfig<SeqTypes>,
127 upgrade: versions::Upgrade,
128 validator_config: ValidatorConfig<SeqTypes>,
129 membership_coordinator: EpochMembershipCoordinator<SeqTypes>,
130 initializer: HotShotInitializer<SeqTypes>,
131 anchor_view: Option<ViewNumber>,
132 storage: Option<RequestResponseStorage>,
133 state_catchup: ParallelStateCatchup,
134 persistence: Arc<P>,
135 coordinator_network: F,
136 state_relay_server: Option<Url>,
137 metrics: &dyn Metrics,
138 stake_table_capacity: usize,
139 event_consumer: impl PersistenceEventConsumer + 'static,
140 proposal_fetcher_cfg: ProposalFetcherConfig,
141 bootstrap_epoch_catchup_timeout: Duration,
142 empty_block_delay: Duration,
143 block_sizes: BTreeMap<Version, u64>,
144 leader_trace_dir: Option<PathBuf>,
145 ) -> anyhow::Result<Self>
146 where
147 F: AsyncFnOnce(UpgradeLock<SeqTypes>) -> Result<Cliquenet<SeqTypes>, NetworkError>,
148 {
149 let config = &network_config.config;
150 let pub_key = validator_config.public_key;
151 tracing::info!(%pub_key, "initializing consensus");
152 let upgrade_lock = restored_upgrade_lock(upgrade, &initializer);
153 let version = running_version(&upgrade_lock);
154 ensure!(
155 version >= NEW_PROTOCOL_VERSION,
156 "the chain runs version {version}, which predates the new protocol \
157 ({NEW_PROTOCOL_VERSION}), the only consensus this node runs"
158 );
159
160 let instance_state = initializer.instance_state().clone();
161
162 metrics
164 .create_gauge("node_index".into(), None)
165 .set(instance_state.node_id as usize);
166
167 instance_state.l1_client.spawn_tasks().await;
169
170 info!(target: "announce", ?initializer, "starting up sequencer context with initializer");
171
172 let stake_table = config.hotshot_stake_table();
173 let stake_table_commit = stake_table.commitment(stake_table_capacity)?;
174 let stake_table_epoch = None;
175 let should_vote =
176 state_signature::should_vote(&stake_table, &validator_config.state_public_key);
177
178 let epoch_height = initializer.epoch_height();
179
180 seed_membership(&membership_coordinator, config, &upgrade_lock, &initializer).await;
181
182 let mut coordinator_network = coordinator_network(upgrade_lock.clone()).await?;
183
184 let current_epoch = bootstrap_epoch_window(
185 &membership_coordinator,
186 epoch_height,
187 bootstrap_epoch_catchup_timeout,
188 )
189 .await
190 .context("startup stake-table catchup failed")?;
191 tracing::info!(%current_epoch, "Startup catchup complete");
192
193 if let Err(err) = coordinator_network.apply_epoch(current_epoch, &membership_coordinator) {
195 tracing::warn!(%current_epoch, %err, "coordinator network apply_epoch failed at startup");
196 }
197
198 let locked_qc = persistence
201 .load_high_qc2()
202 .await
203 .context("loading persisted locked QC")?;
204 let boundary_qc = persistence
207 .load_boundary_qc2()
208 .await
209 .context("loading persisted boundary QC")?;
210 let anchor_cert2 = persistence
211 .load_cert2(initializer.anchor_leaf().view_number())
212 .await
213 .context("loading the anchor's cert2")?;
214
215 let leader_tracer = leader_trace_dir
216 .map(|dir| {
217 let node_id = instance_state.node_id;
218 let path = dir.join(format!("leader_trace_node{node_id}.csv"));
219 CsvLeaderTracer::new(node_id, &path)
220 .map(|tracer| Arc::new(tracer) as LeaderTracerHandle)
221 .with_context(|| format!("creating leader trace {}", path.display()))
222 })
223 .transpose()?;
224
225 let (external_tx, external_rx) = unbounded_channel();
226 let coordinator = Coordinator::maker()
227 .membership_coordinator(membership_coordinator.clone())
228 .network(coordinator_network)
229 .initializer(&initializer)
230 .upgrade_lock(upgrade_lock.clone())
231 .public_key(validator_config.public_key)
232 .private_key(validator_config.private_key.clone())
233 .state_private_key(validator_config.state_private_key.clone())
234 .stake_table_capacity(stake_table_capacity)
235 .timeout_duration(Duration::from_secs(10))
236 .empty_block_delay(empty_block_delay)
237 .block_sizes(block_sizes)
238 .storage(Arc::clone(&persistence))
239 .metrics(metrics)
240 .consensus_metrics(ConsensusMetricsValue::new(metrics))
241 .maybe_locked_qc(locked_qc)
242 .maybe_boundary_qc(boundary_qc)
243 .maybe_anchor_cert2(anchor_cert2)
244 .maybe_leader_tracer(leader_tracer)
245 .upgrade_config(UpgradeConfig {
246 start_proposing_view: config.start_proposing_view,
247 stop_proposing_view: config.stop_proposing_view,
248 start_voting_view: config.start_voting_view,
249 stop_voting_view: config.stop_voting_view,
250 start_proposing_time: config.start_proposing_time,
251 stop_proposing_time: config.stop_proposing_time,
252 start_voting_time: config.start_voting_time,
253 stop_voting_time: config.stop_voting_time,
254 })
255 .external_tx(external_tx)
256 .make();
257
258 let consensus_handle = Arc::new(ConsensusHandle::new(
259 coordinator,
260 external_rx,
261 upgrade_lock,
262 membership_coordinator.clone(),
263 &initializer,
264 EXTERNAL_EVENT_CHANNEL_SIZE,
265 metrics,
266 ));
267
268 let mut state_signer = StateSigner::new(
269 validator_config.state_private_key.clone(),
270 validator_config.state_public_key.clone(),
271 stake_table_commit,
272 stake_table_epoch,
273 stake_table_capacity,
274 should_vote,
275 );
276 if let Some(url) = state_relay_server {
277 state_signer = state_signer.with_relay_server(url);
278 }
279
280 let (outbound_message_sender, outbound_message_receiver) =
282 channel(OUTBOUND_MESSAGE_CHANNEL_CAPACITY);
283 let (request_response_sender, request_response_receiver) =
284 channel(REQUEST_RESPONSE_CHANNEL_CAPACITY);
285
286 let request_response_config = RequestResponseConfig {
288 incoming_request_ttl: Duration::from_secs(40),
289 incoming_request_timeout: Duration::from_secs(5),
290 request_batch_size: 5,
291 request_batch_interval: Duration::from_secs(2),
292 max_incoming_requests: 32,
295 max_incoming_requests_per_key: 4,
296 };
297
298 let request_response_protocol = RequestResponseProtocol::new(
300 request_response_config,
301 RequestResponseSender::new(outbound_message_sender),
302 request_response_receiver,
303 RecipientSource {
304 memberships: membership_coordinator,
305 consensus_handle: consensus_handle.clone(),
306 public_key: validator_config.public_key,
307 },
308 DataSource {
309 node_state: instance_state.clone(),
310 storage,
311 persistence: persistence.clone(),
312 consensus_handle: consensus_handle.clone(),
313 },
314 validator_config.public_key,
315 validator_config.private_key.clone(),
316 );
317
318 state_catchup.add_provider(Arc::new(request_response_protocol.clone()));
322
323 let mut tasks = TaskList::default();
325 let external_event_handler = ExternalEventHandler::new(
326 &mut tasks,
327 request_response_sender,
328 outbound_message_receiver,
329 consensus_handle.clone(),
330 pub_key,
331 metrics
332 .subgroup("request_response".into())
333 .create_counter("inbound_dropped".into(), None)
334 .into(),
335 )
336 .await
337 .with_context(|| "Failed to create external event handler")?;
338
339 Ok(Self::new(
340 consensus_handle,
341 persistence,
342 state_signer,
343 external_event_handler,
344 request_response_protocol,
345 instance_state,
346 network_config,
347 validator_config,
348 event_consumer,
349 anchor_view,
350 proposal_fetcher_cfg,
351 metrics,
352 )
353 .with_task_list(tasks))
354 }
355
356 #[allow(clippy::too_many_arguments)]
358 fn new(
359 consensus_handle: Arc<ConsensusHandle<SeqTypes, Arc<P>>>,
360 persistence: Arc<P>,
361 state_signer: StateSigner<SequencerApiVersion>,
362 external_event_handler: ExternalEventHandler,
363 request_response_protocol: RequestResponseProtocol<P>,
364 node_state: NodeState,
365 network_config: NetworkConfig<SeqTypes>,
366 validator_config: ValidatorConfig<SeqTypes>,
367 event_consumer: impl PersistenceEventConsumer + 'static,
368 anchor_view: Option<ViewNumber>,
369 proposal_fetcher_cfg: ProposalFetcherConfig,
370 metrics: &dyn Metrics,
371 ) -> Self {
372 let events = consensus_handle.event_stream();
373
374 let node_id = node_state.node_id;
375 let mut ctx = Self {
376 consensus_handle,
377 persistence: persistence.clone(),
378 state_signatures: Arc::clone(&state_signer.signatures),
379 request_response_protocol,
380 tasks: Default::default(),
381 detached: false,
382 wait_for_orchestrator: None,
383 node_state,
384 network_config,
385 validator_config,
386 };
387
388 proposal_fetcher_cfg.spawn(
390 &mut ctx.tasks,
391 ctx.consensus_handle.clone(),
392 persistence.clone(),
393 metrics,
394 );
395
396 let event_consumer = Arc::new(event_consumer);
398
399 let (decide_tx, decide_rx) = watch::channel::<DecideSignal>(None);
402
403 ctx.spawn(
405 "decide processor",
406 process_decided_events_task(
407 persistence.clone(),
408 event_consumer.clone(),
409 decide_rx,
410 anchor_view,
411 DecideProcessorMetrics::new(metrics),
412 ),
413 );
414
415 let (signer_tx, signer_rx) = broadcast::channel(SIGNER_QUEUE_CAPACITY);
416 ctx.spawn(
417 "state signer",
418 sign_decided_leaves(ctx.consensus_handle.clone(), state_signer, signer_rx),
419 );
420
421 ctx.spawn(
423 "event handler",
424 handle_events(
425 node_id,
426 events,
427 persistence,
428 signer_tx,
429 external_event_handler,
430 event_consumer,
431 decide_tx,
432 ),
433 );
434
435 ctx
436 }
437
438 pub fn wait_for_orchestrator(
443 mut self,
444 client: OrchestratorClient,
445 peer_config: PeerConfig<SeqTypes>,
446 ) -> Self {
447 self.wait_for_orchestrator = Some((Arc::new(client), peer_config));
448 self
449 }
450
451 pub(crate) fn with_task_list(mut self, tasks: TaskList) -> Self {
453 self.tasks.extend(tasks);
454 self
455 }
456
457 pub fn state_signatures(&self) -> Arc<RwLock<StateSignatureMemStorage>> {
459 self.state_signatures.clone()
460 }
461
462 pub fn event_stream(&self) -> BoxStream<'static, CoordinatorEvent<SeqTypes>> {
464 self.consensus_handle.event_stream()
465 }
466
467 pub async fn submit_transaction(&self, tx: Transaction) -> anyhow::Result<()> {
468 self.consensus_handle.submit_transaction(tx).await
469 }
470
471 pub fn consensus_handle(&self) -> Arc<ConsensusHandle<SeqTypes, Arc<P>>> {
472 self.consensus_handle.clone()
473 }
474
475 pub fn validator_config(&self) -> &ValidatorConfig<SeqTypes> {
476 &self.validator_config
477 }
478
479 pub fn upgrade_lock(&self) -> UpgradeLock<SeqTypes> {
480 self.consensus_handle.upgrade_lock()
481 }
482
483 pub async fn shutdown_consensus(&self) {
484 self.consensus_handle.shut_down().await
485 }
486
487 pub async fn decided_leaf(&self) -> Leaf2<SeqTypes> {
488 self.consensus_handle.decided_leaf().await
489 }
490
491 pub async fn state(&self, view: ViewNumber) -> Option<Arc<ValidatedState>> {
492 self.consensus_handle.state(view).await
493 }
494
495 pub async fn decided_state(&self) -> Option<Arc<ValidatedState>> {
496 self.consensus_handle.decided_state().await
497 }
498
499 pub fn node_id(&self) -> u64 {
500 self.node_state.node_id
501 }
502
503 pub fn node_state(&self) -> NodeState {
504 self.node_state.clone()
505 }
506
507 pub fn persistence(&self) -> Arc<P> {
508 self.persistence.clone()
509 }
510
511 pub async fn start_consensus(&self) {
513 if let Some((orchestrator_client, peer_config)) = &self.wait_for_orchestrator {
514 tracing::warn!("waiting for orchestrated start");
515 orchestrator_client
516 .wait_for_all_nodes_ready(PeerConfig::to_bytes(peer_config))
517 .await;
518 } else {
519 tracing::info!("no orchestrator configured");
523 }
524 tracing::warn!("starting consensus");
525 self.consensus_handle.start_consensus().await;
526 }
527
528 pub fn spawn(&mut self, name: impl Display, task: impl Future<Output: Debug> + Send + 'static) {
533 self.tasks.spawn(name, task);
534 }
535
536 pub fn spawn_short_lived(
544 &mut self,
545 name: impl Display,
546 task: impl Future<Output: Debug> + Send + 'static,
547 ) {
548 self.tasks.spawn_short_lived(name, task);
549 }
550
551 pub async fn shut_down(&mut self) {
553 tracing::info!("shutting down SequencerContext");
554 self.consensus_handle.shut_down().await;
555 self.tasks.shut_down();
556 self.node_state.l1_client.shut_down_tasks().await;
557
558 self.detached = true;
561 }
562
563 pub async fn join(&mut self) {
570 select! {
571 () = self.tasks.join() => {},
572 () = self.consensus_handle.stopped() => tracing::error!("coordinator stopped"),
573 }
574 }
575
576 pub fn detach(&mut self) {
578 self.detached = true;
580 }
581
582 pub fn network_config(&self) -> NetworkConfig<SeqTypes> {
584 self.network_config.clone()
585 }
586}
587
588fn restored_upgrade_lock(
591 upgrade: versions::Upgrade,
592 initializer: &HotShotInitializer<SeqTypes>,
593) -> UpgradeLock<SeqTypes> {
594 let decided_upgrade_certificate = initializer
595 .decided_upgrade_certificate()
596 .filter(|cert| cert.data.new_version > upgrade.base)
597 .cloned();
598 tracing::info!(?upgrade, ?decided_upgrade_certificate, "resuming consensus");
599 UpgradeLock::from_certificate(upgrade, &decided_upgrade_certificate)
600}
601
602fn running_version(upgrade_lock: &UpgradeLock<SeqTypes>) -> Version {
604 upgrade_lock
605 .decided_upgrade_cert()
606 .map(|cert| cert.data.new_version)
607 .unwrap_or(upgrade_lock.upgrade().base)
608}
609
610async fn seed_membership(
611 membership_coordinator: &EpochMembershipCoordinator<SeqTypes>,
612 config: &HotShotConfig<SeqTypes>,
613 upgrade_lock: &UpgradeLock<SeqTypes>,
614 initializer: &HotShotInitializer<SeqTypes>,
615) {
616 membership_coordinator.set_drb_difficulty_selector(drb_difficulty_selector(config));
617
618 let running_version = running_version(upgrade_lock);
619 let membership = membership_coordinator.membership();
620 for da_committee in &config.da_committees {
621 if running_version >= da_committee.start_version {
622 membership.add_da_committee(
623 da_committee.start_epoch.into(),
624 da_committee.committee.clone(),
625 );
626 }
627 }
628
629 load_start_epoch_info(membership_coordinator, initializer.start_epoch_info()).await;
630}
631
632impl<P: SequencerPersistence> Drop for SequencerContext<P> {
633 fn drop(&mut self) {
634 if !self.detached {
635 let consensus_handle = self.consensus_handle.clone();
637 let tasks_clone = self.tasks.clone();
638 let node_state_clone = self.node_state.clone();
639
640 spawn(async move {
641 tracing::info!("shutting down SequencerContext");
642 consensus_handle.shut_down().await;
643 tasks_clone.shut_down();
644 node_state_clone.l1_client.shut_down_tasks().await;
645 });
646
647 self.detached = true;
649 }
650 }
651}
652
653type DecideSignal = Option<ViewNumber>;
656
657struct DecideProcessorMetrics {
660 last_decided: Arc<dyn Gauge>,
661 last_processed: Arc<dyn Gauge>,
662 backlog: Arc<dyn Gauge>,
663 duration: Arc<dyn Histogram>,
664 failures: Arc<dyn Counter>,
665}
666
667impl DecideProcessorMetrics {
668 fn new(metrics: &(impl Metrics + ?Sized)) -> Self {
669 let metrics = metrics.subgroup("decide_processor".into());
670 Self {
671 last_decided: metrics
672 .create_gauge("last_decided".into(), Some("view".into()))
673 .into(),
674 last_processed: metrics
675 .create_gauge("last_processed".into(), Some("view".into()))
676 .into(),
677 backlog: metrics
678 .create_gauge("backlog".into(), Some("view".into()))
679 .into(),
680 duration: metrics
681 .create_histogram("process_duration".into(), Some("seconds".into()))
682 .into(),
683 failures: metrics.create_counter("failures".into(), None).into(),
684 }
685 }
686}
687
688#[tracing::instrument(skip_all, fields(node_id))]
689async fn handle_events<P, C>(
690 node_id: u64,
691 mut events: impl Stream<Item = CoordinatorEvent<SeqTypes>> + Unpin,
692 persistence: Arc<P>,
693 signer_tx: broadcast::Sender<DecidedLeaf>,
694 external_event_handler: ExternalEventHandler,
695 event_consumer: Arc<C>,
696 decide_tx: watch::Sender<DecideSignal>,
697) where
698 P: SequencerPersistence,
699 C: PersistenceEventConsumer + 'static,
700{
701 while let Some(event) = events.next().await {
702 tracing::debug!(node_id, ?event, "consensus event");
703
704 match &event {
705 CoordinatorEvent::ExternalMessageReceived { data, .. } => {
706 if let Err(err) = external_event_handler.handle_event(data).await {
707 tracing::warn!("Failed to handle external message: {:?}", err);
708 }
709 },
710 CoordinatorEvent::BlockPayloadReconstructed { .. } => {
711 if let Err(err) = event_consumer.handle_event(&event).await {
715 tracing::warn!("failed to handle reconstructed payload: {err:#}");
716 }
717 },
718 _ => {},
719 }
720
721 if let Some(leaf) = DecidedLeaf::from_event(&event) {
722 let _ = signer_tx.send(leaf);
723 }
724
725 if let Some(signal) = persistence
728 .persist_event(&event, event_consumer.as_ref())
729 .await
730 {
731 decide_tx.send_modify(|current| match current {
734 Some(view) if *view > signal => {},
735 _ => *current = Some(signal),
736 });
737 }
738 }
739}
740
741const SIGNER_QUEUE_CAPACITY: usize = 128;
743
744async fn sign_decided_leaves<P>(
749 consensus_handle: Arc<ConsensusHandle<SeqTypes, Arc<P>>>,
750 mut state_signer: StateSigner<SequencerApiVersion>,
751 mut leaves: broadcast::Receiver<DecidedLeaf>,
752) where
753 P: SequencerPersistence,
754{
755 loop {
756 match leaves.recv().await {
757 Ok(leaf) => {
758 state_signer
759 .handle_decide(&leaf, consensus_handle.as_ref())
760 .await;
761 },
762 Err(broadcast::error::RecvError::Lagged(skipped)) => {
763 tracing::warn!(skipped, "state signer fell behind, dropped oldest decides");
764 },
765 Err(broadcast::error::RecvError::Closed) => break,
766 }
767 }
768}
769
770const PROCESS_RETRY_INTERVAL: Duration = Duration::from_secs(30);
771
772#[tracing::instrument(skip_all)]
776async fn process_decided_events_task<P, C>(
777 persistence: Arc<P>,
778 consumer: Arc<C>,
779 mut decide_rx: watch::Receiver<DecideSignal>,
780 anchor_view: Option<ViewNumber>,
781 metrics: DecideProcessorMetrics,
782) where
783 P: SequencerPersistence,
784 C: PersistenceEventConsumer + 'static,
785{
786 let mut last_processed = anchor_view.map(|v| v.u64()).unwrap_or(0);
789
790 if let Some(view) = anchor_view {
792 match persistence
793 .process_decided_events(view, consumer.as_ref())
794 .await
795 {
796 Ok(processed) => {
797 if let Some(v) = processed {
798 last_processed = last_processed.max(v.u64());
799 }
800 },
801 Err(err) => tracing::warn!(
802 "failed to process decided leaves on startup, chain may not be up to date: {err:#}"
803 ),
804 }
805 }
806
807 let mut latest: DecideSignal = None;
809
810 loop {
811 match tokio::time::timeout(PROCESS_RETRY_INTERVAL, decide_rx.changed()).await {
813 Ok(Ok(())) => latest = *decide_rx.borrow_and_update(),
814 Ok(Err(_)) => {
815 tracing::info!("decide signal channel closed, stopping decide processor");
816 return;
817 },
818 Err(_) => {}, }
820
821 let Some(view) = latest else {
822 continue;
823 };
824 let decided = view.u64();
825 metrics.last_decided.set(decided as usize);
826 metrics
827 .backlog
828 .set(decided.saturating_sub(last_processed) as usize);
829
830 let start = Instant::now();
831 let result = persistence
832 .process_decided_events(view, consumer.as_ref())
833 .await;
834 metrics.duration.add_point(start.elapsed().as_secs_f64());
835
836 match result {
837 Ok(processed) => {
838 if let Some(v) = processed {
841 last_processed = last_processed.max(v.u64());
842 }
843 if let Some(view) = latest
845 && last_processed >= view.u64()
846 {
847 latest = None;
848 }
849 metrics.last_processed.set(last_processed as usize);
850 metrics
851 .backlog
852 .set(decided.saturating_sub(last_processed) as usize);
853 },
854 Err(err) => {
855 metrics.failures.add(1);
857 tracing::warn!(?view, "deferred decide processing failed: {err:#}");
858 },
859 }
860 }
861}
862
863#[derive(Debug, Default, Clone)]
864#[allow(clippy::type_complexity)]
865pub struct TaskList(Arc<Mutex<Vec<(String, JoinHandle<()>)>>>);
866
867macro_rules! spawn_with_log_level {
868 ($this:expr, $lvl:expr, $name:expr, $task: expr) => {
869 let name = $name.to_string();
870 let task = {
871 let name = name.clone();
872 let span = tracing::span!($lvl, "background task", name);
873 spawn(
874 async move {
875 tracing::event!($lvl, "spawning background task");
876 let res = $task.await;
877 tracing::event!($lvl, ?res, "background task exited");
878 }
879 .instrument(span),
880 )
881 };
882 $this.0.lock().push((name, task));
883 };
884}
885
886impl TaskList {
887 pub fn spawn(&mut self, name: impl Display, task: impl Future<Output: Debug> + Send + 'static) {
892 spawn_with_log_level!(self, Level::INFO, name, task);
893 }
894
895 pub fn spawn_short_lived(
903 &mut self,
904 name: impl Display,
905 task: impl Future<Output: Debug> + Send + 'static,
906 ) {
907 spawn_with_log_level!(self, Level::DEBUG, name, task);
908 }
909
910 pub fn shut_down(&self) {
912 let tasks: Vec<(String, JoinHandle<()>)> = self.0.lock().drain(..).collect();
913 for (name, task) in tasks.into_iter().rev() {
914 tracing::info!(name, "cancelling background task");
915 task.abort();
916 }
917 }
918
919 pub async fn join(&mut self) {
921 let tasks: Vec<(String, JoinHandle<()>)> = self.0.lock().drain(..).collect();
922 join_all(tasks.into_iter().map(|(_, task)| task)).await;
923 }
924
925 pub fn extend(&mut self, tasks: TaskList) {
926 self.0.lock().extend(
927 tasks
928 .0
929 .lock()
930 .drain(..)
931 .collect::<Vec<(String, JoinHandle<()>)>>(),
932 );
933 }
934}
935
936impl Drop for TaskList {
937 fn drop(&mut self) {
938 self.shut_down()
939 }
940}