Skip to content

Commit eb2085a

Browse files
adamspofford-dfinitylwshangraymondk
authored
feat: Stop canisters before upgrading (#418)
* Stop canisters before upgrading * Clear chunk store irrespective of installation success * don't stop stopped canisters * put the cli.md file back --------- Co-authored-by: Linwei Shang <linwei.shang@dfinity.org> Co-authored-by: raymondk <raymond.khalife@dfinity.org>
1 parent dccebcd commit eb2085a

4 files changed

Lines changed: 135 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

33
* feat: Added support for creating canisters on cloud engine subnets. Note that local networks cannot yet create these subnets.
4+
* feat: Upgrading canisters now stops them before the upgrade and starts them again afterwards
45
* feat: `icp canister logs` supports filtering by timestamp (`--since`, `--until`) and log index (`--since-index`, `--until-index`)
56
* feat: Support `log_memory_limit` canister setting in `icp canister settings update` and `icp canister settings sync`
67
* feat: Leaving off the method name parameter in `icp canister call` prompts you with an interactive list of methods

crates/icp-cli/src/commands/canister/install.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{
1313
commands::args,
1414
operations::{
1515
candid_compat::{CandidCompatibility, check_candid_compatibility},
16-
install::{install_canister, resolve_install_mode},
16+
install::{install_canister, resolve_install_mode_and_status},
1717
},
1818
};
1919

@@ -120,8 +120,9 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow
120120
.transpose()?;
121121

122122
let canister_display = args.cmd_args.canister.to_string();
123-
let install_mode =
124-
resolve_install_mode(&agent, &canister_display, &canister_id, &args.mode).await?;
123+
let (install_mode, status) =
124+
resolve_install_mode_and_status(&agent, &canister_display, &canister_id, &args.mode)
125+
.await?;
125126

126127
// Candid interface compatibility check for upgrades
127128
if !args.yes && matches!(install_mode, CanisterInstallMode::Upgrade(_)) {
@@ -158,6 +159,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow
158159
&canister_display,
159160
&wasm,
160161
install_mode,
162+
status,
161163
init_args_bytes.as_deref(),
162164
)
163165
.await?;

crates/icp-cli/src/commands/deploy.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
build::build_many_with_progress_bar,
2121
candid_compat::check_candid_compatibility_many,
2222
create::CreateOperation,
23-
install::{install_many, resolve_install_mode},
23+
install::{install_many, resolve_install_mode_and_status},
2424
settings::sync_settings_many,
2525
sync::sync_many,
2626
},
@@ -242,7 +242,8 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow:
242242
.await
243243
.map_err(|e| anyhow!(e))?;
244244

245-
let mode = resolve_install_mode(&agent, name, &cid, &args.mode).await?;
245+
let (mode, status) =
246+
resolve_install_mode_and_status(&agent, name, &cid, &args.mode).await?;
246247

247248
let env = ctx.get_environment(&environment_selection).await?;
248249
let (_canister_path, canister_info) =
@@ -254,7 +255,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow:
254255
.map(|ia| ia.to_bytes())
255256
.transpose()?;
256257

257-
Ok::<_, anyhow::Error>((name.clone(), cid, mode, init_args_bytes))
258+
Ok::<_, anyhow::Error>((name.clone(), cid, mode, status, init_args_bytes))
258259
}
259260
}))
260261
.await?;
@@ -265,7 +266,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow:
265266
agent.clone(),
266267
canisters
267268
.iter()
268-
.map(|(name, cid, mode, _)| (&**name, *cid, *mode)),
269+
.map(|(name, cid, mode, _, _)| (&**name, *cid, *mode)),
269270
ctx.artifacts.clone(),
270271
Arc::new(ctx.term.clone()),
271272
ctx.debug,

crates/icp-cli/src/operations/install.rs

