use super::super::super::store_pid_for_binding;
use super::super::WasmStorePublicationWorkflow;
use crate::{
ids::{WasmStoreBinding, WasmStoreGcMode},
ops::storage::state::subnet::SubnetStateOps,
view::state::PublicationStoreStateView,
workflow::runtime::template::publication::error::PublicationWorkflowError,
};
use canic_core::cdk::types::Principal;
use canic_core::control_plane_support::{
error::{InternalError, InternalErrorOrigin},
ops::ic::IcOps,
};
use canic_core::{log, log::Topic};
impl WasmStorePublicationWorkflow {
pub(in crate::workflow::runtime::template::publication::lifecycle) fn binding_for_store_pid(
store_pid: Principal,
) -> WasmStoreBinding {
WasmStoreBinding::owned(store_pid.to_text())
}
fn binding_slot(slot: Option<&WasmStoreBinding>) -> String {
slot.map_or_else(|| "-".to_string(), std::string::ToString::to_string)
}
pub(in crate::workflow::runtime::template::publication) fn binding_is_reserved_for_publication(
state: &PublicationStoreStateView,
binding: &WasmStoreBinding,
) -> bool {
state.detached_binding.as_ref() == Some(binding)
|| state.retired_binding.as_ref() == Some(binding)
}
fn ensure_binding_is_selectable_for_publication(
state: &PublicationStoreStateView,
binding: &WasmStoreBinding,
) -> Result<(), InternalError> {
if Self::binding_is_reserved_for_publication(state, binding) {
return Err(InternalError::workflow(
InternalErrorOrigin::Workflow,
format!("ws binding '{binding}' is detached/retired"),
));
}
Ok(())
}
fn ensure_binding_is_writable(binding: &WasmStoreBinding) -> Result<(), InternalError> {
let store = SubnetStateOps::wasm_stores()
.into_iter()
.find(|store| &store.binding == binding)
.ok_or_else(|| {
PublicationWorkflowError::InvalidState(format!(
"ws binding '{binding}' is missing from runtime inventory"
))
})?;
if store.gc.mode != WasmStoreGcMode::Normal {
return Err(PublicationWorkflowError::StoreNotWritable {
binding: binding.clone(),
mode: store.gc.mode,
}
.into());
}
Ok(())
}
pub(in crate::workflow::runtime::template::publication::lifecycle) fn log_publication_state_transition(
transition_kind: &str,
previous: &PublicationStoreStateView,
current: &PublicationStoreStateView,
changed_at: u64,
) {
if previous == current {
return;
}
log!(
Topic::Wasm,
Info,
"ws.transition kind={} gen={} at={} old_a={} old_d={} old_r={} new_a={} new_d={} new_r={}",
transition_kind,
current.generation,
changed_at,
Self::binding_slot(previous.active_binding.as_ref()),
Self::binding_slot(previous.detached_binding.as_ref()),
Self::binding_slot(previous.retired_binding.as_ref()),
Self::binding_slot(current.active_binding.as_ref()),
Self::binding_slot(current.detached_binding.as_ref()),
Self::binding_slot(current.retired_binding.as_ref()),
);
}
pub(in crate::workflow::runtime::template::publication) fn ensure_retired_binding_slot_available_for_promotion()
-> Result<(), InternalError> {
let state = SubnetStateOps::publication_store_state();
if state.detached_binding.is_some() && state.retired_binding.is_some() {
return Err(InternalError::workflow(
InternalErrorOrigin::Workflow,
"ws rollover blocked: retired slot occupied".to_string(),
));
}
Ok(())
}
pub(in crate::workflow::runtime::template::publication) fn ensure_retired_binding_slot_available_for_retirement()
-> Result<(), InternalError> {
let state = SubnetStateOps::publication_store_state();
if state.retired_binding.is_some() {
return Err(InternalError::workflow(
InternalErrorOrigin::Workflow,
"ws retirement blocked: retired slot occupied".to_string(),
));
}
Ok(())
}
pub fn retire_detached_publication_store_binding() -> Option<WasmStoreBinding> {
if let Err(err) = Self::ensure_retired_binding_slot_available_for_retirement() {
log!(Topic::Wasm, Warn, "{err}");
return None;
}
let changed_at = IcOps::now_secs();
let previous = SubnetStateOps::publication_store_state();
let retired = SubnetStateOps::retire_detached_publication_store_binding(changed_at);
if let Some(binding) = retired.as_ref() {
let current = SubnetStateOps::publication_store_state();
Self::log_publication_state_transition(
"retire_detached_binding",
&previous,
¤t,
changed_at,
);
log!(Topic::Wasm, Ok, "ws retired {}", binding);
}
retired
}
pub fn set_current_publication_store_binding(
binding: WasmStoreBinding,
) -> Result<(), InternalError> {
let _ = store_pid_for_binding(&binding)?;
Self::ensure_retired_binding_slot_available_for_promotion()?;
let previous = SubnetStateOps::publication_store_state();
Self::ensure_binding_is_selectable_for_publication(&previous, &binding)?;
Self::ensure_binding_is_writable(&binding)?;
let changed_at = IcOps::now_secs();
if SubnetStateOps::activate_publication_store_binding(binding, changed_at) {
let current = SubnetStateOps::publication_store_state();
Self::log_publication_state_transition(
"pin_publication_binding",
&previous,
¤t,
changed_at,
);
}
Ok(())
}
pub fn clear_current_publication_store_binding() -> Result<(), InternalError> {
Self::ensure_retired_binding_slot_available_for_promotion()?;
let changed_at = IcOps::now_secs();
let previous = SubnetStateOps::publication_store_state();
if SubnetStateOps::clear_publication_store_binding(changed_at) {
let current = SubnetStateOps::publication_store_state();
Self::log_publication_state_transition(
"clear_publication_binding",
&previous,
¤t,
changed_at,
);
}
Ok(())
}
pub(in crate::workflow::runtime::template::publication::lifecycle) fn oldest_registered_store_binding()
-> Option<WasmStoreBinding> {
SubnetStateOps::wasm_stores()
.into_iter()
.min_by(|left, right| left.created_at.cmp(&right.created_at))
.map(|record| record.binding)
}
pub(in crate::workflow::runtime::template::publication::lifecycle) fn clear_stale_publication_binding(
binding: WasmStoreBinding,
) -> Result<WasmStoreBinding, InternalError> {
log!(Topic::Wasm, Warn, "ws clear stale binding {}", binding);
let changed_at = IcOps::now_secs();
Self::ensure_retired_binding_slot_available_for_promotion()?;
let previous = SubnetStateOps::publication_store_state();
if !SubnetStateOps::clear_publication_store_binding(changed_at) {
return Err(PublicationWorkflowError::InvalidState(format!(
"stale ws binding '{binding}' was no longer active"
))
.into());
}
let current = SubnetStateOps::publication_store_state();
Self::log_publication_state_transition(
"clear_stale_publication_binding",
&previous,
¤t,
changed_at,
);
Self::oldest_registered_store_binding().ok_or_else(|| {
InternalError::workflow(
InternalErrorOrigin::Workflow,
"no registered wasm stores after clearing stale publication binding",
)
})
}
}