use candid::Principal;
use std::{
collections::{BTreeMap, BTreeSet},
num::NonZeroUsize,
ops::Deref,
panic::{AssertUnwindSafe, catch_unwind},
time::{Duration, Instant},
};
use super::{
CachedPocketIcBaseline,
bounded_pool::{BoundedSlotLease, BoundedSlotPool},
};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct FixtureRecipeId(String);
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ResetDomainKind {
CanisterSnapshots,
CanisterCycles,
PocketIcTime,
ExtraCanisters,
PendingMessages,
SubnetState,
ExternalResources,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CycleResetPolicy {
PreserveCurrent,
TopUpTo(u128),
RestoreExactBaseline,
RebuildOnMutation,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimeResetPolicy {
PreserveCurrent,
RestoreBaseline,
RebuildOnMutation,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExtraCanisterPolicy {
RequireBaselineSet,
RemoveTracked,
RebuildOnChange,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StateResetPolicy {
ResetByRecipe,
ValidateUnchanged,
IrrelevantByRecipeContract,
RebuildOnChange,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ResetRequirement {
CanisterSnapshots,
CanisterCycles(CycleResetPolicy),
PocketIcTime(TimeResetPolicy),
ExtraCanisters(ExtraCanisterPolicy),
PendingMessages(StateResetPolicy),
SubnetState(StateResetPolicy),
ExternalResources(StateResetPolicy),
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ResetAchievement {
CanisterSnapshots,
CanisterCycles(CycleResetPolicy),
PocketIcTime(TimeResetPolicy),
ExtraCanisters(ExtraCanisterPolicy),
PendingMessages(StateResetPolicy),
SubnetState(StateResetPolicy),
ExternalResources(StateResetPolicy),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResetRequirements(BTreeMap<ResetDomainKind, ResetRequirement>);
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ResetReceipt(BTreeMap<ResetDomainKind, ResetAchievement>);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CanisterRestoreReceipt {
canister_ids: Vec<Principal>,
cycle_policy: CycleResetPolicy,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReadinessReceipt {
identity: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ValidationReceipt {
recipe_id: FixtureRecipeId,
invariant_identity: String,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BaselinePoolContractError {
EmptyRecipeIdentity,
EmptyReceiptIdentity { receipt: &'static str },
DuplicateResetDomain { domain: ResetDomainKind },
DuplicateCanisterId { canister_id: Principal },
EmptyCanisterSet,
UndeclaredRequiredResetDomain { domain: ResetDomainKind },
RestoreCanisterSetMismatch {
expected: Vec<Principal>,
actual: Vec<Principal>,
},
MissingResetDomain { domain: ResetDomainKind },
ResetPolicyMismatch {
requirement: ResetRequirement,
achievement: ResetAchievement,
},
RecipeIdentityMismatch {
expected: FixtureRecipeId,
actual: FixtureRecipeId,
},
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PreparedBaseline {
Built,
Restored {
canisters: CanisterRestoreReceipt,
reset: ResetReceipt,
readiness: ReadinessReceipt,
},
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BaselinePreparationStage {
Build,
RestoreCanisters,
ResetNonSnapshotState,
DriveToReadiness,
ValidateBuilt,
ValidateRestored,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RebuildReason {
DeadPocketIcTransport,
SnapshotRestoreFailure,
ResetFailure,
ReadinessFailure,
ResetCoverageMismatch,
InvariantValidationFailure,
ExplicitLeaseInvalidation,
UnwindWhileLeased,
RecipeClassified { code: String },
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FailureDisposition {
Fatal,
Rebuild(RebuildReason),
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct BaselinePoolTimings {
wait: Duration,
build: Option<Duration>,
restore: Option<Duration>,
reset: Option<Duration>,
readiness: Option<Duration>,
validation: Option<Duration>,
stale_teardown: Option<Duration>,
total: Duration,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BaselinePoolOutcome {
Built {
slot: usize,
timings: BaselinePoolTimings,
},
Restored {
slot: usize,
timings: BaselinePoolTimings,
},
Rebuilt {
slot: usize,
reason: RebuildReason,
timings: BaselinePoolTimings,
},
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BaselinePoolPreparationError<E> {
Recipe {
stage: BaselinePreparationStage,
source: E,
},
Contract(BaselinePoolContractError),
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BaselinePoolError<E> {
Preparation {
error: BaselinePoolPreparationError<E>,
timings: Box<BaselinePoolTimings>,
},
RecoveryFailed {
original: Box<BaselinePoolPreparationError<E>>,
rebuild: Box<BaselinePoolPreparationError<E>>,
timings: Box<BaselinePoolTimings>,
},
}
pub trait PocketIcBaselineRecipe: Send + Sync + 'static {
type Metadata: Send + 'static;
type Error: std::error::Error + Send + Sync + 'static;
fn id(&self) -> &FixtureRecipeId;
fn reset_requirements(&self) -> &ResetRequirements;
fn build(&self) -> Result<CachedPocketIcBaseline<Self::Metadata>, Self::Error>;
fn restore_canisters(
&self,
baseline: &CachedPocketIcBaseline<Self::Metadata>,
) -> Result<CanisterRestoreReceipt, Self::Error>;
fn reset_non_snapshot_state(
&self,
baseline: &CachedPocketIcBaseline<Self::Metadata>,
) -> Result<ResetReceipt, Self::Error>;
fn drive_to_readiness(
&self,
baseline: &CachedPocketIcBaseline<Self::Metadata>,
) -> Result<ReadinessReceipt, Self::Error>;
fn validate(
&self,
baseline: &CachedPocketIcBaseline<Self::Metadata>,
preparation: &PreparedBaseline,
) -> Result<ValidationReceipt, Self::Error>;
fn classify_failure(
&self,
stage: BaselinePreparationStage,
_error: &Self::Error,
) -> FailureDisposition {
FailureDisposition::Rebuild(stage.default_rebuild_reason())
}
}
pub struct CachedPocketIcBaselinePool<R>
where
R: PocketIcBaselineRecipe,
{
recipe: R,
slots: BoundedSlotPool<BaselineSlot<R::Metadata>>,
}
struct BaselineSlot<M> {
baseline: CachedPocketIcBaseline<M>,
invalidation_reason: Option<RebuildReason>,
}
pub struct CachedPocketIcBaselinePoolGuard<'a, R>
where
R: PocketIcBaselineRecipe,
{
slot: BoundedSlotLease<'a, BaselineSlot<R::Metadata>>,
}
impl FixtureRecipeId {
pub fn try_new(identity: impl Into<String>) -> Result<Self, BaselinePoolContractError> {
let identity = identity.into();
if identity.trim().is_empty() {
return Err(BaselinePoolContractError::EmptyRecipeIdentity);
}
Ok(Self(identity))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl ResetRequirement {
#[must_use]
pub const fn domain(&self) -> ResetDomainKind {
match self {
Self::CanisterSnapshots => ResetDomainKind::CanisterSnapshots,
Self::CanisterCycles(_) => ResetDomainKind::CanisterCycles,
Self::PocketIcTime(_) => ResetDomainKind::PocketIcTime,
Self::ExtraCanisters(_) => ResetDomainKind::ExtraCanisters,
Self::PendingMessages(_) => ResetDomainKind::PendingMessages,
Self::SubnetState(_) => ResetDomainKind::SubnetState,
Self::ExternalResources(_) => ResetDomainKind::ExternalResources,
}
}
}
impl ResetAchievement {
#[must_use]
pub const fn domain(&self) -> ResetDomainKind {
match self {
Self::CanisterSnapshots => ResetDomainKind::CanisterSnapshots,
Self::CanisterCycles(_) => ResetDomainKind::CanisterCycles,
Self::PocketIcTime(_) => ResetDomainKind::PocketIcTime,
Self::ExtraCanisters(_) => ResetDomainKind::ExtraCanisters,
Self::PendingMessages(_) => ResetDomainKind::PendingMessages,
Self::SubnetState(_) => ResetDomainKind::SubnetState,
Self::ExternalResources(_) => ResetDomainKind::ExternalResources,
}
}
fn satisfies(&self, requirement: &ResetRequirement) -> bool {
match (requirement, self) {
(ResetRequirement::CanisterSnapshots, Self::CanisterSnapshots) => true,
(ResetRequirement::CanisterCycles(left), Self::CanisterCycles(right)) => left == right,
(ResetRequirement::PocketIcTime(left), Self::PocketIcTime(right)) => left == right,
(ResetRequirement::ExtraCanisters(left), Self::ExtraCanisters(right)) => left == right,
(ResetRequirement::PendingMessages(left), Self::PendingMessages(right))
| (ResetRequirement::SubnetState(left), Self::SubnetState(right))
| (ResetRequirement::ExternalResources(left), Self::ExternalResources(right)) => {
left == right
}
_ => false,
}
}
}
impl ResetRequirements {
pub fn try_new<I>(requirements: I) -> Result<Self, BaselinePoolContractError>
where
I: IntoIterator<Item = ResetRequirement>,
{
let mut domains = BTreeMap::new();
for requirement in requirements {
let domain = requirement.domain();
if domains.insert(domain, requirement).is_some() {
return Err(BaselinePoolContractError::DuplicateResetDomain { domain });
}
}
for domain in [
ResetDomainKind::CanisterSnapshots,
ResetDomainKind::CanisterCycles,
] {
if !domains.contains_key(&domain) {
return Err(BaselinePoolContractError::UndeclaredRequiredResetDomain { domain });
}
}
Ok(Self(domains))
}
#[must_use]
pub fn get(&self, domain: ResetDomainKind) -> Option<&ResetRequirement> {
self.0.get(&domain)
}
pub fn iter(&self) -> impl Iterator<Item = &ResetRequirement> {
self.0.values()
}
fn verify(&self, receipt: &ResetReceipt) -> Result<(), BaselinePoolContractError> {
for (domain, requirement) in &self.0 {
let Some(achievement) = receipt.0.get(domain) else {
return Err(BaselinePoolContractError::MissingResetDomain { domain: *domain });
};
if !achievement.satisfies(requirement) {
return Err(BaselinePoolContractError::ResetPolicyMismatch {
requirement: requirement.clone(),
achievement: achievement.clone(),
});
}
}
Ok(())
}
}
impl ResetReceipt {
pub fn try_new<I>(achievements: I) -> Result<Self, BaselinePoolContractError>
where
I: IntoIterator<Item = ResetAchievement>,
{
let mut domains = BTreeMap::new();
for achievement in achievements {
let domain = achievement.domain();
if domains.insert(domain, achievement).is_some() {
return Err(BaselinePoolContractError::DuplicateResetDomain { domain });
}
}
Ok(Self(domains))
}
#[must_use]
pub const fn empty() -> Self {
Self(BTreeMap::new())
}
#[must_use]
pub fn get(&self, domain: ResetDomainKind) -> Option<&ResetAchievement> {
self.0.get(&domain)
}
pub fn iter(&self) -> impl Iterator<Item = &ResetAchievement> {
self.0.values()
}
fn include_restore(
&mut self,
restore: &CanisterRestoreReceipt,
) -> Result<(), BaselinePoolContractError> {
self.insert(ResetAchievement::CanisterSnapshots)?;
self.insert(ResetAchievement::CanisterCycles(restore.cycle_policy))
}
fn insert(&mut self, achievement: ResetAchievement) -> Result<(), BaselinePoolContractError> {
let domain = achievement.domain();
if self.0.insert(domain, achievement).is_some() {
return Err(BaselinePoolContractError::DuplicateResetDomain { domain });
}
Ok(())
}
}
impl CanisterRestoreReceipt {
pub fn try_new<I>(
canister_ids: I,
cycle_policy: CycleResetPolicy,
) -> Result<Self, BaselinePoolContractError>
where
I: IntoIterator<Item = Principal>,
{
let mut unique = BTreeSet::new();
for canister_id in canister_ids {
if !unique.insert(canister_id) {
return Err(BaselinePoolContractError::DuplicateCanisterId { canister_id });
}
}
if unique.is_empty() {
return Err(BaselinePoolContractError::EmptyCanisterSet);
}
Ok(Self {
canister_ids: unique.into_iter().collect(),
cycle_policy,
})
}
pub fn try_from_baseline<M>(
baseline: &CachedPocketIcBaseline<M>,
cycle_policy: CycleResetPolicy,
) -> Result<Self, BaselinePoolContractError> {
Self::try_new(baseline.snapshot_canister_ids(), cycle_policy)
}
#[must_use]
pub fn canister_ids(&self) -> &[Principal] {
&self.canister_ids
}
#[must_use]
pub const fn cycle_policy(&self) -> CycleResetPolicy {
self.cycle_policy
}
}
impl ReadinessReceipt {
pub fn try_new(identity: impl Into<String>) -> Result<Self, BaselinePoolContractError> {
Ok(Self {
identity: nonempty_receipt_identity("readiness", identity.into())?,
})
}
#[must_use]
pub fn identity(&self) -> &str {
&self.identity
}
}
impl ValidationReceipt {
pub fn try_new(
recipe_id: FixtureRecipeId,
invariant_identity: impl Into<String>,
) -> Result<Self, BaselinePoolContractError> {
Ok(Self {
recipe_id,
invariant_identity: nonempty_receipt_identity("validation", invariant_identity.into())?,
})
}
#[must_use]
pub const fn recipe_id(&self) -> &FixtureRecipeId {
&self.recipe_id
}
#[must_use]
pub fn invariant_identity(&self) -> &str {
&self.invariant_identity
}
}
impl BaselinePreparationStage {
#[must_use]
pub fn default_rebuild_reason(self) -> RebuildReason {
match self {
Self::RestoreCanisters => RebuildReason::SnapshotRestoreFailure,
Self::ResetNonSnapshotState => RebuildReason::ResetFailure,
Self::DriveToReadiness => RebuildReason::ReadinessFailure,
Self::ValidateRestored | Self::ValidateBuilt => {
RebuildReason::InvariantValidationFailure
}
Self::Build => RebuildReason::RecipeClassified {
code: "build".to_owned(),
},
}
}
}
impl BaselinePoolTimings {
#[must_use]
pub const fn wait(self) -> Duration {
self.wait
}
#[must_use]
pub const fn build(self) -> Option<Duration> {
self.build
}
#[must_use]
pub const fn restore(self) -> Option<Duration> {
self.restore
}
#[must_use]
pub const fn reset(self) -> Option<Duration> {
self.reset
}
#[must_use]
pub const fn readiness(self) -> Option<Duration> {
self.readiness
}
#[must_use]
pub const fn validation(self) -> Option<Duration> {
self.validation
}
#[must_use]
pub const fn stale_teardown(self) -> Option<Duration> {
self.stale_teardown
}
#[must_use]
pub const fn total(self) -> Duration {
self.total
}
}
impl BaselinePoolOutcome {
#[must_use]
pub const fn slot(&self) -> usize {
match self {
Self::Built { slot, .. } | Self::Restored { slot, .. } | Self::Rebuilt { slot, .. } => {
*slot
}
}
}
#[must_use]
pub const fn timings(&self) -> BaselinePoolTimings {
match self {
Self::Built { timings, .. }
| Self::Restored { timings, .. }
| Self::Rebuilt { timings, .. } => *timings,
}
}
}
impl<E> BaselinePoolError<E> {
#[must_use]
pub const fn timings(&self) -> BaselinePoolTimings {
match self {
Self::Preparation { timings, .. } | Self::RecoveryFailed { timings, .. } => **timings,
}
}
}
impl<R> CachedPocketIcBaselinePool<R>
where
R: PocketIcBaselineRecipe,
{
#[must_use]
pub fn new(capacity: NonZeroUsize, recipe: R) -> Self {
Self {
recipe,
slots: BoundedSlotPool::new(capacity),
}
}
#[must_use]
pub fn recipe_id(&self) -> &FixtureRecipeId {
self.recipe.id()
}
#[must_use]
pub fn capacity(&self) -> NonZeroUsize {
self.slots.capacity()
}
pub fn acquire(
&self,
) -> Result<
(CachedPocketIcBaselinePoolGuard<'_, R>, BaselinePoolOutcome),
BaselinePoolError<R::Error>,
> {
let total_started = Instant::now();
let mut slot = self.slots.acquire();
let mut timings = BaselinePoolTimings {
wait: slot.wait(),
..BaselinePoolTimings::default()
};
let slot_index = slot.slot_index();
if slot.is_reusable() {
match self.prepare_reused(&mut slot, &mut timings) {
Ok(()) => {
timings.total = total_started.elapsed();
return Ok((
CachedPocketIcBaselinePoolGuard { slot },
BaselinePoolOutcome::Restored {
slot: slot_index,
timings,
},
));
}
Err(original) => {
let disposition = self.failure_disposition(&original);
match disposition {
FailureDisposition::Fatal => {
Self::discard_stale_slot(&mut slot, &mut timings);
timings.total = total_started.elapsed();
return Err(BaselinePoolError::Preparation {
error: original,
timings: Box::new(timings),
});
}
FailureDisposition::Rebuild(reason) => {
Self::discard_stale_slot(&mut slot, &mut timings);
if let Err(rebuild) = self.build_slot(&mut slot, &mut timings) {
timings.total = total_started.elapsed();
return Err(BaselinePoolError::RecoveryFailed {
original: Box::new(original),
rebuild: Box::new(rebuild),
timings: Box::new(timings),
});
}
timings.total = total_started.elapsed();
return Ok((
CachedPocketIcBaselinePoolGuard { slot },
BaselinePoolOutcome::Rebuilt {
slot: slot_index,
reason,
timings,
},
));
}
}
}
}
}
let rebuild_reason = if slot.invalidated_by_unwind() {
Some(RebuildReason::UnwindWhileLeased)
} else {
slot.get()
.and_then(|slot| slot.invalidation_reason.clone())
.or_else(|| {
slot.is_populated()
.then_some(RebuildReason::ExplicitLeaseInvalidation)
})
};
if slot.is_populated() {
Self::discard_stale_slot(&mut slot, &mut timings);
}
if let Err(error) = self.build_slot(&mut slot, &mut timings) {
timings.total = total_started.elapsed();
return Err(BaselinePoolError::Preparation {
error,
timings: Box::new(timings),
});
}
timings.total = total_started.elapsed();
let outcome = rebuild_reason.map_or_else(
|| BaselinePoolOutcome::Built {
slot: slot_index,
timings,
},
|reason| BaselinePoolOutcome::Rebuilt {
slot: slot_index,
reason,
timings,
},
);
Ok((CachedPocketIcBaselinePoolGuard { slot }, outcome))
}
fn prepare_reused(
&self,
slot: &mut BoundedSlotLease<'_, BaselineSlot<R::Metadata>>,
timings: &mut BaselinePoolTimings,
) -> Result<(), BaselinePoolPreparationError<R::Error>> {
let baseline = &slot
.get()
.expect("reusable baseline slot must be populated")
.baseline;
let started = Instant::now();
let restore = self.recipe.restore_canisters(baseline);
add_timing(&mut timings.restore, started.elapsed());
let canisters = restore.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::RestoreCanisters,
source,
})?;
let expected_canisters = baseline.snapshot_canister_ids().collect::<Vec<_>>();
if canisters.canister_ids() != expected_canisters {
return Err(BaselinePoolPreparationError::Contract(
BaselinePoolContractError::RestoreCanisterSetMismatch {
expected: expected_canisters,
actual: canisters.canister_ids().to_vec(),
},
));
}
let started = Instant::now();
let reset_result = self.recipe.reset_non_snapshot_state(baseline);
add_timing(&mut timings.reset, started.elapsed());
let mut reset = reset_result.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::ResetNonSnapshotState,
source,
})?;
reset
.include_restore(&canisters)
.map_err(BaselinePoolPreparationError::Contract)?;
let started = Instant::now();
let readiness_result = self.recipe.drive_to_readiness(baseline);
add_timing(&mut timings.readiness, started.elapsed());
let readiness =
readiness_result.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::DriveToReadiness,
source,
})?;
self.recipe
.reset_requirements()
.verify(&reset)
.map_err(BaselinePoolPreparationError::Contract)?;
let preparation = PreparedBaseline::Restored {
canisters,
reset,
readiness,
};
self.validate_baseline(
baseline,
&preparation,
BaselinePreparationStage::ValidateRestored,
timings,
)?;
slot.get_mut()
.expect("validated baseline slot must remain populated")
.invalidation_reason = None;
Ok(())
}
fn build_slot(
&self,
slot: &mut BoundedSlotLease<'_, BaselineSlot<R::Metadata>>,
timings: &mut BaselinePoolTimings,
) -> Result<(), BaselinePoolPreparationError<R::Error>> {
let started = Instant::now();
let build = self.recipe.build();
add_timing(&mut timings.build, started.elapsed());
let baseline = build.map_err(|source| BaselinePoolPreparationError::Recipe {
stage: BaselinePreparationStage::Build,
source,
})?;
if let Err(error) = self.validate_baseline(
&baseline,
&PreparedBaseline::Built,
BaselinePreparationStage::ValidateBuilt,
timings,
) {
drop_baseline_safely(baseline);
return Err(error);
}
let replaced = slot.replace(BaselineSlot {
baseline,
invalidation_reason: None,
});
debug_assert!(replaced.is_none());
Ok(())
}
fn validate_baseline(
&self,
baseline: &CachedPocketIcBaseline<R::Metadata>,
preparation: &PreparedBaseline,
stage: BaselinePreparationStage,
timings: &mut BaselinePoolTimings,
) -> Result<(), BaselinePoolPreparationError<R::Error>> {
let started = Instant::now();
let validation = self.recipe.validate(baseline, preparation);
add_timing(&mut timings.validation, started.elapsed());
let receipt =
validation.map_err(|source| BaselinePoolPreparationError::Recipe { stage, source })?;
if receipt.recipe_id() != self.recipe.id() {
return Err(BaselinePoolPreparationError::Contract(
BaselinePoolContractError::RecipeIdentityMismatch {
expected: self.recipe.id().clone(),
actual: receipt.recipe_id().clone(),
},
));
}
Ok(())
}
fn failure_disposition(
&self,
error: &BaselinePoolPreparationError<R::Error>,
) -> FailureDisposition {
match error {
BaselinePoolPreparationError::Recipe { stage, source } => {
self.recipe.classify_failure(*stage, source)
}
BaselinePoolPreparationError::Contract(
BaselinePoolContractError::RecipeIdentityMismatch { .. },
) => FailureDisposition::Fatal,
BaselinePoolPreparationError::Contract(_) => {
FailureDisposition::Rebuild(rebuild_reason_for_error(error))
}
}
}
fn discard_stale_slot(
slot: &mut BoundedSlotLease<'_, BaselineSlot<R::Metadata>>,
timings: &mut BaselinePoolTimings,
) {
let started = Instant::now();
if let Some(stale) = slot.take() {
drop_baseline_safely(stale.baseline);
}
timings.stale_teardown = Some(started.elapsed());
}
}
impl<R> CachedPocketIcBaselinePoolGuard<'_, R>
where
R: PocketIcBaselineRecipe,
{
#[must_use]
pub const fn slot(&self) -> usize {
self.slot.slot_index()
}
pub fn invalidate(&mut self, reason: RebuildReason) {
if let Some(slot) = self.slot.get_mut() {
slot.invalidation_reason = Some(reason);
}
self.slot.invalidate();
}
}
impl<R> Deref for CachedPocketIcBaselinePoolGuard<'_, R>
where
R: PocketIcBaselineRecipe,
{
type Target = CachedPocketIcBaseline<R::Metadata>;
fn deref(&self) -> &Self::Target {
&self
.slot
.get()
.expect("leased baseline pool slot must be populated")
.baseline
}
}
fn nonempty_receipt_identity(
receipt: &'static str,
identity: String,
) -> Result<String, BaselinePoolContractError> {
if identity.trim().is_empty() {
return Err(BaselinePoolContractError::EmptyReceiptIdentity { receipt });
}
Ok(identity)
}
fn add_timing(total: &mut Option<Duration>, elapsed: Duration) {
*total = Some(total.unwrap_or_default().saturating_add(elapsed));
}
fn rebuild_reason_for_error<E>(error: &BaselinePoolPreparationError<E>) -> RebuildReason {
match error {
BaselinePoolPreparationError::Recipe { stage, .. } => stage.default_rebuild_reason(),
BaselinePoolPreparationError::Contract(
BaselinePoolContractError::MissingResetDomain { .. }
| BaselinePoolContractError::ResetPolicyMismatch { .. }
| BaselinePoolContractError::DuplicateResetDomain { .. }
| BaselinePoolContractError::RestoreCanisterSetMismatch { .. },
) => RebuildReason::ResetCoverageMismatch,
BaselinePoolPreparationError::Contract(_) => RebuildReason::InvariantValidationFailure,
}
}
fn drop_baseline_safely<M>(baseline: CachedPocketIcBaseline<M>) {
let _ = catch_unwind(AssertUnwindSafe(|| drop(baseline)));
}
impl std::fmt::Display for FixtureRecipeId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::fmt::Display for BaselinePreparationStage {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Build => "build",
Self::RestoreCanisters => "canister restore",
Self::ResetNonSnapshotState => "non-snapshot reset",
Self::DriveToReadiness => "readiness",
Self::ValidateBuilt => "built-baseline validation",
Self::ValidateRestored => "restored-baseline validation",
})
}
}
impl std::fmt::Display for BaselinePoolContractError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyRecipeIdentity => formatter.write_str("fixture recipe identity is empty"),
Self::EmptyReceiptIdentity { receipt } => {
write!(formatter, "{receipt} receipt identity is empty")
}
Self::DuplicateResetDomain { domain } => {
write!(
formatter,
"reset domain {domain:?} was reported more than once"
)
}
Self::DuplicateCanisterId { canister_id } => {
write!(formatter, "restore receipt repeats canister {canister_id}")
}
Self::EmptyCanisterSet => formatter.write_str("restore receipt contains no canisters"),
Self::UndeclaredRequiredResetDomain { domain } => write!(
formatter,
"baseline recipe does not declare required reset domain {domain:?}",
),
Self::RestoreCanisterSetMismatch { expected, actual } => write!(
formatter,
"restore receipt identified canisters {actual:?}, expected captured set {expected:?}",
),
Self::MissingResetDomain { domain } => {
write!(
formatter,
"required reset domain {domain:?} was not achieved"
)
}
Self::ResetPolicyMismatch {
requirement,
achievement,
} => write!(
formatter,
"reset achievement {achievement:?} does not satisfy {requirement:?}",
),
Self::RecipeIdentityMismatch { expected, actual } => write!(
formatter,
"validation receipt used recipe `{actual}` instead of `{expected}`",
),
}
}
}
impl std::error::Error for BaselinePoolContractError {}
impl<E> std::fmt::Display for BaselinePoolPreparationError<E>
where
E: std::fmt::Display,
{
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Recipe { stage, source } => {
write!(formatter, "baseline {stage} failed: {source}")
}
Self::Contract(error) => write!(formatter, "baseline pool contract failed: {error}"),
}
}
}
impl<E> std::error::Error for BaselinePoolPreparationError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Recipe { source, .. } => Some(source),
Self::Contract(error) => Some(error),
}
}
}
impl<E> std::fmt::Display for BaselinePoolError<E>
where
E: std::fmt::Display,
{
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Preparation { error, .. } => error.fmt(formatter),
Self::RecoveryFailed {
original, rebuild, ..
} => write!(
formatter,
"baseline preparation failed ({original}); rebuilding the slot also failed: {rebuild}",
),
}
}
}
impl<E> std::error::Error for BaselinePoolError<E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Preparation { error, .. } => Some(error),
Self::RecoveryFailed { original, .. } => Some(original.as_ref()),
}
}
}
#[cfg(test)]
mod tests {
use super::{
BaselinePoolContractError, CycleResetPolicy, FixtureRecipeId, ResetAchievement,
ResetDomainKind, ResetReceipt, ResetRequirement, ResetRequirements,
};
#[test]
fn recipe_identity_must_be_nonempty() {
assert!(matches!(
FixtureRecipeId::try_new(" "),
Err(BaselinePoolContractError::EmptyRecipeIdentity)
));
}
#[test]
fn reset_requirements_reject_duplicate_domains() {
let result = ResetRequirements::try_new([
ResetRequirement::CanisterCycles(CycleResetPolicy::PreserveCurrent),
ResetRequirement::CanisterCycles(CycleResetPolicy::RestoreExactBaseline),
]);
assert!(matches!(
result,
Err(BaselinePoolContractError::DuplicateResetDomain {
domain: ResetDomainKind::CanisterCycles,
})
));
}
#[test]
fn required_policy_must_match_achieved_policy() {
let requirements = ResetRequirements::try_new([
ResetRequirement::CanisterSnapshots,
ResetRequirement::CanisterCycles(CycleResetPolicy::RestoreExactBaseline),
])
.unwrap();
let receipt = ResetReceipt::try_new([
ResetAchievement::CanisterSnapshots,
ResetAchievement::CanisterCycles(CycleResetPolicy::PreserveCurrent),
])
.unwrap();
assert!(matches!(
requirements.verify(&receipt),
Err(BaselinePoolContractError::ResetPolicyMismatch { .. })
));
}
}