Lines changed: 124 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use futures::{StreamExt, stream::FuturesOrdered};
22
use ic_agent::{Agent, AgentError, export::Principal};
33
use ic_management_canister_types::{
4-
CanisterId, ChunkHash, UpgradeFlags, UploadChunkArgs, WasmMemoryPersistence,
4+
CanisterId, CanisterStatusType, ChunkHash, UpgradeFlags, UploadChunkArgs, WasmMemoryPersistence,
55
};
66
use ic_utils::interfaces::{
77
ManagementCanister, management_canister::builders::CanisterInstallMode,
@@ -10,7 +10,7 @@ use icp::context::TermWriter;
1010
use sha2::{Digest, Sha256};
1111
use snafu::{ResultExt, Snafu};
1212
use std::sync::Arc;
13-
use tracing::debug;
13+
use tracing::{debug, warn};
1414

1515
use crate::progress::{ProgressManager, ProgressManagerSettings};
1616

@@ -23,6 +23,18 @@ pub enum InstallOperationError {
2323

2424
#[snafu(display("agent error: {source}"))]
2525
Agent { source: AgentError },
26+
27+
#[snafu(display("Failed to stop canister '{canister_name}' before upgrade"))]
28+
StopCanister {
29+
canister_name: String,
30+
source: AgentError,
31+
},
32+
33+
#[snafu(display("Failed to start canister '{canister_name}' after upgrade"))]
34+
StartCanister {
35+
canister_name: String,
36+
source: AgentError,
37+
},
2638
}
2739

2840
#[derive(Debug, Snafu)]
@@ -41,28 +53,26 @@ struct InstallFailure {
4153
/// Resolve a mode string ("auto", "install", "reinstall", "upgrade") into
4254
/// a [`CanisterInstallMode`]. For "auto", queries `canister_status` to
4355
/// determine whether the canister already has code installed.
44-
pub(crate) async fn resolve_install_mode(
56+
pub(crate) async fn resolve_install_mode_and_status(
4557
agent: &Agent,
4658
canister_name: &str,
4759
canister_id: &Principal,
4860
mode: &str,
49-
) -> Result<CanisterInstallMode, ResolveInstallModeError> {
61+
) -> Result<(CanisterInstallMode, CanisterStatusType), ResolveInstallModeError> {
62+
let mgmt = ManagementCanister::create(agent);
63+
let (status,) = mgmt
64+
.canister_status(canister_id)
65+
.await
66+
.context(ResolveInstallModeSnafu { canister_name })?;
5067
match mode {
51-
"auto" => {
52-
let mgmt = ManagementCanister::create(agent);
53-
let (status,) = mgmt
54-
.canister_status(canister_id)
55-
.await
56-
.context(ResolveInstallModeSnafu { canister_name })?;
57-
Ok(if status.module_hash.is_some() {
58-
CanisterInstallMode::Upgrade(None)
59-
} else {
60-
CanisterInstallMode::Install
61-
})
62-
}
63-
"install" => Ok(CanisterInstallMode::Install),
64-
"reinstall" => Ok(CanisterInstallMode::Reinstall),
65-
"upgrade" => Ok(CanisterInstallMode::Upgrade(None)),
68+
"auto" => Ok(if status.module_hash.is_some() {
69+
(CanisterInstallMode::Upgrade(None), status.status)
70+
} else {
71+
(CanisterInstallMode::Install, status.status)
72+
}),
73+
"install" => Ok((CanisterInstallMode::Install, status.status)),
74+
"reinstall" => Ok((CanisterInstallMode::Reinstall, status.status)),
75+
"upgrade" => Ok((CanisterInstallMode::Upgrade(None), status.status)),
6676
_ => panic!("invalid install mode: {mode}"),
6777
}
6878
}
@@ -80,6 +90,7 @@ pub(crate) async fn install_canister(
8090
canister_name: &str,
8191
wasm: &[u8],
8292
mode: CanisterInstallMode,
93+
status: CanisterStatusType,
8394
init_args: Option<&[u8]>,
8495
) -> Result<(), InstallOperationError> {
8596
let mode = match mode {
@@ -106,7 +117,16 @@ pub(crate) async fn install_canister(
106117
canister_name, mode
107118
);
108119

109-
do_install_operation(agent, canister_id, canister_name, wasm, mode, init_args).await
120+
do_install_operation(
121+
agent,
122+
canister_id,
123+
canister_name,
124+
wasm,
125+
mode,
126+
status,
127+
init_args,
128+
)
129+
.await
110130
}
111131

112132
async fn do_install_operation(
@@ -115,6 +135,7 @@ async fn do_install_operation(
115135
canister_name: &str,
116136
wasm: &[u8],
117137
mode: CanisterInstallMode,
138+
status: CanisterStatusType,
118139
init_args: Option<&[u8]>,
119140
) -> Result<(), InstallOperationError> {
120141
let mgmt = ManagementCanister::create(agent);
@@ -143,9 +164,12 @@ async fn do_install_operation(
143164
builder = builder.with_raw_arg(args.into());
144165
}
145166

146-
builder
147-
.await
148-
.map_err(|source| InstallOperationError::Agent { source })?;
167+
stop_and_start_if_upgrade(&mgmt, canister_id, canister_name, mode, status, async {
168+
builder
169+
.await
170+
.map_err(|source| InstallOperationError::Agent { source })
171+
})
172+
.await?;
149173
} else {
150174
// Large wasm: use chunked installation
151175
debug!("Installing wasm for {canister_name} using chunked installation");
@@ -197,31 +221,91 @@ async fn do_install_operation(
197221
builder = builder.with_raw_arg(args.to_vec());
198222
}
199223

200-
builder
201-
.await
202-
.map_err(|source| InstallOperationError::Agent { source })?;
224+
let install_res =
225+
stop_and_start_if_upgrade(&mgmt, canister_id, canister_name, mode, status, async {
226+
builder
227+
.await
228+
.map_err(|source| InstallOperationError::Agent { source })
229+
})
230+
.await;
203231

204232
// Clear chunk store after successful installation to free up storage
205-
mgmt.clear_chunk_store(canister_id)
233+
let clear_res = mgmt
234+
.clear_chunk_store(canister_id)
206235
.await
207-
.map_err(|source| InstallOperationError::Agent { source })?;
236+
.map_err(|source| InstallOperationError::Agent { source });
237+
238+
if let Err(clear_error) = clear_res {
239+
if let Err(install_error) = install_res {
240+
warn!("Failed to clear chunk store after failed install: {clear_error}");
241+
return Err(install_error);
242+
} else {
243+
return Err(clear_error);
244+
}
245+
}
246+
install_res?;
208247
}
209248

210249
Ok(())
211250
}
212251

252+
async fn stop_and_start_if_upgrade(
253+
mgmt: &ManagementCanister<'_>,
254+
canister_id: &Principal,
255+
canister_name: &str,
256+
mode: CanisterInstallMode,
257+
status: CanisterStatusType,
258+
f: impl Future<Output = Result<(), InstallOperationError>>,
259+
) -> Result<(), InstallOperationError> {
260+
let should_guard = matches!(
261+
mode,
262+
CanisterInstallMode::Upgrade(_) | CanisterInstallMode::Reinstall
263+
) && matches!(status, CanisterStatusType::Running);
264+
// Stop the canister before proceeding
265+
if should_guard {
266+
mgmt.stop_canister(canister_id)
267+
.await
268+
.context(StopCanisterSnafu { canister_name })?;
269+
}
270+
// Install the canister
271+
let install_result = f.await;
272+
// Restart the canister whether or not the installation succeeded
273+
if should_guard {
274+
let start_result = mgmt.start_canister(canister_id).await;
275+
if let Err(start_error) = start_result {
276+
// If both install and start failed, report the install error since it's more likely to be the root cause
277+
if let Err(install_error) = install_result {
278+
warn!("Failed to start canister after failed upgrade: {start_error}");
279+
return Err(install_error);
280+
} else {
281+
return Err(start_error).context(StartCanisterSnafu { canister_name });
282+
}
283+
}
284+
}
285+
286+
install_result
287+
}
288+
213289
/// Installs code to multiple canisters and displays progress bars.
214290
pub(crate) async fn install_many(
215291
agent: Agent,
216-
canisters: impl IntoIterator<Item = (String, Principal, CanisterInstallMode, Option<Vec<u8>>)>,
292+
canisters: impl IntoIterator<
293+
Item = (
294+
String,
295+
Principal,
296+
CanisterInstallMode,
297+
CanisterStatusType,
298+
Option<Vec<u8>>,
299+
),
300+
>,
217301
artifacts: Arc<dyn icp::store_artifact::Access>,
218302
term: Arc<TermWriter>,
219303
debug: bool,
220304
) -> Result<(), InstallManyError> {
221305
let mut futs = FuturesOrdered::new();
222306
let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: debug });
223307

224-
for (name, cid, mode, init_args) in canisters {
308+
for (name, cid, mode, status, init_args) in canisters {
225309
let pb = progress_manager.create_progress_bar(&name);
226310
let agent = agent.clone();
227311
let install_fn = {
@@ -238,7 +322,16 @@ pub(crate) async fn install_many(
238322
}
239323
})?;
240324

241-
install_canister(&agent, &cid, &name, &wasm, mode, init_args.as_deref()).await
325+
install_canister(
326+
&agent,
327+
&cid,
328+
&name,
329+
&wasm,
330+
mode,
331+
status,
332+
init_args.as_deref(),
333+
)
334+
.await
242335
}
243336
};
244337

0 commit comments

Comments
 (0)