1use crate::{
3 merkle::{Family, Location},
4 qmdb::{
5 self,
6 sync::{
7 Database, Error as SyncError, Journal, Metrics, SourceFor, Target,
8 database::Config as _,
9 error::EngineError,
10 requests::{Id as RequestId, Requests},
11 source::{Request, Response, Source},
12 },
13 },
14};
15use commonware_codec::Encode;
16use commonware_cryptography::{Digest, Hasher};
17use commonware_macros::{boxed, select};
18use commonware_runtime::Supervisor as _;
19use commonware_utils::channel::{mpsc, ring};
20use futures::future::{Aborted, Either, pending};
21use mpsc::error::TryRecvError;
22use std::{
23 collections::BTreeMap,
24 fmt::Debug,
25 num::{NonZeroU64, NonZeroUsize},
26 sync::Arc,
27};
28
29type Error<DB, S> =
31 qmdb::sync::Error<<DB as Database>::Family, <S as Source>::Error, <DB as Database>::Digest>;
32
33#[derive(Debug)]
35pub(crate) enum NextStep<C, D> {
36 Continue(C),
38 Complete(D),
40}
41
42#[derive(Debug)]
44enum Event<F: Family, Op, D: Digest, E> {
45 TargetUpdate(Target<F, D>),
47 BatchReceived(Result<IndexedFetchResult<F, Op, D, E>, Aborted>),
49 UpdateChannelClosed,
51 FinishRequested,
53 FinishChannelClosed,
55}
56
57#[derive(Debug)]
59pub(super) struct IndexedFetchResult<F: Family, Op, D: Digest, E> {
60 pub id: RequestId,
62 pub result: Result<Option<Response<F, Op, D>>, E>,
64}
65
66fn verify_response<F, Op, H>(
68 request: Request<F>,
69 root: &H::Digest,
70 response: &Response<F, Op, H::Digest>,
71) -> bool
72where
73 F: Family,
74 Op: Encode,
75 H: Hasher,
76{
77 if response.proof().leaves != request.size() {
78 return false;
79 }
80
81 let hasher = qmdb::hasher::<H>();
82 match (&request, response) {
83 (
84 Request::Operations { start, max_ops, .. },
85 Response::Operations { proof, operations },
86 ) => {
87 let operations_len = operations.len() as u64;
88 if operations_len == 0 || operations_len > max_ops.get() {
89 false
90 } else {
91 let elements = operations.iter().map(Encode::encode).collect::<Vec<_>>();
92 proof.verify_range_inclusion(&hasher, &elements, *start, root)
93 }
94 }
95 (
96 Request::Boundary { start, .. },
97 Response::Boundary {
98 proof,
99 op,
100 pinned_nodes,
101 },
102 ) => {
103 proof.verify_proof_and_pinned_nodes(&hasher, &[op.encode()], *start, pinned_nodes, root)
104 }
105 _ => false,
106 }
107}
108
109async fn wait_for_event<F: Family, Op: Send, D: Digest, E: Send>(
112 update_rx: &mut Option<ring::Receiver<Target<F, D>>>,
113 finish_rx: &mut Option<mpsc::Receiver<()>>,
114 outstanding_requests: &mut Requests<F, Op, D, E>,
115) -> Option<Event<F, Op, D, E>> {
116 if outstanding_requests.len() == 0 && update_rx.is_none() && finish_rx.is_none() {
117 return None;
118 }
119
120 let target_update_fut = update_rx.as_mut().map_or_else(
121 || Either::Right(pending()),
122 |update_rx| Either::Left(update_rx.recv()),
123 );
124 let finish_fut = finish_rx.as_mut().map_or_else(
125 || Either::Right(pending()),
126 |finish_rx| Either::Left(finish_rx.recv()),
127 );
128 let batch_result_fut = outstanding_requests.next_completed();
129
130 select! {
131 finish = finish_fut => finish.map_or_else(
132 || Some(Event::FinishChannelClosed),
133 |_| Some(Event::FinishRequested)
134 ),
135 target = target_update_fut => target.map_or_else(
136 || Some(Event::UpdateChannelClosed),
137 |target| Some(Event::TargetUpdate(target))
138 ),
139 result = batch_result_fut => Some(Event::BatchReceived(result)),
140 }
141}
142
143pub struct Config<DB, S>
145where
146 DB: Database,
147{
148 pub context: DB::Context,
150 pub source: S,
152 pub target: Target<DB::Family, DB::Digest>,
157 pub max_outstanding_requests: NonZeroUsize,
165 pub fetch_batch_size: NonZeroU64,
167 pub apply_batch_size: NonZeroU64,
169 pub db_config: DB::Config,
171 pub update_rx: Option<ring::Receiver<Target<DB::Family, DB::Digest>>>,
177 pub finish_rx: Option<mpsc::Receiver<()>>,
181 pub reached_target_tx: Option<ring::Sender<Target<DB::Family, DB::Digest>>>,
186}
187pub(crate) struct Engine<DB, S>
189where
190 DB: Database,
191 S: SourceFor<DB>,
192{
193 outstanding_requests: Requests<DB::Family, DB::Op, DB::Digest, S::Error>,
195
196 fetched_operations: BTreeMap<Location<DB::Family>, Vec<DB::Op>>,
202
203 pinned_nodes: Option<Vec<DB::Digest>>,
205
206 target: Target<DB::Family, DB::Digest>,
208
209 max_outstanding_requests: NonZeroUsize,
211
212 fetch_batch_size: NonZeroU64,
214
215 apply_batch_size: NonZeroU64,
217
218 journal: DB::Journal,
220
221 source: Arc<S>,
223
224 context: DB::Context,
226
227 config: DB::Config,
229
230 update_rx: Option<ring::Receiver<Target<DB::Family, DB::Digest>>>,
232
233 finish_requested: bool,
235
236 finish_rx: Option<mpsc::Receiver<()>>,
240
241 reached_target_tx: Option<ring::Sender<Target<DB::Family, DB::Digest>>>,
243
244 metrics: Metrics,
246
247 reached_current_target_reported: bool,
249}
250
251#[cfg(test)]
252impl<DB, S> Engine<DB, S>
253where
254 DB: Database,
255 S: SourceFor<DB>,
256{
257 pub(crate) fn journal(&self) -> &DB::Journal {
258 &self.journal
259 }
260}
261
262impl<DB, S> Engine<DB, S>
263where
264 DB: Database,
265 S: SourceFor<DB>,
266{
267 pub async fn new(config: Config<DB, S>) -> Result<Self, Error<DB, S>> {
268 if !config.target.range.end().is_valid() {
269 return Err(SyncError::Engine(EngineError::InvalidTarget {
270 lower_bound_pos: config.target.range.start(),
271 upper_bound_pos: config.target.range.end(),
272 }));
273 }
274
275 let journal = <DB::Journal as Journal<DB::Family>>::new(
277 config.context.child("journal"),
278 config.db_config.journal_config(),
279 config.target.range.clone(),
280 )
281 .await?;
282 let journal_size = journal.size();
283
284 let pinned_nodes = if journal_size == *config.target.range.end() {
289 DB::local_pinned_nodes(
290 config.context.child("local_pinned_nodes"),
291 &config.db_config,
292 &config.target,
293 &journal,
294 )
295 .await?
296 } else {
297 None
298 };
299
300 let sync_context = config.context.child("sync");
301 let metrics = Metrics::new(&sync_context);
302 let mut engine = Self {
303 outstanding_requests: Requests::new(),
304 fetched_operations: BTreeMap::new(),
305 pinned_nodes,
306 target: config.target.clone(),
307 max_outstanding_requests: config.max_outstanding_requests,
308 fetch_batch_size: config.fetch_batch_size,
309 apply_batch_size: config.apply_batch_size,
310 journal,
311 source: Arc::new(config.source),
312 context: config.context,
313 config: config.db_config,
314 update_rx: config.update_rx,
315 finish_requested: false,
316 finish_rx: config.finish_rx,
317 reached_target_tx: config.reached_target_tx,
318 reached_current_target_reported: false,
319 metrics,
320 };
321 engine.schedule_requests();
322 engine.record_progress();
323 Ok(engine)
324 }
325
326 fn spawn_fetch(&mut self, request: Request<DB::Family>) {
328 let source = Arc::clone(&self.source);
329 let root = self.target.root;
330 self.outstanding_requests
331 .insert(request, move |id| async move {
332 let result: Result<_, S::Error> = async {
333 let (mut response, mut feedback) = source.serve(request).await?;
334 loop {
335 if verify_response::<DB::Family, DB::Op, DB::Hasher>(
336 request, &root, &response,
337 ) {
338 if let Some(feedback) = feedback {
339 feedback.accept();
340 }
341 return Ok(Some(response));
342 }
343
344 let Some(current) = feedback else {
345 return Ok(None);
346 };
347 let Some((next_response, next_feedback)) = current.reject().await else {
348 return Ok(None);
349 };
350 response = next_response;
351 feedback = Some(next_feedback);
352 }
353 }
354 .await;
355 IndexedFetchResult { id, result }
356 });
357 }
358
359 fn schedule_requests(&mut self) {
361 let target_size = self.target.range.end();
362
363 if !self.pinned_nodes_ready()
367 && !self
368 .outstanding_requests
369 .contains(&self.target.range.start())
370 {
371 let request = Request::Boundary {
372 size: target_size,
373 start: self.target.range.start(),
374 };
375 self.spawn_fetch(request);
376 }
377
378 let num_requests = self
380 .max_outstanding_requests
381 .get()
382 .saturating_sub(self.outstanding_requests.len());
383
384 let log_size = self.journal.size();
385
386 for _ in 0..num_requests {
387 let Some(gap_range) = crate::qmdb::sync::gaps::find_next(
389 Location::new(log_size)..self.target.range.end(),
390 self.fetched_operations.iter().map(|(&start, operations)| {
391 start..start.checked_add(operations.len() as u64).unwrap()
392 }),
393 self.outstanding_requests.ranges(),
394 ) else {
395 break; };
397
398 let gap_size = *gap_range.end.checked_sub(*gap_range.start).unwrap();
400 let gap_size: NonZeroU64 = gap_size.try_into().unwrap();
401 let batch_size = self.fetch_batch_size.min(gap_size);
402
403 let request = Request::Operations {
405 size: target_size,
406 start: gap_range.start,
407 max_ops: batch_size,
408 };
409 self.spawn_fetch(request);
410 }
411 }
412
413 pub async fn reset_for_target_update(
419 mut self,
420 new_target: Target<DB::Family, DB::Digest>,
421 ) -> Result<Self, Error<DB, S>> {
422 let start_moved = self.target.range.start() != new_target.range.start();
423 self.journal = self.journal.resize(new_target.range.start()).await?;
424 if start_moved {
425 self.pinned_nodes = None;
426
427 let new_start = new_target.range.start();
431 self.outstanding_requests
432 .retain(|request| request.start() > new_start);
433 }
434
435 self.target = new_target;
436 self.reached_current_target_reported = false;
437 Ok(self)
438 }
439
440 fn drain_finish_requests(&mut self) -> Result<(), Error<DB, S>> {
447 let Some(finish_rx) = self.finish_rx.as_mut() else {
448 return Ok(());
449 };
450 match finish_rx.try_recv() {
451 Ok(()) => {
452 self.finish_rx = None;
453 self.finish_requested = true;
454 Ok(())
455 }
456 Err(TryRecvError::Empty) => Ok(()),
457 Err(TryRecvError::Disconnected) => {
458 Err(SyncError::Engine(EngineError::FinishChannelClosed))
459 }
460 }
461 }
462
463 fn report_reached_target(&mut self) {
469 if self.reached_current_target_reported {
470 return;
471 }
472 if let Some(sender) = self.reached_target_tx.as_ref()
473 && !sender.send_lossy(self.target.clone())
474 {
475 self.reached_target_tx = None;
476 }
477 self.reached_current_target_reported = true;
478 }
479
480 fn record_progress(&mut self) {
482 self.metrics.record_target(*self.target.range.end());
483 self.metrics.record_synced(self.journal.size());
484 }
485
486 pub(crate) fn store_operations(
491 &mut self,
492 start_loc: Location<DB::Family>,
493 operations: Vec<DB::Op>,
494 ) {
495 if operations.is_empty() {
496 return;
497 }
498 self.fetched_operations.insert(start_loc, operations);
499 }
500
501 pub(crate) async fn apply_operations(mut self) -> Result<Self, Error<DB, S>> {
507 let mut next_loc = self.journal.size();
508
509 self.fetched_operations.retain(|&start_loc, operations| {
512 assert!(!operations.is_empty());
513 let end_loc = start_loc.checked_add(operations.len() as u64 - 1).unwrap();
514 end_loc >= next_loc
515 });
516
517 loop {
518 let range_start_loc =
521 self.fetched_operations
522 .iter()
523 .find_map(|(range_start, range_ops)| {
524 assert!(!range_ops.is_empty());
525 let range_end =
526 range_start.checked_add(range_ops.len() as u64 - 1).unwrap();
527 if *range_start <= next_loc && next_loc <= range_end {
528 Some(*range_start)
529 } else {
530 None
531 }
532 });
533
534 let Some(range_start_loc) = range_start_loc else {
535 break;
537 };
538
539 let mut operations = self.fetched_operations.remove(&range_start_loc).unwrap();
541 assert!(!operations.is_empty());
542 operations.drain(..(next_loc - *range_start_loc) as usize);
546 next_loc += operations.len() as u64;
547 self.journal = self.journal.append(operations).await?;
548 }
549
550 Ok(self)
551 }
552
553 fn is_at_target(&self) -> Result<bool, Error<DB, S>> {
555 let journal_size = self.journal.size();
556 let target_journal_size = self.target.range.end();
557
558 if journal_size >= target_journal_size {
560 if journal_size > target_journal_size {
561 return Err(SyncError::Engine(EngineError::InvalidState));
563 }
564 return Ok(true);
565 }
566
567 Ok(false)
568 }
569
570 fn needs_pinned_nodes(&self) -> bool {
572 self.target.range.start() > Location::new(0)
573 }
574
575 fn pinned_nodes_ready(&self) -> bool {
577 !self.needs_pinned_nodes() || self.pinned_nodes.is_some()
578 }
579
580 fn is_ready_to_complete(&self) -> Result<bool, Error<DB, S>> {
582 Ok(self.is_at_target()? && self.pinned_nodes_ready())
583 }
584
585 fn handle_fetch_result(
587 &mut self,
588 fetch_result: IndexedFetchResult<DB::Family, DB::Op, DB::Digest, S::Error>,
589 ) -> Result<(), Error<DB, S>> {
590 let Some(request) = self.outstanding_requests.remove(fetch_result.id) else {
592 return Ok(());
593 };
594
595 let response = fetch_result
596 .result
597 .map_err(SyncError::Source)?
598 .ok_or(SyncError::Engine(EngineError::InvalidResponse))?;
599
600 let start_loc = request.start();
601 match response {
602 Response::Operations { operations, .. } => {
603 self.store_operations(start_loc, operations);
604 }
605 Response::Boundary {
606 op, pinned_nodes, ..
607 } => {
608 self.pinned_nodes = Some(pinned_nodes);
611 self.fetched_operations
612 .entry(start_loc)
613 .or_insert_with(|| vec![op]);
614 }
615 }
616
617 Ok(())
618 }
619
620 async fn handle_event(
622 mut self,
623 event: Event<DB::Family, DB::Op, DB::Digest, S::Error>,
624 ) -> Result<NextStep<Self, DB>, Error<DB, S>> {
625 match event {
626 Event::TargetUpdate(new_target) => {
627 if !new_target.advances(&self.target) {
629 return Ok(NextStep::Continue(self));
630 }
631 if new_target.root == self.target.root {
634 return Err(SyncError::Engine(EngineError::SyncTargetRootUnchanged));
635 }
636
637 let mut updated_self = self.reset_for_target_update(new_target).await?;
638 updated_self.record_progress();
639 updated_self.schedule_requests();
640 Ok(NextStep::Continue(updated_self))
641 }
642 Event::UpdateChannelClosed => {
643 self.update_rx = None;
644 Ok(NextStep::Continue(self))
645 }
646 Event::FinishRequested => {
647 self.finish_rx = None;
648 self.finish_requested = true;
649 Ok(NextStep::Continue(self))
650 }
651 Event::FinishChannelClosed => Err(SyncError::Engine(EngineError::FinishChannelClosed)),
652 Event::BatchReceived(fetch_result) => {
653 if let Ok(fetch_result) = fetch_result {
655 self.handle_fetch_result(fetch_result)?;
656 }
657 self.schedule_requests();
658 let mut engine = self.apply_operations().await?;
659 engine.record_progress();
660 Ok(NextStep::Continue(engine))
661 }
662 }
663 }
664
665 #[boxed]
676 pub(crate) async fn step(mut self) -> Result<NextStep<Self, DB>, Error<DB, S>> {
677 self.drain_finish_requests()?;
678
679 if self.is_ready_to_complete()? {
681 self.report_reached_target();
683
684 if !self.finish_requested {
688 while let Some(update_rx) = self.update_rx.as_mut() {
689 match update_rx.try_recv() {
690 Ok(new_target) => {
691 if new_target.advances(&self.target) {
692 return self.handle_event(Event::TargetUpdate(new_target)).await;
693 }
694 }
695 Err(ring::TryRecvError::Empty) => break,
696 Err(ring::TryRecvError::Disconnected) => {
697 self.update_rx = None;
698 }
699 }
700 }
701 }
702
703 if self.finish_rx.is_some() {
704 let event = wait_for_event(
705 &mut self.update_rx,
706 &mut self.finish_rx,
707 &mut self.outstanding_requests,
708 )
709 .await
710 .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
711 return self.handle_event(event).await;
712 }
713
714 return Ok(NextStep::Complete(self.complete().await?));
715 }
716
717 let event = wait_for_event(
719 &mut self.update_rx,
720 &mut self.finish_rx,
721 &mut self.outstanding_requests,
722 )
723 .await
724 .ok_or(SyncError::Engine(EngineError::SyncStalled))?;
725 self.handle_event(event).await
726 }
727
728 async fn complete(mut self) -> Result<DB, Error<DB, S>> {
731 self.journal = self.journal.sync().await?;
732
733 let database = DB::from_sync_result(
734 self.context,
735 self.config,
736 self.journal,
737 self.pinned_nodes,
738 self.target.range.clone(),
739 self.apply_batch_size,
740 )
741 .await?;
742
743 let got_root = database.root();
744 let expected_root = self.target.root;
745 if got_root != expected_root {
746 return Err(SyncError::Engine(EngineError::RootMismatch {
747 expected: expected_root,
748 actual: got_root,
749 }));
750 }
751
752 Ok(database.persist_sync_result().await?)
753 }
754
755 pub async fn sync(mut self) -> Result<DB, Error<DB, S>> {
760 loop {
762 match self.step().await? {
763 NextStep::Continue(new_engine) => self = new_engine,
764 NextStep::Complete(database) => return Ok(database),
765 }
766 }
767 }
768}
769
770#[cfg(test)]
771pub(crate) mod tests {
772 use super::*;
773 use crate::{
774 merkle::{Proof, mmr::Family as MmrFamily},
775 qmdb::sync::{
776 harness::{DbOf, OpOf, SyncTestHarness},
777 source::{self, tests::FailSource},
778 },
779 };
780 use commonware_cryptography::{Sha256, sha256};
781 use commonware_runtime::{Runner as _, deterministic};
782 use commonware_utils::{NZU64, NZUsize, non_empty_range};
783 use std::{
784 convert::Infallible,
785 sync::{
786 Arc,
787 atomic::{AtomicUsize, Ordering},
788 },
789 };
790
791 #[derive(Clone)]
792 struct TestConfig {
793 journal_size: u64,
794 pinned_node_probes: Arc<AtomicUsize>,
795 }
796
797 impl crate::qmdb::sync::DatabaseConfig for TestConfig {
798 type JournalConfig = u64;
799
800 fn journal_config(&self) -> Self::JournalConfig {
801 self.journal_size
802 }
803 }
804
805 struct TestJournal {
806 size: u64,
807 }
808
809 impl Journal<MmrFamily> for TestJournal {
810 type Config = u64;
811 type Context = deterministic::Context;
812 type Error = crate::journal::Error;
813 type Op = i32;
814
815 async fn new(
816 _context: Self::Context,
817 size: Self::Config,
818 _range: commonware_utils::range::NonEmptyRange<Location<MmrFamily>>,
819 ) -> Result<Self, Self::Error> {
820 Ok(Self { size })
821 }
822
823 async fn resize(mut self, start: Location<MmrFamily>) -> Result<Self, Self::Error> {
824 self.size = *start;
825 Ok(self)
826 }
827
828 async fn sync(self) -> Result<Self, Self::Error> {
829 Ok(self)
830 }
831
832 fn size(&self) -> u64 {
833 self.size
834 }
835
836 async fn append(mut self, ops: Vec<Self::Op>) -> Result<Self, Self::Error> {
837 self.size += ops.len() as u64;
838 Ok(self)
839 }
840 }
841
842 struct TestDb;
843
844 impl Database for TestDb {
845 type Config = TestConfig;
846 type Context = deterministic::Context;
847 type Digest = sha256::Digest;
848 type Family = MmrFamily;
849 type Hasher = Sha256;
850 type Journal = TestJournal;
851 type Op = i32;
852
853 async fn from_sync_result(
854 _context: Self::Context,
855 _config: Self::Config,
856 _journal: Self::Journal,
857 _pinned_nodes: Option<Vec<Self::Digest>>,
858 _range: commonware_utils::range::NonEmptyRange<Location<Self::Family>>,
859 _apply_batch_size: NonZeroU64,
860 ) -> Result<Self, qmdb::Error<Self::Family>> {
861 Ok(Self)
862 }
863
864 async fn persist_sync_result(self) -> Result<Self, qmdb::Error<Self::Family>> {
865 Ok(self)
866 }
867
868 async fn local_pinned_nodes(
869 _context: Self::Context,
870 config: &Self::Config,
871 _target: &Target<Self::Family, Self::Digest>,
872 _journal: &Self::Journal,
873 ) -> Result<Option<Vec<Self::Digest>>, qmdb::Error<Self::Family>> {
874 config.pinned_node_probes.fetch_add(1, Ordering::SeqCst);
875 Ok(Some(vec![]))
876 }
877
878 fn root(&self) -> Self::Digest {
879 sha256::Digest::from([0u8; 32])
880 }
881 }
882
883 #[derive(Clone)]
884 struct TestSource;
885
886 impl Source for TestSource {
887 type Digest = sha256::Digest;
888 type Error = Infallible;
889 type Family = MmrFamily;
890 type Op = i32;
891
892 async fn serve(&self, _request: Request<MmrFamily>) -> source::Result<Self> {
893 Ok((
894 Response::Operations {
895 proof: Proof {
896 leaves: Location::new(0),
897 inactive_peaks: 0,
898 digests: vec![],
899 },
900 operations: vec![],
901 },
902 None,
903 ))
904 }
905 }
906
907 fn test_engine_config(
908 context: deterministic::Context,
909 journal_size: u64,
910 pinned_node_probes: Arc<AtomicUsize>,
911 ) -> Config<TestDb, TestSource> {
912 Config {
913 context,
914 source: TestSource,
915 target: Target {
916 root: sha256::Digest::from([1u8; 32]),
917 range: non_empty_range!(Location::new(5), Location::new(10)),
918 },
919 max_outstanding_requests: NZUsize!(1),
920 fetch_batch_size: NZU64!(1),
921 apply_batch_size: NZU64!(1),
922 db_config: TestConfig {
923 journal_size,
924 pinned_node_probes,
925 },
926 update_rx: None,
927 finish_rx: None,
928 reached_target_tx: None,
929 }
930 }
931
932 async fn retention_engine<H: SyncTestHarness>(
933 context: deterministic::Context,
934 ) -> Engine<DbOf<H>, FailSource<H::Family, OpOf<H>, sha256::Digest>>
935 where
936 OpOf<H>: 'static,
937 {
938 let config = Config {
940 db_config: H::config("retention"),
941 context,
942 source: FailSource::new(),
943 target: Target {
944 root: sha256::Digest::from([1u8; 32]),
945 range: non_empty_range!(Location::new(5), Location::new(10)),
946 },
947 max_outstanding_requests: NZUsize!(1),
948 fetch_batch_size: NZU64!(1),
949 apply_batch_size: NZU64!(1),
950 update_rx: None,
951 finish_rx: None,
952 reached_target_tx: None,
953 };
954 Engine::new(config).await.unwrap()
955 }
956
957 fn insert_pending_request<DB, S>(
958 engine: &mut Engine<DB, S>,
959 request: Request<DB::Family>,
960 ) -> RequestId
961 where
962 DB: Database,
963 S: SourceFor<DB>,
964 DB::Op: 'static,
965 {
966 engine
967 .outstanding_requests
968 .insert(request, |_| std::future::pending())
969 }
970
971 fn late_fetch_result<F: Family, Op>(
972 id: RequestId,
973 operation: Op,
974 ) -> IndexedFetchResult<F, Op, sha256::Digest, qmdb::Error<F>> {
975 IndexedFetchResult {
976 id,
977 result: Ok(Some(Response::Operations {
978 proof: Proof {
979 leaves: Location::new(10),
980 inactive_peaks: 0,
981 digests: vec![],
982 },
983 operations: vec![operation],
984 })),
985 }
986 }
987
988 pub(crate) fn test_boundary_response_keeps_fetched_batch_at_lower_bound<H: SyncTestHarness>()
990 where
991 OpOf<H>: Clone + PartialEq + Debug + 'static,
992 {
993 deterministic::Runner::default().start(|context| async move {
994 let mut engine = retention_engine::<H>(context).await;
995 let operations = H::create_ops(3);
996 engine
997 .fetched_operations
998 .insert(Location::new(6), operations.clone());
999
1000 let next = Target {
1002 root: sha256::Digest::from([2; 32]),
1003 range: non_empty_range!(Location::new(6), Location::new(12)),
1004 };
1005 let mut engine = engine.reset_for_target_update(next).await.unwrap();
1006 assert!(engine.pinned_nodes.is_none());
1007
1008 let id = insert_pending_request(
1010 &mut engine,
1011 Request::Boundary {
1012 size: Location::new(12),
1013 start: Location::new(6),
1014 },
1015 );
1016 let pinned = vec![sha256::Digest::from([7; 32])];
1017 engine
1018 .handle_fetch_result(IndexedFetchResult {
1019 id,
1020 result: Ok(Some(Response::Boundary {
1021 proof: Proof {
1022 leaves: Location::new(12),
1023 inactive_peaks: 0,
1024 digests: vec![],
1025 },
1026 op: operations[0].clone(),
1027 pinned_nodes: pinned.clone(),
1028 })),
1029 })
1030 .unwrap();
1031 assert_eq!(engine.pinned_nodes, Some(pinned));
1032 assert_eq!(
1033 engine.fetched_operations.get(&Location::new(6)),
1034 Some(&operations)
1035 );
1036
1037 let engine = engine.apply_operations().await.unwrap();
1039 assert_eq!(engine.journal.size(), 9);
1040 });
1041 }
1042
1043 pub(crate) fn test_target_update_drops_queued_result_of_cancelled_request<H: SyncTestHarness>()
1046 where
1047 OpOf<H>: 'static,
1048 {
1049 deterministic::Runner::default().start(|context| async move {
1050 let mut engine = retention_engine::<H>(context).await;
1051
1052 assert!(engine.outstanding_requests.contains(&Location::new(5)));
1054 let request = Request::Operations {
1055 size: Location::new(10),
1056 start: Location::new(6),
1057 max_ops: NZU64!(1),
1058 };
1059 let old_id = insert_pending_request(&mut engine, request);
1060 let queued_result = late_fetch_result(old_id, H::create_ops(1).pop().unwrap());
1061 assert_eq!(engine.outstanding_requests.len(), 2);
1062
1063 let new_target = Target {
1065 root: sha256::Digest::from([2u8; 32]),
1066 range: non_empty_range!(Location::new(7), Location::new(12)),
1067 };
1068 let mut engine = engine.reset_for_target_update(new_target).await.unwrap();
1069 assert_eq!(engine.outstanding_requests.len(), 0);
1070 assert!(engine.outstanding_requests.remove(old_id).is_none());
1071
1072 engine.handle_fetch_result(queued_result).unwrap();
1074 assert!(engine.fetched_operations.is_empty());
1075 assert!(engine.pinned_nodes.is_none());
1076 assert!(matches!(
1077 engine.outstanding_requests.next_completed().await,
1078 Err(Aborted)
1079 ));
1080 assert!(matches!(
1081 engine.outstanding_requests.next_completed().await,
1082 Err(Aborted)
1083 ));
1084 assert_eq!(engine.outstanding_requests.len(), 0);
1085 });
1086 }
1087
1088 pub(crate) fn test_target_updates_keep_old_size_request_beyond_floor<H: SyncTestHarness>()
1092 where
1093 OpOf<H>: Clone + PartialEq + Debug + 'static,
1094 {
1095 deterministic::Runner::default().start(|context| async move {
1096 let mut engine = retention_engine::<H>(context).await;
1097
1098 assert!(engine.outstanding_requests.contains(&Location::new(5)));
1101 insert_pending_request(
1102 &mut engine,
1103 Request::Boundary {
1104 size: Location::new(10),
1105 start: Location::new(5),
1106 },
1107 );
1108 let request = Request::Operations {
1109 size: Location::new(10),
1110 start: Location::new(8),
1111 max_ops: NZU64!(1),
1112 };
1113 let id = insert_pending_request(&mut engine, request);
1114 assert_eq!(engine.outstanding_requests.len(), 2);
1115
1116 let mut root = 2u8;
1118 for end in 11..21 {
1119 let next = Target {
1120 root: sha256::Digest::from([root; 32]),
1121 range: non_empty_range!(Location::new(5), Location::new(end)),
1122 };
1123 root += 1;
1124 engine = engine.reset_for_target_update(next).await.unwrap();
1125 assert!(engine.outstanding_requests.contains(&Location::new(5)));
1126 assert!(engine.outstanding_requests.contains(&Location::new(8)));
1127 assert_eq!(engine.outstanding_requests.len(), 2);
1128 }
1129
1130 for (start, end) in [(6, 22), (7, 24)] {
1133 let next = Target {
1134 root: sha256::Digest::from([root; 32]),
1135 range: non_empty_range!(Location::new(start), Location::new(end)),
1136 };
1137 root += 1;
1138 engine = engine.reset_for_target_update(next).await.unwrap();
1139 assert!(engine.outstanding_requests.contains(&Location::new(8)));
1140 assert_eq!(engine.outstanding_requests.len(), 1);
1141 }
1142
1143 let operation = H::create_ops(1).pop().unwrap();
1145 engine
1146 .handle_fetch_result(late_fetch_result(id, operation.clone()))
1147 .unwrap();
1148 assert_eq!(engine.outstanding_requests.len(), 0);
1149 assert_eq!(
1150 engine.fetched_operations.get(&Location::new(8)),
1151 Some(&vec![operation])
1152 );
1153 });
1154 }
1155
1156 pub(crate) fn test_target_update_floor_move_cancels_operations_below_bound<H: SyncTestHarness>()
1159 where
1160 OpOf<H>: 'static,
1161 {
1162 deterministic::Runner::default().start(|context| async move {
1163 let mut engine = retention_engine::<H>(context).await;
1164
1165 let below = Request::Operations {
1167 size: Location::new(10),
1168 start: Location::new(6),
1169 max_ops: NZU64!(2),
1170 };
1171 insert_pending_request(&mut engine, below);
1172 let beyond = Request::Operations {
1173 size: Location::new(10),
1174 start: Location::new(9),
1175 max_ops: NZU64!(1),
1176 };
1177 insert_pending_request(&mut engine, beyond);
1178 assert_eq!(engine.outstanding_requests.len(), 3);
1179
1180 let next = Target {
1183 root: sha256::Digest::from([2; 32]),
1184 range: non_empty_range!(Location::new(8), Location::new(14)),
1185 };
1186 let engine = engine.reset_for_target_update(next).await.unwrap();
1187 assert!(!engine.outstanding_requests.contains(&Location::new(5)));
1188 assert!(!engine.outstanding_requests.contains(&Location::new(6)));
1189 assert!(engine.outstanding_requests.contains(&Location::new(9)));
1190 assert_eq!(engine.outstanding_requests.len(), 1);
1191 });
1192 }
1193
1194 pub(crate) fn test_moved_floor_schedules_boundary_without_waiting_for_old_operation<
1197 H: SyncTestHarness,
1198 >()
1199 where
1200 OpOf<H>: 'static,
1201 {
1202 deterministic::Runner::default().start(|context| async move {
1203 let mut engine = retention_engine::<H>(context).await;
1204
1205 let operation_id = insert_pending_request(
1207 &mut engine,
1208 Request::Operations {
1209 size: Location::new(10),
1210 start: Location::new(6),
1211 max_ops: NZU64!(1),
1212 },
1213 );
1214 assert!(engine.outstanding_requests.contains(&Location::new(5)));
1215
1216 let next = Target {
1218 root: sha256::Digest::from([2; 32]),
1219 range: non_empty_range!(Location::new(6), Location::new(12)),
1220 };
1221 let mut engine = engine.reset_for_target_update(next).await.unwrap();
1222 engine.schedule_requests();
1223
1224 assert!(engine.outstanding_requests.remove(operation_id).is_none());
1227 assert!(!engine.outstanding_requests.contains(&Location::new(5)));
1228 assert!(engine.outstanding_requests.contains(&Location::new(6)));
1229 assert_eq!(engine.outstanding_requests.len(), 1);
1230 });
1231 }
1232
1233 #[test]
1237 fn target_updates_keep_operations_and_reset_pins_only_when_floor_moves() {
1238 deterministic::Runner::default().start(|context| async move {
1239 let mut engine = Engine::new(test_engine_config(
1240 context,
1241 5,
1242 Arc::new(AtomicUsize::new(0)),
1243 ))
1244 .await
1245 .unwrap();
1246
1247 let fetched = BTreeMap::from([
1249 (Location::new(6), vec![1, 2]),
1250 (Location::new(8), vec![3, 4]),
1251 ]);
1252 engine.fetched_operations = fetched.clone();
1253 let pinned = vec![sha256::Digest::from([7; 32])];
1254 engine.pinned_nodes = Some(pinned.clone());
1255
1256 let next = Target {
1258 root: sha256::Digest::from([2; 32]),
1259 range: non_empty_range!(Location::new(5), Location::new(12)),
1260 };
1261 let engine = engine.reset_for_target_update(next).await.unwrap();
1262 assert_eq!(engine.fetched_operations, fetched);
1263 assert_eq!(engine.pinned_nodes, Some(pinned));
1264
1265 let next = Target {
1267 root: sha256::Digest::from([3; 32]),
1268 range: non_empty_range!(Location::new(9), Location::new(14)),
1269 };
1270 let engine = engine.reset_for_target_update(next).await.unwrap();
1271 assert_eq!(engine.fetched_operations, fetched);
1272 assert!(engine.pinned_nodes.is_none());
1273
1274 let engine = engine.apply_operations().await.unwrap();
1277 assert_eq!(engine.journal.size(), 10);
1278 assert!(engine.fetched_operations.is_empty());
1279 });
1280 }
1281
1282 #[test]
1283 fn new_probes_local_pinned_nodes_when_journal_reaches_target() {
1284 deterministic::Runner::default().start(|context| async move {
1285 let pinned_node_probes = Arc::new(AtomicUsize::new(0));
1286 Engine::new(test_engine_config(context, 10, pinned_node_probes.clone()))
1287 .await
1288 .unwrap();
1289
1290 assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 1);
1291 });
1292 }
1293
1294 #[test]
1295 fn new_skips_local_pinned_nodes_when_journal_is_partial() {
1296 deterministic::Runner::default().start(|context| async move {
1297 let pinned_node_probes = Arc::new(AtomicUsize::new(0));
1298 Engine::new(test_engine_config(context, 7, pinned_node_probes.clone()))
1299 .await
1300 .unwrap();
1301
1302 assert_eq!(pinned_node_probes.load(Ordering::SeqCst), 0);
1303 });
1304 }
1305
1306 #[test]
1307 fn new_schedules_operations_after_boundary_request() {
1308 deterministic::Runner::default().start(|context| async move {
1309 let mut config = test_engine_config(context, 5, Arc::new(AtomicUsize::new(0)));
1310 config.max_outstanding_requests = NZUsize!(2);
1311 config.fetch_batch_size = NZU64!(5);
1312
1313 let engine = Engine::new(config).await.unwrap();
1314 let requests = &engine.outstanding_requests;
1315
1316 assert_eq!(requests.len(), 2);
1317 assert!(requests.contains(&Location::new(5)));
1318 assert!(requests.contains(&Location::new(6)));
1319 });
1320 }
1321
1322 #[test]
1323 fn step_takes_queued_update_before_completing() {
1324 deterministic::Runner::default().start(|context| async move {
1325 let (update_tx, update_rx) = ring::channel(NZUsize!(2));
1326 let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1327 config.update_rx = Some(update_rx);
1328 let stale = Target {
1331 root: sha256::Digest::from([2u8; 32]),
1332 range: non_empty_range!(Location::new(5), Location::new(10)),
1333 };
1334 let advancing = Target {
1335 root: sha256::Digest::from([3u8; 32]),
1336 range: non_empty_range!(Location::new(5), Location::new(12)),
1337 };
1338 assert!(update_tx.send_lossy(stale));
1339 assert!(update_tx.send_lossy(advancing.clone()));
1340
1341 let engine = Engine::new(config).await.unwrap();
1342 let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1343 panic!("engine should retarget instead of completing");
1344 };
1345 assert_eq!(engine.target, advancing);
1346 });
1347 }
1348
1349 #[test]
1350 fn step_completes_at_current_target_after_finish() {
1351 deterministic::Runner::default().start(|context| async move {
1352 let (update_tx, update_rx) = ring::channel(NZUsize!(1));
1353 let (finish_tx, finish_rx) = mpsc::channel(1);
1354 let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1355 config.target.root = sha256::Digest::from([0u8; 32]);
1357 config.update_rx = Some(update_rx);
1358 config.finish_rx = Some(finish_rx);
1359 let advancing = Target {
1360 root: sha256::Digest::from([3u8; 32]),
1361 range: non_empty_range!(Location::new(5), Location::new(12)),
1362 };
1363 assert!(update_tx.send_lossy(advancing));
1364 finish_tx.send(()).await.unwrap();
1365
1366 let engine = Engine::new(config).await.unwrap();
1367 let NextStep::Complete(_) = engine.step().await.unwrap() else {
1368 panic!("a requested finish must win over a queued update");
1369 };
1370 });
1371 }
1372
1373 fn dummy_result(id: RequestId) -> IndexedFetchResult<MmrFamily, i32, sha256::Digest, ()> {
1375 IndexedFetchResult {
1376 id,
1377 result: Ok(Some(Response::Operations {
1378 proof: Proof {
1379 leaves: Location::new(0),
1380 inactive_peaks: 0,
1381 digests: vec![],
1382 },
1383 operations: vec![],
1384 })),
1385 }
1386 }
1387
1388 fn add(requests: &mut Requests<MmrFamily, i32, sha256::Digest, ()>, loc: u64) -> RequestId {
1390 requests.insert(
1391 Request::Operations {
1392 size: Location::new(loc),
1393 start: Location::new(loc),
1394 max_ops: NZU64!(1),
1395 },
1396 |id| std::future::ready(dummy_result(id)),
1397 )
1398 }
1399
1400 #[test]
1401 fn test_add_and_remove() {
1402 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1403 assert_eq!(requests.len(), 0);
1404
1405 let id = add(&mut requests, 10);
1406 assert_eq!(requests.len(), 1);
1407 assert!(requests.contains(&Location::new(10)));
1408
1409 assert!(requests.remove(id).is_some());
1410 assert!(!requests.contains(&Location::new(10)));
1411 assert!(requests.remove(id).is_none());
1412 }
1413
1414 #[test]
1415 fn test_retain_matching() {
1416 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1417
1418 add(&mut requests, 5);
1419 add(&mut requests, 10);
1420 add(&mut requests, 15);
1421 add(&mut requests, 20);
1422 assert_eq!(requests.len(), 4);
1423
1424 requests.retain(|request| request.start() >= Location::new(10));
1425 assert_eq!(requests.len(), 3);
1426 assert!(!requests.contains(&Location::new(5)));
1427 assert!(requests.contains(&Location::new(10)));
1428 assert!(requests.contains(&Location::new(15)));
1429 assert!(requests.contains(&Location::new(20)));
1430 }
1431
1432 #[test]
1433 fn test_retain_none() {
1434 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1435
1436 add(&mut requests, 5);
1437 add(&mut requests, 10);
1438 assert_eq!(requests.len(), 2);
1439
1440 requests.retain(|_| false);
1441 assert_eq!(requests.len(), 0);
1442 }
1443
1444 #[test]
1445 fn test_retain_empty() {
1446 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1447 requests.retain(|_| true);
1448 assert_eq!(requests.len(), 0);
1449 }
1450
1451 #[test]
1452 fn test_retain_all() {
1453 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1454
1455 add(&mut requests, 10);
1456 add(&mut requests, 20);
1457 assert_eq!(requests.len(), 2);
1458
1459 requests.retain(|_| true);
1460 assert_eq!(requests.len(), 2);
1461 assert!(requests.contains(&Location::new(10)));
1462 assert!(requests.contains(&Location::new(20)));
1463 }
1464
1465 #[test]
1466 fn test_superseded_request() {
1467 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1468
1469 let old_id = add(&mut requests, 10);
1471 assert_eq!(requests.len(), 1);
1472
1473 let new_id = add(&mut requests, 10);
1475 assert_eq!(requests.len(), 1);
1476
1477 assert!(requests.remove(old_id).is_none());
1479
1480 assert!(requests.contains(&Location::new(10)));
1482 assert!(requests.remove(new_id).is_some());
1483 assert!(!requests.contains(&Location::new(10)));
1484 }
1485
1486 #[test]
1487 fn test_stale_completion_after_retain() {
1488 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1489
1490 let old_id = add(&mut requests, 5);
1491 let queued_result = dummy_result(old_id);
1492 add(&mut requests, 15);
1493 requests.retain(|request| request.start() >= Location::new(10));
1494
1495 assert!(requests.remove(queued_result.id).is_none());
1497
1498 let new_id = add(&mut requests, 5);
1500 assert_ne!(old_id, new_id);
1501 assert!(requests.remove(new_id).is_some());
1502 }
1503
1504 #[test]
1505 fn test_retain_aborts_future() {
1506 deterministic::Runner::default().start(|_context| async move {
1507 let mut requests: Requests<MmrFamily, i32, sha256::Digest, ()> = Requests::new();
1508 requests.insert(
1509 Request::Operations {
1510 size: Location::new(5),
1511 start: Location::new(5),
1512 max_ops: NZU64!(1),
1513 },
1514 |_| std::future::pending(),
1515 );
1516 requests.retain(|_| false);
1517 assert!(matches!(requests.next_completed().await, Err(Aborted)));
1518 });
1519 }
1520
1521 #[test]
1522 fn reports_reached_target_before_queued_update() {
1523 deterministic::Runner::default().start(|context| async move {
1524 let (update_tx, update_rx) = ring::channel(NZUsize!(1));
1525 let (_finish_tx, finish_rx) = mpsc::channel(1);
1526 let (reached_tx, mut reached_rx) = ring::channel(NZUsize!(1));
1527 let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1528 config.update_rx = Some(update_rx);
1529 config.finish_rx = Some(finish_rx);
1530 config.reached_target_tx = Some(reached_tx);
1531 let original = config.target.clone();
1532
1533 let next = Target {
1535 root: sha256::Digest::from([2; 32]),
1536 range: non_empty_range!(Location::new(5), Location::new(12)),
1537 };
1538 assert!(update_tx.send_lossy(next.clone()));
1539
1540 let engine = Engine::new(config).await.unwrap();
1543 let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1544 panic!("the coordinator has not requested completion");
1545 };
1546 assert_eq!(
1547 reached_rx.try_recv().ok(),
1548 Some(original),
1549 "a queued update hid a reached target from the coordinator"
1550 );
1551 assert_eq!(engine.target, next);
1552 });
1553 }
1554
1555 #[test]
1556 fn reports_never_wait_for_the_observer() {
1557 deterministic::Runner::default().start(|context| async move {
1558 let (finish_tx, finish_rx) = mpsc::channel(1);
1559 let (reached_tx, mut reached_rx) = ring::channel(NZUsize!(1));
1560 let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1561 config.finish_rx = Some(finish_rx);
1562
1563 config.target.root = sha256::Digest::from([0; 32]);
1565 let original = config.target.clone();
1566
1567 let stale = Target {
1569 root: sha256::Digest::from([9; 32]),
1570 range: non_empty_range!(Location::new(5), Location::new(8)),
1571 };
1572 assert!(reached_tx.send_lossy(stale));
1573 config.reached_target_tx = Some(reached_tx);
1574
1575 finish_tx.send(()).await.unwrap();
1578 let engine = Engine::new(config).await.unwrap();
1579 let NextStep::Complete(_) = engine.step().await.unwrap() else {
1580 panic!("a requested finish completes at the reached target");
1581 };
1582 assert_eq!(reached_rx.try_recv(), Ok(original));
1583 });
1584 }
1585
1586 #[test]
1587 fn step_adopts_newest_target_after_overflow() {
1588 deterministic::Runner::default().start(|context| async move {
1589 let (update_tx, update_rx) = ring::channel(NZUsize!(1));
1590 let mut config = test_engine_config(context, 10, Arc::new(AtomicUsize::new(0)));
1591 config.update_rx = Some(update_rx);
1592
1593 let targets = [12u64, 14, 16].map(|end| Target {
1595 root: sha256::Digest::from([end as u8; 32]),
1596 range: non_empty_range!(Location::new(5), Location::new(end)),
1597 });
1598 for target in targets.iter().cloned() {
1599 assert!(update_tx.send_lossy(target));
1600 }
1601
1602 let engine = Engine::new(config).await.unwrap();
1604 let NextStep::Continue(engine) = engine.step().await.unwrap() else {
1605 panic!("engine should retarget instead of completing");
1606 };
1607 assert_eq!(engine.target, targets[2]);
1608 });
1609 }
1610}