use candid::Principal;
use ic_call_retry::{
call_idempotent_method_with_retry, call_nonidempotent_method_with_retry,
when_out_of_time_or_stopping, Deadline, ErrorCause, RetryError,
};
use ic_cdk::api::canister_self;
use ic_cdk::call::CallErrorExt;
use ic_cdk::management_canister::InstallChunkedCodeArgs;
use ic_cdk::management_canister::{
CanisterInfoArgs, CanisterInfoResult, CanisterInstallMode, ChunkHash, ClearChunkStoreArgs,
InstallCodeArgs, UploadChunkArgs,
};
use ic_management_canister_types::{
ChangeDetails, ChangeOrigin, StartCanisterArgs, StopCanisterArgs,
};
use sha2::{Digest, Sha256};
#[cfg(feature = "use_call_chaos")]
use ic_call_chaos::Call;
#[cfg(not(feature = "use_call_chaos"))]
use ic_cdk::call::Call;
pub type CanisterId = Principal;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpgradeStage {
Stopping,
ObtainingInfo,
Installing,
Starting,
}
#[derive(Debug, Clone)]
pub enum UpgradeErrorReason {
RetryError(RetryError),
ConcurrentChangeDetected,
}
#[derive(Debug, Clone)]
pub struct UpgradeError {
pub stage: UpgradeStage,
pub reason: UpgradeErrorReason,
}
#[derive(Debug, Clone)]
pub struct ChunkedModule {
pub wasm_module_hash: Vec<u8>,
pub store_canister_id: CanisterId,
pub chunk_hashes_list: Vec<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub enum WasmModule {
Bytes(Vec<u8>),
ChunkedModule(ChunkedModule),
}
enum VersionChangeCheck {
NoChange,
UpgradeSucceeded,
ConcurrentChangeDetected,
}
async fn version_change_check(
target_id: CanisterId,
wasm_module: &WasmModule,
old_version: u64,
should_retry: &mut impl FnMut() -> bool,
) -> Result<VersionChangeCheck, RetryError> {
let (new_version, mut recent_changes) =
best_effort_canister_info(target_id, Some(1), should_retry)
.await
.map(|info| (info.total_num_changes, info.recent_changes))?;
let last_change = if let Some(change) = recent_changes.pop() {
change
} else {
return Ok(VersionChangeCheck::ConcurrentChangeDetected);
};
match (
new_version - old_version,
last_change.details,
last_change.origin,
) {
(0, _, _) => Ok(VersionChangeCheck::NoChange),
(1, ChangeDetails::CodeDeployment(dep), ChangeOrigin::FromCanister(rec))
if rec.canister_id == canister_self() =>
{
let expected_hash: Vec<u8> = match wasm_module {
WasmModule::Bytes(ref wasm_bytes) => Sha256::digest(wasm_bytes).to_vec(),
WasmModule::ChunkedModule(ref chunked) => chunked.wasm_module_hash.clone(),
};
if dep.module_hash != expected_hash {
Ok(VersionChangeCheck::ConcurrentChangeDetected)
} else {
Ok(VersionChangeCheck::UpgradeSucceeded)
}
}
(_, _, _) => Ok(VersionChangeCheck::ConcurrentChangeDetected),
}
}
pub async fn upgrade_canister<P>(
target_id: CanisterId,
wasm_module: WasmModule,
arg: Vec<u8>,
should_retry: &mut P,
) -> Result<(), UpgradeError>
where
P: FnMut() -> bool,
{
let add_stage = |stage: UpgradeStage| {
move |error: RetryError| UpgradeError {
stage,
reason: UpgradeErrorReason::RetryError(error),
}
};
best_effort_stop(target_id, should_retry)
.await
.map_err(add_stage(UpgradeStage::Stopping))?;
let version = best_effort_canister_info(target_id, None, should_retry)
.await
.map(|info| info.total_num_changes)
.map_err(add_stage(UpgradeStage::ObtainingInfo))?;
loop {
let install_result = match wasm_module {
WasmModule::Bytes(ref wasm_bytes) => {
best_effort_install_single_chunk(target_id, wasm_bytes, &arg, should_retry).await
}
WasmModule::ChunkedModule(ref chunked) => {
best_effort_install_chunked(target_id, chunked, &arg, should_retry).await
}
};
match install_result {
Ok(()) => break,
Err(RetryError::StatusUnknown(ErrorCause::CallFailed(rejection)))
if !rejection.is_clean_reject() =>
{
let version_check_result =
version_change_check(target_id, &wasm_module, version, should_retry)
.await
.map_err(add_stage(UpgradeStage::Installing))?;
match version_check_result {
VersionChangeCheck::NoChange => {
ic_cdk::println!(
"Failed to upgrade {:?} and the version hasn't moved, retrying",
target_id
);
continue;
}
VersionChangeCheck::UpgradeSucceeded => {
break;
}
VersionChangeCheck::ConcurrentChangeDetected => {
return Err(UpgradeError {
stage: UpgradeStage::Installing,
reason: UpgradeErrorReason::ConcurrentChangeDetected,
});
}
}
}
Err(error) => return Err(add_stage(UpgradeStage::Installing)(error)),
}
}
best_effort_start(target_id, should_retry)
.await
.map_err(add_stage(UpgradeStage::Starting))
}
async fn best_effort_stop<P>(target_id: Principal, should_retry: &mut P) -> Result<(), RetryError>
where
P: FnMut() -> bool,
{
let args = StopCanisterArgs {
canister_id: target_id,
};
Ok(call_idempotent_method_with_retry(
Call::bounded_wait(Principal::management_canister(), "stop_canister").with_arg(&args),
should_retry,
)
.await?
.candid()
.unwrap())
}
async fn best_effort_start<P>(target_id: CanisterId, should_retry: &mut P) -> Result<(), RetryError>
where
P: FnMut() -> bool,
{
let args = StartCanisterArgs {
canister_id: target_id,
};
Ok(call_idempotent_method_with_retry(
Call::bounded_wait(Principal::management_canister(), "start_canister").with_arg(&args),
should_retry,
)
.await?
.candid()
.unwrap())
}
async fn best_effort_canister_info<P>(
target_id: CanisterId,
num_requested_changes: Option<u64>,
should_retry: &mut P,
) -> Result<CanisterInfoResult, RetryError>
where
P: FnMut() -> bool,
{
let arg = CanisterInfoArgs {
canister_id: target_id,
num_requested_changes,
};
Ok(call_idempotent_method_with_retry(
Call::bounded_wait(Principal::management_canister(), "canister_info").with_arg(&arg),
should_retry,
)
.await?
.candid()
.unwrap())
}
async fn best_effort_install_single_chunk<P>(
target_id: CanisterId,
wasm_bytes: &[u8],
arg: &[u8],
should_retry: &mut P,
) -> Result<(), RetryError>
where
P: FnMut() -> bool,
{
let install_args = InstallCodeArgs {
mode: CanisterInstallMode::Upgrade(None),
canister_id: target_id,
wasm_module: wasm_bytes.to_vec(),
arg: arg.to_vec(),
};
Ok(call_nonidempotent_method_with_retry(
Call::bounded_wait(Principal::management_canister(), "install_code")
.with_arg(&install_args),
should_retry,
)
.await?
.candid()
.expect("Candid decoding failed"))
}
#[allow(dead_code)]
async fn upload_chunks(
store_canister_id: CanisterId,
chunks: Vec<Vec<u8>>,
deadline: &Deadline,
) -> Result<(), RetryError> {
let call = Call::bounded_wait(Principal::management_canister(), "clear_chunk_store").with_arg(
&ClearChunkStoreArgs {
canister_id: store_canister_id,
},
);
let mut retry_fn = when_out_of_time_or_stopping(deadline);
let _: () = call_idempotent_method_with_retry(call, &mut retry_fn)
.await?
.candid()
.unwrap();
for chunk in chunks {
let chunk_install_args = UploadChunkArgs {
canister_id: store_canister_id,
chunk,
};
let call = Call::bounded_wait(Principal::management_canister(), "upload_chunk")
.with_arg(&chunk_install_args);
let mut retry_fn = when_out_of_time_or_stopping(deadline);
let _: () = call_idempotent_method_with_retry(call, &mut retry_fn)
.await?
.candid()
.unwrap();
}
Ok(())
}
async fn best_effort_install_chunked<P>(
target_id: CanisterId,
chunked: &ChunkedModule,
arg: &[u8],
should_retry: &mut P,
) -> Result<(), RetryError>
where
P: FnMut() -> bool,
{
let install_args = InstallChunkedCodeArgs {
mode: CanisterInstallMode::Upgrade(None),
target_canister: target_id,
store_canister: Some(chunked.store_canister_id),
chunk_hashes_list: chunked
.chunk_hashes_list
.iter()
.map(|hash| ChunkHash { hash: hash.clone() })
.collect(),
wasm_module_hash: chunked.wasm_module_hash.clone(),
arg: arg.to_vec(),
};
let install_call = Call::bounded_wait(Principal::management_canister(), "install_chunked_code")
.with_arg(&install_args);
let res = call_nonidempotent_method_with_retry(install_call, should_retry).await?;
Ok(res.candid().unwrap())
}