1use std::{
2 collections::BTreeMap,
3 num::NonZeroUsize,
4 ops::Deref,
5 panic::{AssertUnwindSafe, catch_unwind},
6 time::{Duration, Instant},
7};
8
9use super::{
10 CachedPocketIcBaseline, ControllerSnapshotError, SnapshotRestoreFunding,
11 bounded_pool::{BoundedSlotLease, BoundedSlotPool},
12};
13
14#[non_exhaustive]
16#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum ResetDomainKind {
18 PocketIcTime,
20 ExtraCanisters,
22 PendingMessages,
24 SubnetState,
26 ExternalResources,
28}
29
30#[non_exhaustive]
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum TimeResetPolicy {
34 PreserveCurrent,
36 RestoreBaseline,
38 RebuildOnMutation,
40}
41
42#[non_exhaustive]
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum ExtraCanisterPolicy {
46 RequireBaselineSet,
48 RemoveTracked,
50 RebuildOnChange,
52}
53
54#[non_exhaustive]
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum StateResetPolicy {
58 ResetByRecipe,
60 ValidateUnchanged,
62 IrrelevantByRecipeContract,
64 RebuildOnChange,
66}
67
68#[non_exhaustive]
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub enum ResetDomainPolicy {
97 PocketIcTime(TimeResetPolicy),
99 ExtraCanisters(ExtraCanisterPolicy),
101 PendingMessages(StateResetPolicy),
103 SubnetState(StateResetPolicy),
105 ExternalResources(StateResetPolicy),
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct ResetRequirements {
112 funding: SnapshotRestoreFunding,
113 domains: BTreeMap<ResetDomainKind, ResetDomainPolicy>,
114}
115
116#[derive(Clone, Debug, Default, Eq, PartialEq)]
118pub struct ResetReceipt(BTreeMap<ResetDomainKind, ResetDomainPolicy>);
119
120#[non_exhaustive]
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub enum BaselinePoolContractError {
124 DuplicateResetDomain { domain: ResetDomainKind },
126 MissingResetDomain { domain: ResetDomainKind },
128 ResetPolicyMismatch {
130 requirement: ResetDomainPolicy,
131 achievement: ResetDomainPolicy,
132 },
133}
134
135#[non_exhaustive]
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub enum PreparedBaseline {
139 Built,
141 Restored {
143 reset: ResetReceipt,
145 },
146}
147
148#[non_exhaustive]
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
151pub enum BaselinePreparationStage {
152 Build,
154 RestoreCanisters,
156 ResetNonSnapshotState,
158 DriveToReadiness,
160 ValidateBuilt,
162 ValidateRestored,
164}
165
166#[non_exhaustive]
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub enum RebuildReason {
170 DeadPocketIcTransport,
172 SnapshotRestoreFailure,
174 ResetFailure,
176 ReadinessFailure,
178 ResetCoverageMismatch,
180 InvariantValidationFailure,
182 ExplicitLeaseInvalidation,
184 UnwindWhileLeased,
186 RecipeClassified { code: String },
188}
189
190#[non_exhaustive]
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub enum FailureDisposition {
194 Fatal,
196 Rebuild(RebuildReason),
198}
199
200#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
202pub struct BaselinePoolTimings {
203 wait: Duration,
204 build: Option<Duration>,
205 restore: Option<Duration>,
206 reset: Option<Duration>,
207 readiness: Option<Duration>,
208 validation: Option<Duration>,
209 stale_teardown: Option<Duration>,
210 total: Duration,
211}
212
213#[derive(Clone, Debug, Eq, PartialEq)]
215pub struct BaselinePoolOutcome {
216 pub slot: usize,
218 pub timings: BaselinePoolTimings,
220 pub disposition: BaselinePoolDisposition,
222}
223
224#[non_exhaustive]
226#[derive(Clone, Debug, Eq, PartialEq)]
227pub enum BaselinePoolDisposition {
228 Built,
230 Restored,
232 Rebuilt(RebuildReason),
234}
235
236#[non_exhaustive]
238#[derive(Debug)]
239pub enum BaselinePoolPreparationError<E> {
240 Recipe {
242 stage: BaselinePreparationStage,
244 source: E,
246 },
247 Restore(ControllerSnapshotError),
249 Contract(BaselinePoolContractError),
251}
252
253#[non_exhaustive]
255#[derive(Debug)]
256pub enum BaselinePoolError<E> {
257 Preparation {
259 error: BaselinePoolPreparationError<E>,
261 timings: Box<BaselinePoolTimings>,
263 },
264 RecoveryFailed {
266 original: Box<BaselinePoolPreparationError<E>>,
268 rebuild: Box<BaselinePoolPreparationError<E>>,
270 timings: Box<BaselinePoolTimings>,
272 },
273}
274
275pub trait PocketIcBaselineRecipe: Send + Sync + 'static {
277 type Metadata: Send + 'static;
279 type Error: std::error::Error + Send + Sync + 'static;
281
282 fn reset_requirements(&self) -> &ResetRequirements;
284
285 fn build(&self) -> Result<CachedPocketIcBaseline<Self::Metadata>, Self::Error>;
287
288 fn prepare_restore(
294 &self,
295 _baseline: &CachedPocketIcBaseline<Self::Metadata>,
296 ) -> Result<(), Self::Error> {
297 Ok(())
298 }
299
300 fn reset_non_snapshot_state(
302 &self,
303 baseline: &CachedPocketIcBaseline<Self::Metadata>,
304 ) -> Result<ResetReceipt, Self::Error>;
305
306 fn drive_to_readiness(
309 &self,
310 baseline: &CachedPocketIcBaseline<Self::Metadata>,
311 ) -> Result<(), Self::Error>;
312
313 fn validate(
316 &self,
317 baseline: &CachedPocketIcBaseline<Self::Metadata>,
318 preparation: &PreparedBaseline,
319 ) -> Result<(), Self::Error>;
320
321 fn classify_failure(
323 &self,
324 stage: BaselinePreparationStage,
325 _error: &(dyn std::error::Error + 'static),
326 ) -> FailureDisposition {
327 FailureDisposition::Rebuild(stage.default_rebuild_reason())
328 }
329}
330
331pub struct CachedPocketIcBaselinePool<R>
343where
344 R: PocketIcBaselineRecipe,
345{
346 recipe: R,
347 slots: BoundedSlotPool<CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
348}
349
350pub struct CachedPocketIcBaselinePoolGuard<'a, R>
352where
353 R: PocketIcBaselineRecipe,
354{
355 slot: BoundedSlotLease<'a, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
356}
357
358impl ResetDomainPolicy {
359 #[must_use]
361 pub const fn domain(&self) -> ResetDomainKind {
362 match self {
363 Self::PocketIcTime(_) => ResetDomainKind::PocketIcTime,
364 Self::ExtraCanisters(_) => ResetDomainKind::ExtraCanisters,
365 Self::PendingMessages(_) => ResetDomainKind::PendingMessages,
366 Self::SubnetState(_) => ResetDomainKind::SubnetState,
367 Self::ExternalResources(_) => ResetDomainKind::ExternalResources,
368 }
369 }
370}
371
372fn collect_reset_domains(
373 policies: impl IntoIterator<Item = ResetDomainPolicy>,
374) -> Result<BTreeMap<ResetDomainKind, ResetDomainPolicy>, BaselinePoolContractError> {
375 let mut domains = BTreeMap::new();
376 for policy in policies {
377 let domain = policy.domain();
378 if domains.insert(domain, policy).is_some() {
379 return Err(BaselinePoolContractError::DuplicateResetDomain { domain });
380 }
381 }
382 Ok(domains)
383}
384
385impl ResetRequirements {
386 pub fn try_new<I>(
391 funding: SnapshotRestoreFunding,
392 requirements: I,
393 ) -> Result<Self, BaselinePoolContractError>
394 where
395 I: IntoIterator<Item = ResetDomainPolicy>,
396 {
397 Ok(Self {
398 funding,
399 domains: collect_reset_domains(requirements)?,
400 })
401 }
402
403 #[must_use]
405 pub const fn funding(&self) -> SnapshotRestoreFunding {
406 self.funding
407 }
408
409 #[must_use]
411 pub fn get(&self, domain: ResetDomainKind) -> Option<&ResetDomainPolicy> {
412 self.domains.get(&domain)
413 }
414
415 pub fn iter(&self) -> impl Iterator<Item = &ResetDomainPolicy> {
417 self.domains.values()
418 }
419
420 fn verify(&self, receipt: &ResetReceipt) -> Result<(), BaselinePoolContractError> {
421 for (domain, requirement) in &self.domains {
422 let Some(achievement) = receipt.0.get(domain) else {
423 return Err(BaselinePoolContractError::MissingResetDomain { domain: *domain });
424 };
425 if achievement != requirement {
426 return Err(BaselinePoolContractError::ResetPolicyMismatch {
427 requirement: requirement.clone(),
428 achievement: achievement.clone(),
429 });
430 }
431 }
432 Ok(())
433 }
434}
435
436impl ResetReceipt {
437 pub fn try_new<I>(achievements: I) -> Result<Self, BaselinePoolContractError>
441 where
442 I: IntoIterator<Item = ResetDomainPolicy>,
443 {
444 Ok(Self(collect_reset_domains(achievements)?))
445 }
446
447 #[must_use]
449 pub const fn empty() -> Self {
450 Self(BTreeMap::new())
451 }
452
453 #[must_use]
455 pub fn get(&self, domain: ResetDomainKind) -> Option<&ResetDomainPolicy> {
456 self.0.get(&domain)
457 }
458
459 pub fn iter(&self) -> impl Iterator<Item = &ResetDomainPolicy> {
461 self.0.values()
462 }
463}
464
465impl BaselinePreparationStage {
466 #[must_use]
471 pub fn default_rebuild_reason(self) -> RebuildReason {
472 match self {
473 Self::RestoreCanisters => RebuildReason::SnapshotRestoreFailure,
474 Self::ResetNonSnapshotState => RebuildReason::ResetFailure,
475 Self::DriveToReadiness => RebuildReason::ReadinessFailure,
476 Self::ValidateRestored | Self::ValidateBuilt => {
477 RebuildReason::InvariantValidationFailure
478 }
479 Self::Build => RebuildReason::RecipeClassified {
480 code: "build".to_owned(),
481 },
482 }
483 }
484}
485
486impl BaselinePoolTimings {
487 #[must_use]
489 pub const fn wait(self) -> Duration {
490 self.wait
491 }
492
493 #[must_use]
495 pub const fn build(self) -> Option<Duration> {
496 self.build
497 }
498
499 #[must_use]
501 pub const fn restore(self) -> Option<Duration> {
502 self.restore
503 }
504
505 #[must_use]
507 pub const fn reset(self) -> Option<Duration> {
508 self.reset
509 }
510
511 #[must_use]
513 pub const fn readiness(self) -> Option<Duration> {
514 self.readiness
515 }
516
517 #[must_use]
519 pub const fn validation(self) -> Option<Duration> {
520 self.validation
521 }
522
523 #[must_use]
525 pub const fn stale_teardown(self) -> Option<Duration> {
526 self.stale_teardown
527 }
528
529 #[must_use]
531 pub const fn total(self) -> Duration {
532 self.total
533 }
534}
535
536impl std::fmt::Display for BaselinePoolTimings {
537 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538 write!(
539 formatter,
540 "total={:?} wait={:?} build={:?} restore={:?} reset={:?} readiness={:?} validation={:?} stale_teardown={:?}",
541 self.total,
542 self.wait,
543 self.build,
544 self.restore,
545 self.reset,
546 self.readiness,
547 self.validation,
548 self.stale_teardown,
549 )
550 }
551}
552
553impl std::fmt::Display for BaselinePoolOutcome {
554 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 match &self.disposition {
556 BaselinePoolDisposition::Built => formatter.write_str("built")?,
557 BaselinePoolDisposition::Restored => formatter.write_str("restored")?,
558 BaselinePoolDisposition::Rebuilt(reason) => {
559 write!(
560 formatter,
561 "rebuilt slot={} reason={reason:?} {}",
562 self.slot, self.timings
563 )?;
564 return Ok(());
565 }
566 }
567 write!(formatter, " slot={} {}", self.slot, self.timings)
568 }
569}
570
571impl<E> BaselinePoolError<E> {
572 #[must_use]
574 pub const fn timings(&self) -> BaselinePoolTimings {
575 match self {
576 Self::Preparation { timings, .. } | Self::RecoveryFailed { timings, .. } => **timings,
577 }
578 }
579}
580
581impl<R> CachedPocketIcBaselinePool<R>
582where
583 R: PocketIcBaselineRecipe,
584{
585 #[must_use]
590 pub const fn new(capacity: NonZeroUsize, recipe: R) -> Self {
591 Self {
592 recipe,
593 slots: BoundedSlotPool::new(capacity),
594 }
595 }
596
597 #[must_use]
599 pub const fn capacity(&self) -> NonZeroUsize {
600 self.slots.capacity()
601 }
602
603 pub fn acquire(
615 &self,
616 ) -> Result<
617 (CachedPocketIcBaselinePoolGuard<'_, R>, BaselinePoolOutcome),
618 BaselinePoolError<R::Error>,
619 > {
620 let total_started = Instant::now();
621 let mut slot = self.slots.acquire();
622 let mut timings = BaselinePoolTimings {
623 wait: slot.wait(),
624 ..BaselinePoolTimings::default()
625 };
626 let slot_index = slot.slot_index();
627
628 let (rebuild_reason, original) = if slot.is_reusable() {
629 match self.prepare_reused(&slot, &mut timings) {
630 Ok(()) => {
631 timings.total = total_started.elapsed();
632 return Ok((
633 CachedPocketIcBaselinePoolGuard { slot },
634 BaselinePoolOutcome {
635 slot: slot_index,
636 timings,
637 disposition: BaselinePoolDisposition::Restored,
638 },
639 ));
640 }
641 Err(original) => match self.failure_disposition(&original) {
642 FailureDisposition::Fatal => {
643 Self::discard_stale_slot(&mut slot, &mut timings);
646 timings.total = total_started.elapsed();
647 return Err(BaselinePoolError::Preparation {
648 error: original,
649 timings: Box::new(timings),
650 });
651 }
652 FailureDisposition::Rebuild(reason) => (Some(reason), Some(original)),
653 },
654 }
655 } else {
656 let reason = if slot.invalidated_by_unwind() {
657 Some(RebuildReason::UnwindWhileLeased)
658 } else {
659 slot.invalidation_reason().cloned()
660 };
661 (reason, None)
662 };
663
664 if slot.is_populated() {
665 Self::discard_stale_slot(&mut slot, &mut timings);
666 }
667 let build = self.build_slot(&mut slot, &mut timings);
668 timings.total = total_started.elapsed();
669 if let Err(error) = build {
670 return Err(match original {
671 Some(original) => BaselinePoolError::RecoveryFailed {
672 original: Box::new(original),
673 rebuild: Box::new(error),
674 timings: Box::new(timings),
675 },
676 None => BaselinePoolError::Preparation {
677 error,
678 timings: Box::new(timings),
679 },
680 });
681 }
682
683 let outcome = BaselinePoolOutcome {
684 slot: slot_index,
685 timings,
686 disposition: rebuild_reason.map_or(
687 BaselinePoolDisposition::Built,
688 BaselinePoolDisposition::Rebuilt,
689 ),
690 };
691 Ok((CachedPocketIcBaselinePoolGuard { slot }, outcome))
692 }
693
694 fn prepare_reused(
695 &self,
696 slot: &BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
697 timings: &mut BaselinePoolTimings,
698 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
699 let baseline = slot
700 .get()
701 .expect("reusable baseline slot must be populated");
702
703 let started = Instant::now();
704 let restore = self
705 .recipe
706 .prepare_restore(baseline)
707 .map_err(|source| BaselinePoolPreparationError::Recipe {
708 stage: BaselinePreparationStage::RestoreCanisters,
709 source,
710 })
711 .and_then(|()| {
712 baseline
713 .restore(self.recipe.reset_requirements().funding())
714 .map_err(BaselinePoolPreparationError::Restore)
715 });
716 add_timing(&mut timings.restore, started.elapsed());
717 restore?;
718
719 let started = Instant::now();
720 let reset_result = self.recipe.reset_non_snapshot_state(baseline);
721 add_timing(&mut timings.reset, started.elapsed());
722 let reset = reset_result.map_err(|source| BaselinePoolPreparationError::Recipe {
723 stage: BaselinePreparationStage::ResetNonSnapshotState,
724 source,
725 })?;
726
727 let started = Instant::now();
728 let readiness_result = self.recipe.drive_to_readiness(baseline);
729 add_timing(&mut timings.readiness, started.elapsed());
730 readiness_result.map_err(|source| BaselinePoolPreparationError::Recipe {
731 stage: BaselinePreparationStage::DriveToReadiness,
732 source,
733 })?;
734 self.recipe
735 .reset_requirements()
736 .verify(&reset)
737 .map_err(BaselinePoolPreparationError::Contract)?;
738
739 let preparation = PreparedBaseline::Restored { reset };
740 self.validate_baseline(
741 baseline,
742 &preparation,
743 BaselinePreparationStage::ValidateRestored,
744 timings,
745 )?;
746 Ok(())
747 }
748
749 fn build_slot(
750 &self,
751 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
752 timings: &mut BaselinePoolTimings,
753 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
754 let started = Instant::now();
755 let build = self.recipe.build();
756 add_timing(&mut timings.build, started.elapsed());
757 let baseline = build.map_err(|source| BaselinePoolPreparationError::Recipe {
758 stage: BaselinePreparationStage::Build,
759 source,
760 })?;
761
762 if let Err(error) = self.validate_baseline(
763 &baseline,
764 &PreparedBaseline::Built,
765 BaselinePreparationStage::ValidateBuilt,
766 timings,
767 ) {
768 drop_baseline_safely(baseline);
769 return Err(error);
770 }
771 let replaced = slot.replace(baseline);
772 debug_assert!(replaced.is_none());
773 Ok(())
774 }
775
776 fn validate_baseline(
777 &self,
778 baseline: &CachedPocketIcBaseline<R::Metadata>,
779 preparation: &PreparedBaseline,
780 stage: BaselinePreparationStage,
781 timings: &mut BaselinePoolTimings,
782 ) -> Result<(), BaselinePoolPreparationError<R::Error>> {
783 let started = Instant::now();
784 let validation = self.recipe.validate(baseline, preparation);
785 add_timing(&mut timings.validation, started.elapsed());
786 validation.map_err(|source| BaselinePoolPreparationError::Recipe { stage, source })
787 }
788
789 fn failure_disposition(
790 &self,
791 error: &BaselinePoolPreparationError<R::Error>,
792 ) -> FailureDisposition {
793 match error {
794 BaselinePoolPreparationError::Recipe { stage, source } => {
795 self.recipe.classify_failure(*stage, source)
796 }
797 BaselinePoolPreparationError::Restore(source) => self
798 .recipe
799 .classify_failure(BaselinePreparationStage::RestoreCanisters, source),
800 BaselinePoolPreparationError::Contract(_) => {
801 FailureDisposition::Rebuild(RebuildReason::ResetCoverageMismatch)
802 }
803 }
804 }
805
806 fn discard_stale_slot(
807 slot: &mut BoundedSlotLease<'_, CachedPocketIcBaseline<R::Metadata>, RebuildReason>,
808 timings: &mut BaselinePoolTimings,
809 ) {
810 let started = Instant::now();
811 if let Some(stale) = slot.take() {
812 drop_baseline_safely(stale);
813 }
814 timings.stale_teardown = Some(started.elapsed());
815 }
816}
817
818impl<R> CachedPocketIcBaselinePoolGuard<'_, R>
819where
820 R: PocketIcBaselineRecipe,
821{
822 #[must_use]
824 pub const fn slot(&self) -> usize {
825 self.slot.slot_index()
826 }
827
828 pub fn invalidate(&mut self, reason: RebuildReason) {
830 self.slot.invalidate(reason);
831 }
832}
833
834impl<R> Deref for CachedPocketIcBaselinePoolGuard<'_, R>
835where
836 R: PocketIcBaselineRecipe,
837{
838 type Target = CachedPocketIcBaseline<R::Metadata>;
839
840 fn deref(&self) -> &Self::Target {
841 self.slot
842 .get()
843 .expect("leased baseline pool slot must be populated")
844 }
845}
846
847const fn add_timing(total: &mut Option<Duration>, elapsed: Duration) {
848 *total = Some(match *total {
849 None => elapsed,
850 Some(previous) => previous.saturating_add(elapsed),
851 });
852}
853
854fn drop_baseline_safely<M>(baseline: CachedPocketIcBaseline<M>) {
855 let _ = catch_unwind(AssertUnwindSafe(|| drop(baseline)));
856}
857
858impl std::fmt::Display for BaselinePreparationStage {
859 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860 formatter.write_str(match self {
861 Self::Build => "build",
862 Self::RestoreCanisters => "canister restore",
863 Self::ResetNonSnapshotState => "non-snapshot reset",
864 Self::DriveToReadiness => "readiness",
865 Self::ValidateBuilt => "built-baseline validation",
866 Self::ValidateRestored => "restored-baseline validation",
867 })
868 }
869}
870
871impl std::fmt::Display for BaselinePoolContractError {
872 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
873 match self {
874 Self::DuplicateResetDomain { domain } => {
875 write!(
876 formatter,
877 "reset domain {domain:?} was reported more than once"
878 )
879 }
880 Self::MissingResetDomain { domain } => {
881 write!(
882 formatter,
883 "required reset domain {domain:?} was not achieved"
884 )
885 }
886 Self::ResetPolicyMismatch {
887 requirement,
888 achievement,
889 } => write!(
890 formatter,
891 "reset achievement {achievement:?} does not satisfy {requirement:?}",
892 ),
893 }
894 }
895}
896
897impl std::error::Error for BaselinePoolContractError {}
898
899impl<E> std::fmt::Display for BaselinePoolPreparationError<E>
900where
901 E: std::fmt::Display,
902{
903 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
904 match self {
905 Self::Recipe { stage, source } => {
906 write!(formatter, "baseline {stage} failed: {source}")
907 }
908 Self::Restore(source) => {
909 write!(formatter, "baseline canister restore failed: {source}")
910 }
911 Self::Contract(error) => write!(formatter, "baseline pool contract failed: {error}"),
912 }
913 }
914}
915
916impl<E> std::error::Error for BaselinePoolPreparationError<E>
917where
918 E: std::error::Error + 'static,
919{
920 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
921 match self {
922 Self::Recipe { source, .. } => Some(source),
923 Self::Restore(source) => Some(source),
924 Self::Contract(error) => Some(error),
925 }
926 }
927}
928
929impl<E> std::fmt::Display for BaselinePoolError<E>
930where
931 E: std::fmt::Display,
932{
933 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
934 match self {
935 Self::Preparation { error, .. } => error.fmt(formatter),
936 Self::RecoveryFailed {
937 original, rebuild, ..
938 } => write!(
939 formatter,
940 "baseline preparation failed ({original}); rebuilding the slot also failed: {rebuild}",
941 ),
942 }
943 }
944}
945
946impl<E> std::error::Error for BaselinePoolError<E>
947where
948 E: std::error::Error + 'static,
949{
950 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
951 match self {
952 Self::Preparation { error, .. } => Some(error),
953 Self::RecoveryFailed { original, .. } => Some(original.as_ref()),
954 }
955 }
956}
957
958#[cfg(test)]
959mod tests {
960 use super::{
961 BaselinePoolContractError, BaselinePoolTimings, ExtraCanisterPolicy, ResetDomainKind,
962 ResetDomainPolicy, ResetReceipt, ResetRequirements, SnapshotRestoreFunding,
963 StateResetPolicy, TimeResetPolicy, add_timing,
964 };
965 use std::time::Duration;
966
967 #[test]
968 fn phase_timing_preserves_absence_zero_and_saturation() {
969 let mut timings = BaselinePoolTimings::default();
970 assert_eq!(timings.restore(), None);
971 add_timing(&mut timings.restore, Duration::ZERO);
972 assert_eq!(timings.restore(), Some(Duration::ZERO));
973 add_timing(&mut timings.restore, Duration::from_secs(2));
974 assert_eq!(timings.restore(), Some(Duration::from_secs(2)));
975 add_timing(&mut timings.restore, Duration::MAX);
976 assert_eq!(timings.restore(), Some(Duration::MAX));
977 assert_eq!(timings.build(), None);
978 }
979
980 #[test]
981 fn reset_declarations_and_receipts_reject_duplicate_domains() {
982 let policies = [
983 ResetDomainPolicy::PocketIcTime(TimeResetPolicy::PreserveCurrent),
984 ResetDomainPolicy::PocketIcTime(TimeResetPolicy::RebuildOnMutation),
985 ];
986 let error = BaselinePoolContractError::DuplicateResetDomain {
987 domain: ResetDomainKind::PocketIcTime,
988 };
989 assert_eq!(
990 ResetRequirements::try_new(SnapshotRestoreFunding::Preserve, policies.clone()),
991 Err(error.clone()),
992 );
993 assert_eq!(ResetReceipt::try_new(policies), Err(error));
994 }
995
996 #[test]
997 fn required_policy_must_match_achieved_policy() {
998 for (requirement, achievement) in [
999 (
1000 ResetDomainPolicy::PocketIcTime(TimeResetPolicy::PreserveCurrent),
1001 ResetDomainPolicy::PocketIcTime(TimeResetPolicy::RebuildOnMutation),
1002 ),
1003 (
1004 ResetDomainPolicy::ExtraCanisters(ExtraCanisterPolicy::RequireBaselineSet),
1005 ResetDomainPolicy::ExtraCanisters(ExtraCanisterPolicy::RemoveTracked),
1006 ),
1007 (
1008 ResetDomainPolicy::PendingMessages(StateResetPolicy::ValidateUnchanged),
1009 ResetDomainPolicy::PendingMessages(StateResetPolicy::IrrelevantByRecipeContract),
1010 ),
1011 (
1012 ResetDomainPolicy::SubnetState(StateResetPolicy::ResetByRecipe),
1013 ResetDomainPolicy::SubnetState(StateResetPolicy::RebuildOnChange),
1014 ),
1015 (
1016 ResetDomainPolicy::ExternalResources(StateResetPolicy::ValidateUnchanged),
1017 ResetDomainPolicy::ExternalResources(StateResetPolicy::ResetByRecipe),
1018 ),
1019 ] {
1020 let requirements =
1021 ResetRequirements::try_new(SnapshotRestoreFunding::Preserve, [requirement.clone()])
1022 .unwrap();
1023 let receipt = ResetReceipt::try_new([requirement.clone()]).unwrap();
1024 assert_eq!(requirements.verify(&receipt), Ok(()));
1025 assert_eq!(
1026 requirements.verify(&ResetReceipt::empty()),
1027 Err(BaselinePoolContractError::MissingResetDomain {
1028 domain: requirement.domain(),
1029 }),
1030 );
1031 let receipt = ResetReceipt::try_new([achievement.clone()]).unwrap();
1032 assert_eq!(
1033 requirements.verify(&receipt),
1034 Err(BaselinePoolContractError::ResetPolicyMismatch {
1035 requirement,
1036 achievement,
1037 }),
1038 );
1039 }
1040 }
1041}