#[cfg(test)]
mod tests;
use crate::{
InternalError,
dto::placement::index::{
PlacementIndexRegistryEntry, PlacementIndexRegistryResponse, PlacementIndexStatusResponse,
},
ops::{prelude::*, storage::StorageOpsError},
storage::stable::placement_index::{
PlacementIndexEntryRecord, PlacementIndexKey, PlacementIndexRegistry,
},
};
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum PlacementIndexRegistryOpsError {
#[error("invalid index key: {0}")]
InvalidKey(String),
#[error("index key '{key_value}' in pool '{pool}' already bound to instance {pid}")]
KeyBound {
pool: String,
key_value: String,
pid: Principal,
},
#[error(
"index key '{key_value}' in pool '{pool}' is pending for provisional child {expected}, not {actual}"
)]
ProvisionalPidMismatch {
pool: String,
key_value: String,
expected: Principal,
actual: Principal,
},
}
impl From<PlacementIndexRegistryOpsError> for InternalError {
fn from(err: PlacementIndexRegistryOpsError) -> Self {
StorageOpsError::from(err).into()
}
}
pub struct PlacementIndexRegistryOps;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PlacementIndexEntryState {
Pending {
claim_id: u64,
owner_pid: Principal,
created_at: u64,
provisional_pid: Option<Principal>,
},
Bound {
instance_pid: Principal,
bound_at: u64,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PlacementIndexPendingClaim {
pub claim_id: u64,
pub owner_pid: Principal,
pub created_at: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PlacementIndexClaimResult {
Bound {
instance_pid: Principal,
bound_at: u64,
},
PendingExisting {
claim_id: u64,
owner_pid: Principal,
created_at: u64,
provisional_pid: Option<Principal>,
},
Claimed(PlacementIndexPendingClaim),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PlacementIndexReleaseResult {
Missing,
Bound {
instance_pid: Principal,
bound_at: u64,
},
PendingRetained {
owner_pid: Principal,
created_at: u64,
provisional_pid: Option<Principal>,
},
ReleasedStalePending {
owner_pid: Principal,
created_at: u64,
provisional_pid: Option<Principal>,
},
}
impl PlacementIndexRegistryOps {
pub const PENDING_TTL_SECS: u64 = 300;
pub fn claim_pending(
pool: &str,
key_value: &str,
owner_pid: Principal,
claim_id: u64,
created_at: u64,
) -> Result<PlacementIndexClaimResult, InternalError> {
let key = PlacementIndexKey::try_new(pool, key_value)
.map_err(PlacementIndexRegistryOpsError::InvalidKey)?;
match PlacementIndexRegistry::get(&key) {
Some(PlacementIndexEntryRecord::Bound {
instance_pid,
bound_at,
}) => Ok(PlacementIndexClaimResult::Bound {
instance_pid,
bound_at,
}),
Some(PlacementIndexEntryRecord::Pending {
claim_id,
owner_pid: existing_owner_pid,
created_at: existing_created_at,
provisional_pid,
}) => Ok(PlacementIndexClaimResult::PendingExisting {
claim_id,
owner_pid: existing_owner_pid,
created_at: existing_created_at,
provisional_pid,
}),
None => {
PlacementIndexRegistry::insert(
key,
PlacementIndexEntryRecord::Pending {
claim_id,
owner_pid,
created_at,
provisional_pid: None,
},
);
Ok(PlacementIndexClaimResult::Claimed(
PlacementIndexPendingClaim {
claim_id,
owner_pid,
created_at,
},
))
}
}
}
#[must_use]
pub fn lookup_state(pool: &str, key_value: &str) -> Option<PlacementIndexEntryState> {
let key = PlacementIndexKey::try_new(pool, key_value).ok()?;
PlacementIndexRegistry::get(&key).map(entry_to_state)
}
pub fn set_provisional_pid_if_claim_matches(
pool: &str,
key_value: &str,
expected_claim_id: u64,
provisional_pid: Principal,
) -> Result<bool, InternalError> {
let key = PlacementIndexKey::try_new(pool, key_value)
.map_err(PlacementIndexRegistryOpsError::InvalidKey)?;
let entry = PlacementIndexRegistry::get(&key);
let Some(PlacementIndexEntryRecord::Pending {
claim_id,
owner_pid,
created_at,
..
}) = entry
else {
return Ok(false);
};
if claim_id != expected_claim_id {
return Ok(false);
}
PlacementIndexRegistry::insert(
key,
PlacementIndexEntryRecord::Pending {
claim_id,
owner_pid,
created_at,
provisional_pid: Some(provisional_pid),
},
);
Ok(true)
}
#[must_use]
pub fn lookup_key(pool: &str, key_value: &str) -> Option<Principal> {
let key = PlacementIndexKey::try_new(pool, key_value).ok()?;
match PlacementIndexRegistry::get(&key) {
Some(PlacementIndexEntryRecord::Bound { instance_pid, .. }) => Some(instance_pid),
Some(PlacementIndexEntryRecord::Pending { .. }) | None => None,
}
}
#[must_use]
pub fn lookup_entry(pool: &str, key_value: &str) -> Option<PlacementIndexStatusResponse> {
let key = PlacementIndexKey::try_new(pool, key_value).ok()?;
PlacementIndexRegistry::get(&key).map(entry_to_response)
}
pub fn release_stale_pending_if_claim_matches(
pool: &str,
key_value: &str,
expected_claim_id: u64,
now: u64,
) -> Result<PlacementIndexReleaseResult, InternalError> {
let key = PlacementIndexKey::try_new(pool, key_value)
.map_err(PlacementIndexRegistryOpsError::InvalidKey)?;
let Some(entry) = PlacementIndexRegistry::get(&key) else {
return Ok(PlacementIndexReleaseResult::Missing);
};
match entry {
PlacementIndexEntryRecord::Bound {
instance_pid,
bound_at,
} => Ok(PlacementIndexReleaseResult::Bound {
instance_pid,
bound_at,
}),
PlacementIndexEntryRecord::Pending {
claim_id,
owner_pid,
created_at,
provisional_pid,
} if claim_id != expected_claim_id
|| !is_pending_stale(now, created_at)
|| provisional_pid.is_none() =>
{
Ok(PlacementIndexReleaseResult::PendingRetained {
owner_pid,
created_at,
provisional_pid,
})
}
PlacementIndexEntryRecord::Pending {
claim_id: _,
owner_pid,
created_at,
provisional_pid,
} => {
let _ = PlacementIndexRegistry::remove(&key);
Ok(PlacementIndexReleaseResult::ReleasedStalePending {
owner_pid,
created_at,
provisional_pid,
})
}
}
}
pub fn bind(
pool: &str,
key_value: &str,
pid: Principal,
bound_at: u64,
) -> Result<(), InternalError> {
let key = PlacementIndexKey::try_new(pool, key_value)
.map_err(PlacementIndexRegistryOpsError::InvalidKey)?;
match PlacementIndexRegistry::get(&key) {
Some(PlacementIndexEntryRecord::Bound { instance_pid, .. }) if instance_pid == pid => {
Ok(())
}
Some(PlacementIndexEntryRecord::Bound { instance_pid, .. }) => {
Err(PlacementIndexRegistryOpsError::KeyBound {
pool: pool.to_string(),
key_value: key_value.to_string(),
pid: instance_pid,
}
.into())
}
Some(PlacementIndexEntryRecord::Pending {
provisional_pid: Some(expected_pid),
..
}) if expected_pid != pid => {
Err(PlacementIndexRegistryOpsError::ProvisionalPidMismatch {
pool: pool.to_string(),
key_value: key_value.to_string(),
expected: expected_pid,
actual: pid,
}
.into())
}
Some(PlacementIndexEntryRecord::Pending { .. }) | None => {
PlacementIndexRegistry::insert(
key,
PlacementIndexEntryRecord::Bound {
instance_pid: pid,
bound_at,
},
);
Ok(())
}
}
}
pub fn bind_if_claim_matches(
pool: &str,
key_value: &str,
expected_claim_id: u64,
pid: Principal,
bound_at: u64,
) -> Result<bool, InternalError> {
let key = PlacementIndexKey::try_new(pool, key_value)
.map_err(PlacementIndexRegistryOpsError::InvalidKey)?;
match PlacementIndexRegistry::get(&key) {
Some(PlacementIndexEntryRecord::Pending {
claim_id,
provisional_pid: Some(expected_pid),
..
}) if claim_id == expected_claim_id && expected_pid != pid => {
Err(PlacementIndexRegistryOpsError::ProvisionalPidMismatch {
pool: pool.to_string(),
key_value: key_value.to_string(),
expected: expected_pid,
actual: pid,
}
.into())
}
Some(PlacementIndexEntryRecord::Pending { claim_id, .. })
if claim_id != expected_claim_id =>
{
Ok(false)
}
Some(PlacementIndexEntryRecord::Pending { .. }) => {
PlacementIndexRegistry::insert(
key,
PlacementIndexEntryRecord::Bound {
instance_pid: pid,
bound_at,
},
);
Ok(true)
}
Some(PlacementIndexEntryRecord::Bound { .. }) | None => Ok(false),
}
}
#[must_use]
pub fn entries_response() -> PlacementIndexRegistryResponse {
let entries = PlacementIndexRegistry::export()
.entries
.into_iter()
.map(|record| PlacementIndexRegistryEntry {
pool: record.key.pool.to_string(),
key_value: record.key.key_value.to_string(),
status: entry_to_response(record.entry),
})
.collect();
PlacementIndexRegistryResponse(entries)
}
#[cfg(test)]
pub(crate) fn clear_for_test() {
PlacementIndexRegistry::clear();
}
}
const fn is_pending_stale(now: u64, created_at: u64) -> bool {
now.saturating_sub(created_at) > PlacementIndexRegistryOps::PENDING_TTL_SECS
}
const fn entry_to_response(entry: PlacementIndexEntryRecord) -> PlacementIndexStatusResponse {
match entry {
PlacementIndexEntryRecord::Pending {
claim_id: _,
owner_pid,
created_at,
provisional_pid,
} => PlacementIndexStatusResponse::Pending {
owner_pid,
created_at,
provisional_pid,
},
PlacementIndexEntryRecord::Bound {
instance_pid,
bound_at,
} => PlacementIndexStatusResponse::Bound {
instance_pid,
bound_at,
},
}
}
const fn entry_to_state(entry: PlacementIndexEntryRecord) -> PlacementIndexEntryState {
match entry {
PlacementIndexEntryRecord::Pending {
claim_id,
owner_pid,
created_at,
provisional_pid,
} => PlacementIndexEntryState::Pending {
claim_id,
owner_pid,
created_at,
provisional_pid,
},
PlacementIndexEntryRecord::Bound {
instance_pid,
bound_at,
} => PlacementIndexEntryState::Bound {
instance_pid,
bound_at,
},
}
}