From 70d1c1a81cca43206d897fa570cc152ca5d1b622 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Jan 2025 20:09:14 -0500 Subject: [PATCH 01/12] Remove CandidEncodeFailed variant --- ic-cdk-timers/src/lib.rs | 4 ++-- ic-cdk/src/call.rs | 33 ++++++++++++++++----------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 064f828c..4ac0c2d3 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -146,8 +146,8 @@ extern "C" fn global_timer() { retry_later = true; } } - CallError::CandidEncodeFailed(_) | CallError::CandidDecodeFailed(_) => { - // These errors are not transient, and will not be retried. + CallError::CandidDecodeFailed(_) => { + // This error is not transient, and will not be retried. } } if retry_later { diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index d3a9644b..3a514b89 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -136,14 +136,6 @@ impl PartialEq for CallPerformErrorCode { /// The error type for inter-canister calls. #[derive(thiserror::Error, Debug, Clone)] pub enum CallError { - /// The arguments could not be encoded. - /// - /// This can only happen when the arguments are provided using [`Call::with_arg`] and [`Call::with_args`]. - /// Though the type system guarantees that the arguments are valid Candid types, - /// it is possible that the encoding fails for reasons such as memory allocation failure. - #[error("Failed to encode the arguments: {0}")] - CandidEncodeFailed(String), - /// The call immediately failed when invoking the call_perform system API. #[error("The IC was not able to enqueue the call with code {0:?}")] CallPerformFailed(CallPerformErrorCode), @@ -398,7 +390,10 @@ impl SendableCall for Call<'_> { impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T> { async fn call_raw(self) -> CallResult> { - let args_raw = encode_args(self.args).map_err(encoder_error_to_call_error::)?; + // Candid Encoding can only fail if heap memory is exhausted. + // That is not a recoverable error, so we panic. + let args_raw = + encode_args(self.args).unwrap_or_else(|e| panic!("Failed to encode args: {}", e)); call_raw_internal( self.call.canister_id, self.call.method, @@ -410,7 +405,10 @@ impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T> } fn call_oneway(self) -> CallResult<()> { - let args_raw = encode_args(self.args).map_err(encoder_error_to_call_error::)?; + // Candid Encoding can only fail if heap memory is exhausted. + // That is not a recoverable error, so we panic. + let args_raw = + encode_args(self.args).unwrap_or_else(|e| panic!("Failed to encode args: {}", e)); call_oneway_internal( self.call.canister_id, self.call.method, @@ -423,7 +421,10 @@ impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T> impl<'a, T: CandidType + Send + Sync> SendableCall for CallWithArg<'a, T> { async fn call_raw(self) -> CallResult> { - let args_raw = encode_one(self.arg).map_err(encoder_error_to_call_error::)?; + // Candid Encoding can only fail if heap memory is exhausted. + // That is not a recoverable error, so we panic. + let args_raw = + encode_one(self.arg).unwrap_or_else(|e| panic!("Failed to encode arg: {}", e)); call_raw_internal( self.call.canister_id, self.call.method, @@ -435,7 +436,10 @@ impl<'a, T: CandidType + Send + Sync> SendableCall for CallWithArg<'a, T> { } fn call_oneway(self) -> CallResult<()> { - let args_raw = encode_one(self.arg).map_err(encoder_error_to_call_error::)?; + // Candid Encoding can only fail if heap memory is exhausted. + // That is not a recoverable error, so we panic. + let args_raw = + encode_one(self.arg).unwrap_or_else(|e| panic!("Failed to encode arg: {}", e)); call_oneway_internal( self.call.canister_id, self.call.method, @@ -700,8 +704,3 @@ fn call_cycles_add(cycles: u128) { fn decoder_error_to_call_error(err: candid::error::Error) -> CallError { CallError::CandidDecodeFailed(format!("{}: {}", std::any::type_name::(), err)) } - -/// Converts a encoder error to a CallError. -fn encoder_error_to_call_error(err: candid::error::Error) -> CallError { - CallError::CandidEncodeFailed(format!("{}: {}", std::any::type_name::(), err)) -} From fedcf2c2c35f0d6675cfa11fdc1a5b428ce6f07d Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Jan 2025 21:08:02 -0500 Subject: [PATCH 02/12] Unify RejectCode --- ic-cdk-timers/src/lib.rs | 6 ++-- ic-cdk/src/call.rs | 66 +++------------------------------------- 2 files changed, 8 insertions(+), 64 deletions(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 4ac0c2d3..3ef42177 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -29,7 +29,7 @@ use std::{ use futures::{stream::FuturesUnordered, StreamExt}; use slotmap::{new_key_type, KeyData, SlotMap}; -use ic_cdk::call::{Call, CallError, CallPerformErrorCode, RejectCode, SendableCall}; +use ic_cdk::call::{Call, CallError, RejectCode, SendableCall}; // To ensure that tasks are removable seamlessly, there are two separate concepts here: tasks, for the actual function being called, // and timers, the scheduled execution of tasks. As this is an implementation detail, this does not affect the exported name TimerId, @@ -141,8 +141,8 @@ extern "C" fn global_timer() { retry_later = true; } } - CallError::CallPerformFailed(call_perform_error_code) => { - if call_perform_error_code == CallPerformErrorCode::SysTransient { + CallError::CallPerformFailed(reject_code) => { + if reject_code == RejectCode::SysTransient { retry_later = true; } } diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index 3a514b89..0128f490 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -77,68 +77,12 @@ impl PartialEq for RejectCode { } } -/// Error codes from the `ic0.call_perform` system API. -/// -/// See [`ic0.call_perform`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#system-api-call) for more details. -/// -/// So far, the specified codes (1, 2, 3) share the same meaning as the corresponding [`RejectCode`]s. -#[repr(u32)] -#[derive(CandidType, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum CallPerformErrorCode { - /// No error. - NoError = 0, - - /// Fatal system error, retry unlikely to be useful. - SysFatal = 1, - /// Transient system error, retry might be possible. - SysTransient = 2, - /// Invalid destination (e.g. canister/account does not exist). - DestinationInvalid = 3, - - /// Unrecognized error code. - /// - /// Note that this variant is not part of the IC interface spec, and is used to represent - /// rejection codes that are not recognized by the library. - Unrecognized(u32), -} - -impl From for CallPerformErrorCode { - fn from(code: u32) -> Self { - match code { - 0 => CallPerformErrorCode::NoError, - 1 => CallPerformErrorCode::SysFatal, - 2 => CallPerformErrorCode::SysTransient, - 3 => CallPerformErrorCode::DestinationInvalid, - n => CallPerformErrorCode::Unrecognized(n), - } - } -} - -impl From for u32 { - fn from(code: CallPerformErrorCode) -> u32 { - match code { - CallPerformErrorCode::NoError => 0, - CallPerformErrorCode::SysFatal => 1, - CallPerformErrorCode::SysTransient => 2, - CallPerformErrorCode::DestinationInvalid => 3, - CallPerformErrorCode::Unrecognized(n) => n, - } - } -} - -impl PartialEq for CallPerformErrorCode { - fn eq(&self, other: &u32) -> bool { - let self_as_u32: u32 = (*self).into(); - self_as_u32 == *other - } -} - /// The error type for inter-canister calls. #[derive(thiserror::Error, Debug, Clone)] pub enum CallError { /// The call immediately failed when invoking the call_perform system API. #[error("The IC was not able to enqueue the call with code {0:?}")] - CallPerformFailed(CallPerformErrorCode), + CallPerformFailed(RejectCode), /// The call was rejected. /// @@ -541,8 +485,8 @@ impl> Future for CallFuture { }; // The conversion fails only when the err_code is 0, which means the call was successfully enqueued. - match CallPerformErrorCode::from(err_code) { - CallPerformErrorCode::NoError => {} + match RejectCode::from(err_code) { + RejectCode::NoError => {} c => { let result = Err(CallError::CallPerformFailed(c)); state.result = Some(result.clone()); @@ -680,8 +624,8 @@ fn call_oneway_internal>( ic0::call_perform() }; // The conversion fails only when the err_code is 0, which means the call was successfully enqueued. - match CallPerformErrorCode::from(err_code) { - CallPerformErrorCode::NoError => Ok(()), + match RejectCode::from(err_code) { + RejectCode::NoError => Ok(()), c => Err(CallError::CallPerformFailed(c)), } } From 7b88edcd646e36bd1f895b2d60b58b217a69f0f2 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Jan 2025 21:33:34 -0500 Subject: [PATCH 03/12] RejectCode doesn't include 0 --- ic-cdk/src/call.rs | 81 ++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index 0128f490..57d56cd9 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -4,6 +4,7 @@ use candid::utils::{ArgumentDecoder, ArgumentEncoder}; use candid::{ decode_args, decode_one, encode_args, encode_one, CandidType, Deserialize, Principal, }; +use std::error::Error; use std::future::Future; use std::pin::Pin; use std::sync::atomic::Ordering; @@ -13,12 +14,8 @@ use std::task::{Context, Poll, Waker}; /// Reject code explains why the inter-canister call is rejected. /// /// See [Reject codes](https://internetcomputer.org/docs/current/references/ic-interface-spec/#reject-codes) for more details. -#[repr(u32)] #[derive(CandidType, Deserialize, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum RejectCode { - /// No error. - NoError = 0, - /// Fatal system error, retry unlikely to be useful. SysFatal = 1, /// Transient system error, retry might be possible. @@ -31,26 +28,34 @@ pub enum RejectCode { CanisterError = 5, /// Response unknown; system stopped waiting for it (e.g., timed out, or system under high load). SysUnknown = 6, +} - /// Unrecognized reject code. - /// - /// Note that this variant is not part of the IC interface spec, and is used to represent - /// reject codes that are not recognized by the library. - Unrecognized(u32), +/// Error type for [`RejectCode`] conversion. +/// +/// A reject code is invalid if it is not one of the known reject codes. +#[derive(Clone, Copy, Debug)] +pub struct InvalidRejectCode(pub u32); + +impl std::fmt::Display for InvalidRejectCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "invalid reject code: {}", self.0) + } } -impl From for RejectCode { - fn from(code: u32) -> Self { +impl Error for InvalidRejectCode {} + +impl TryFrom for RejectCode { + type Error = InvalidRejectCode; + + fn try_from(code: u32) -> Result { match code { - // 0 is a special code meaning "no error" - 0 => RejectCode::NoError, - 1 => RejectCode::SysFatal, - 2 => RejectCode::SysTransient, - 3 => RejectCode::DestinationInvalid, - 4 => RejectCode::CanisterReject, - 5 => RejectCode::CanisterError, - 6 => RejectCode::SysUnknown, - n => RejectCode::Unrecognized(n), + 1 => Ok(RejectCode::SysFatal), + 2 => Ok(RejectCode::SysTransient), + 3 => Ok(RejectCode::DestinationInvalid), + 4 => Ok(RejectCode::CanisterReject), + 5 => Ok(RejectCode::CanisterError), + 6 => Ok(RejectCode::SysUnknown), + n => Err(InvalidRejectCode(n)), } } } @@ -58,14 +63,12 @@ impl From for RejectCode { impl From for u32 { fn from(code: RejectCode) -> u32 { match code { - RejectCode::NoError => 0, RejectCode::SysFatal => 1, RejectCode::SysTransient => 2, RejectCode::DestinationInvalid => 3, RejectCode::CanisterReject => 4, RejectCode::CanisterError => 5, RejectCode::SysUnknown => 6, - RejectCode::Unrecognized(n) => n, } } } @@ -459,7 +462,7 @@ impl> Future for CallFuture { // callback and cleanup are safe to parameterize with T because: // - if the future is dropped before the callback is called, there will be no more strong references and the weak reference will fail to upgrade // - if the future is *not* dropped before the callback is called, the compiler will mandate that any data borrowed by T is still alive - let err_code = unsafe { + let code = unsafe { ic0::call_new( callee.as_ptr() as usize, callee.len(), @@ -484,11 +487,13 @@ impl> Future for CallFuture { ic0::call_perform() }; - // The conversion fails only when the err_code is 0, which means the call was successfully enqueued. - match RejectCode::from(err_code) { - RejectCode::NoError => {} - c => { - let result = Err(CallError::CallPerformFailed(c)); + match code { + 0 => { + // call_perform returns 0 means the call was successfully enqueued. + } + _ => { + let reject_code = RejectCode::try_from(code).unwrap(); + let result = Err(CallError::CallPerformFailed(reject_code)); state.result = Some(result.clone()); return Poll::Ready(result); } @@ -513,9 +518,12 @@ unsafe extern "C" fn callback>(state_ptr: *const RwLock Ok(msg_arg_data()), - c => Err(CallError::CallRejected(c, msg_reject_msg())), + state.write().unwrap().result = Some(match msg_reject_code() { + 0 => Ok(msg_arg_data()), + code => { + let reject_code = RejectCode::try_from(code).unwrap(); + Err(CallError::CallRejected(reject_code, msg_reject_msg())) + } }); } let w = state.write().unwrap().waker.take(); @@ -600,7 +608,7 @@ fn call_oneway_internal>( // `args`, being a &[u8], is a readable sequence of bytes. // ic0.call_with_best_effort_response is always safe to call. // ic0.call_perform is always safe to call. - let err_code = unsafe { + let code = unsafe { ic0::call_new( callee.as_ptr() as usize, callee.len(), @@ -624,9 +632,12 @@ fn call_oneway_internal>( ic0::call_perform() }; // The conversion fails only when the err_code is 0, which means the call was successfully enqueued. - match RejectCode::from(err_code) { - RejectCode::NoError => Ok(()), - c => Err(CallError::CallPerformFailed(c)), + match code { + 0 => Ok(()), + _ => { + let reject_code = RejectCode::try_from(code).unwrap(); + Err(CallError::CallPerformFailed(reject_code)) + } } } From a8a87acf2c07d93365bd6b64ed09a261b21d983e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Jan 2025 21:38:47 -0500 Subject: [PATCH 04/12] CallPerform also has a hard-coded reject message --- ic-cdk-timers/src/lib.rs | 2 +- ic-cdk/src/call.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 3ef42177..191bd3d9 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -141,7 +141,7 @@ extern "C" fn global_timer() { retry_later = true; } } - CallError::CallPerformFailed(reject_code) => { + CallError::CallPerformFailed(reject_code, _) => { if reject_code == RejectCode::SysTransient { retry_later = true; } diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index 57d56cd9..c908b5c6 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -85,7 +85,7 @@ impl PartialEq for RejectCode { pub enum CallError { /// The call immediately failed when invoking the call_perform system API. #[error("The IC was not able to enqueue the call with code {0:?}")] - CallPerformFailed(RejectCode), + CallPerformFailed(RejectCode, String), /// The call was rejected. /// @@ -493,7 +493,10 @@ impl> Future for CallFuture { } _ => { let reject_code = RejectCode::try_from(code).unwrap(); - let result = Err(CallError::CallPerformFailed(reject_code)); + let result = Err(CallError::CallPerformFailed( + reject_code, + "Couldn't send message".to_string(), + )); state.result = Some(result.clone()); return Poll::Ready(result); } @@ -636,7 +639,10 @@ fn call_oneway_internal>( 0 => Ok(()), _ => { let reject_code = RejectCode::try_from(code).unwrap(); - Err(CallError::CallPerformFailed(reject_code)) + Err(CallError::CallPerformFailed( + reject_code, + "Couldn't send message".to_string(), + )) } } } From a3ea5c2e48bb82e9d5d4dd546472af151d134dec Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Jan 2025 23:09:12 -0500 Subject: [PATCH 05/12] SystemError v.s. CallError --- ic-cdk-timers/src/lib.rs | 9 +---- ic-cdk/src/call.rs | 83 +++++++++++++++++++++++++--------------- ic-cdk/src/prelude.rs | 2 +- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 191bd3d9..0da7640d 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -136,13 +136,8 @@ extern "C" fn global_timer() { ic_cdk::println!("[ic-cdk-timers] canister_global_timer: {e:?}"); let mut retry_later = false; match e { - CallError::CallRejected(reject_code, _) => { - if reject_code == RejectCode::SysTransient { - retry_later = true; - } - } - CallError::CallPerformFailed(reject_code, _) => { - if reject_code == RejectCode::SysTransient { + CallError::CallRejected(call_error) => { + if call_error.reject_code == RejectCode::SysTransient { retry_later = true; } } diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index c908b5c6..6865f3a3 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -80,18 +80,14 @@ impl PartialEq for RejectCode { } } -/// The error type for inter-canister calls. +/// The error type for inter-canister calls and decoding the response. #[derive(thiserror::Error, Debug, Clone)] pub enum CallError { - /// The call immediately failed when invoking the call_perform system API. - #[error("The IC was not able to enqueue the call with code {0:?}")] - CallPerformFailed(RejectCode, String), - /// The call was rejected. /// /// Please handle the error by matching on the rejection code. - #[error("The call was rejected with code {0:?} and message: {1}")] - CallRejected(RejectCode, String), + #[error("The call was rejected with code {0:?}")] + CallRejected(SystemError), /// The response could not be decoded. /// @@ -102,7 +98,28 @@ pub enum CallError { CandidDecodeFailed(String), } +/// The error type for inter-canister calls. +#[derive(Debug, Clone)] +pub struct SystemError { + /// See [`RejectCode`]. + pub reject_code: RejectCode, + /// The reject message. + /// + /// When the call was rejected asynchronously (IC rejects the call after it was enqueued), + /// this message is set with [`msg_reject`](crate::api::msg_reject). + /// + /// When the call was rejected synchronously (`ic0.call_preform` returns non-zero code), + /// this message is set to a fixed string ("failed to enqueue the call"). + pub reject_message: String, + /// Whether the call was rejected synchronously (`ic0.call_perform` returned non-zero code) + /// or asynchronously (IC rejects the call after it was enqueued). + pub sync: bool, +} + /// Result of a inter-canister call. +pub type SystemResult = Result; + +/// Result of a inter-canister call and decoding the response. pub type CallResult = Result; /// Inter-Canister Call. @@ -279,7 +296,7 @@ impl<'a, A> ConfigurableCall for CallWithRawArgs<'a, A> { /// Methods to send a call. pub trait SendableCall { /// Sends the call and gets the reply as raw bytes. - fn call_raw(self) -> impl Future>> + Send + Sync; + fn call_raw(self) -> impl Future>> + Send + Sync; /// Sends the call and decodes the reply to a Candid type. fn call(self) -> impl Future> + Send + Sync @@ -289,7 +306,7 @@ pub trait SendableCall { { let fut = self.call_raw(); async { - let bytes = fut.await?; + let bytes = fut.await.map_err(CallError::CallRejected)?; decode_one(&bytes).map_err(decoder_error_to_call_error::) } } @@ -302,17 +319,17 @@ pub trait SendableCall { { let fut = self.call_raw(); async { - let bytes = fut.await?; + let bytes = fut.await.map_err(CallError::CallRejected)?; decode_args(&bytes).map_err(decoder_error_to_call_error::) } } /// Sends the call and ignores the reply. - fn call_oneway(self) -> CallResult<()>; + fn call_oneway(self) -> SystemResult<()>; } impl SendableCall for Call<'_> { - fn call_raw(self) -> impl Future>> + Send + Sync { + fn call_raw(self) -> impl Future>> + Send + Sync { let args_raw = vec![0x44, 0x49, 0x44, 0x4c, 0x00, 0x00]; call_raw_internal::>( self.canister_id, @@ -323,7 +340,7 @@ impl SendableCall for Call<'_> { ) } - fn call_oneway(self) -> CallResult<()> { + fn call_oneway(self) -> SystemResult<()> { let args_raw = vec![0x44, 0x49, 0x44, 0x4c, 0x00, 0x00]; call_oneway_internal::>( self.canister_id, @@ -336,7 +353,7 @@ impl SendableCall for Call<'_> { } impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T> { - async fn call_raw(self) -> CallResult> { + async fn call_raw(self) -> SystemResult> { // Candid Encoding can only fail if heap memory is exhausted. // That is not a recoverable error, so we panic. let args_raw = @@ -351,7 +368,7 @@ impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T> .await } - fn call_oneway(self) -> CallResult<()> { + fn call_oneway(self) -> SystemResult<()> { // Candid Encoding can only fail if heap memory is exhausted. // That is not a recoverable error, so we panic. let args_raw = @@ -367,7 +384,7 @@ impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T> } impl<'a, T: CandidType + Send + Sync> SendableCall for CallWithArg<'a, T> { - async fn call_raw(self) -> CallResult> { + async fn call_raw(self) -> SystemResult> { // Candid Encoding can only fail if heap memory is exhausted. // That is not a recoverable error, so we panic. let args_raw = @@ -382,7 +399,7 @@ impl<'a, T: CandidType + Send + Sync> SendableCall for CallWithArg<'a, T> { .await } - fn call_oneway(self) -> CallResult<()> { + fn call_oneway(self) -> SystemResult<()> { // Candid Encoding can only fail if heap memory is exhausted. // That is not a recoverable error, so we panic. let args_raw = @@ -398,7 +415,7 @@ impl<'a, T: CandidType + Send + Sync> SendableCall for CallWithArg<'a, T> { } impl<'a, A: AsRef<[u8]> + Send + Sync + 'a> SendableCall for CallWithRawArgs<'a, A> { - fn call_raw(self) -> impl Future>> + Send + Sync { + fn call_raw(self) -> impl Future>> + Send + Sync { call_raw_internal( self.call.canister_id, self.call.method, @@ -408,7 +425,7 @@ impl<'a, A: AsRef<[u8]> + Send + Sync + 'a> SendableCall for CallWithRawArgs<'a, ) } - fn call_oneway(self) -> CallResult<()> { + fn call_oneway(self) -> SystemResult<()> { call_oneway_internal( self.call.canister_id, self.call.method, @@ -423,7 +440,7 @@ impl<'a, A: AsRef<[u8]> + Send + Sync + 'a> SendableCall for CallWithRawArgs<'a, // Internal state for the Future when sending a call. struct CallFutureState> { - result: Option>>, + result: Option>>, waker: Option, id: Principal, method: String, @@ -437,7 +454,7 @@ struct CallFuture> { } impl> Future for CallFuture { - type Output = CallResult>; + type Output = SystemResult>; fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { let self_ref = Pin::into_inner(self); @@ -493,10 +510,11 @@ impl> Future for CallFuture { } _ => { let reject_code = RejectCode::try_from(code).unwrap(); - let result = Err(CallError::CallPerformFailed( + let result = Err(SystemError { reject_code, - "Couldn't send message".to_string(), - )); + reject_message: "failed to enqueue the call".to_string(), + sync: true, + }); state.result = Some(result.clone()); return Poll::Ready(result); } @@ -525,7 +543,11 @@ unsafe extern "C" fn callback>(state_ptr: *const RwLock Ok(msg_arg_data()), code => { let reject_code = RejectCode::try_from(code).unwrap(); - Err(CallError::CallRejected(reject_code, msg_reject_msg())) + Err(SystemError { + reject_code, + reject_message: msg_reject_msg(), + sync: false, + }) } }); } @@ -574,7 +596,7 @@ fn call_raw_internal<'a, T: AsRef<[u8]> + Send + Sync + 'a>( args_raw: T, cycles: Option, timeout_seconds: Option, -) -> impl Future>> + Send + Sync + 'a { +) -> impl Future>> + Send + Sync + 'a { let state = Arc::new(RwLock::new(CallFutureState { result: None, waker: None, @@ -593,7 +615,7 @@ fn call_oneway_internal>( args_raw: T, cycles: Option, timeout_seconds: Option, -) -> CallResult<()> { +) -> SystemResult<()> { let callee = id.as_slice(); // We set all callbacks to usize::MAX, which is guaranteed to be invalid callback index. // The system will still deliver the reply, but it will trap immediately because the callback @@ -639,10 +661,11 @@ fn call_oneway_internal>( 0 => Ok(()), _ => { let reject_code = RejectCode::try_from(code).unwrap(); - Err(CallError::CallPerformFailed( + Err(SystemError { reject_code, - "Couldn't send message".to_string(), - )) + reject_message: "failed to enqueue the call".to_string(), + sync: true, + }) } } } diff --git a/ic-cdk/src/prelude.rs b/ic-cdk/src/prelude.rs index d697f178..b8e6978f 100644 --- a/ic-cdk/src/prelude.rs +++ b/ic-cdk/src/prelude.rs @@ -1,6 +1,6 @@ //! The prelude module contains the most commonly used types and traits. pub use crate::api::{canister_self, debug_print, msg_caller, trap}; -pub use crate::call::{Call, CallResult, ConfigurableCall, RejectCode, SendableCall}; +pub use crate::call::{Call, ConfigurableCall, SendableCall}; pub use crate::macros::{ export_candid, heartbeat, init, inspect_message, post_upgrade, pre_upgrade, query, update, }; From 556a19e93e24c07dc013550643e0040896741b25 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Jan 2025 23:21:55 -0500 Subject: [PATCH 06/12] avoid candid en/decoding in timers --- ic-cdk-timers/src/lib.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 0da7640d..85c79813 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -29,7 +29,7 @@ use std::{ use futures::{stream::FuturesUnordered, StreamExt}; use slotmap::{new_key_type, KeyData, SlotMap}; -use ic_cdk::call::{Call, CallError, RejectCode, SendableCall}; +use ic_cdk::call::{Call, RejectCode, SendableCall}; // To ensure that tasks are removable seamlessly, there are two separate concepts here: tasks, for the actual function being called, // and timers, the scheduled execution of tasks. As this is an implementation detail, this does not affect the exported name TimerId, @@ -117,8 +117,8 @@ extern "C" fn global_timer() { ic_cdk::api::canister_self(), " timer_executor", ) - .with_arg(task_id.0.as_ffi()) - .call::<()>() + .with_raw_args(task_id.0.as_ffi().to_be_bytes().to_vec()) + .call_raw() .await, ) }); @@ -135,15 +135,8 @@ extern "C" fn global_timer() { if let Err(e) = res { ic_cdk::println!("[ic-cdk-timers] canister_global_timer: {e:?}"); let mut retry_later = false; - match e { - CallError::CallRejected(call_error) => { - if call_error.reject_code == RejectCode::SysTransient { - retry_later = true; - } - } - CallError::CandidDecodeFailed(_) => { - // This error is not transient, and will not be retried. - } + if e.reject_code == RejectCode::SysTransient { + retry_later = true; } if retry_later { // Try to execute the timer again later. @@ -270,7 +263,6 @@ fn update_ic0_timer() { export_name = "canister_update_ic_cdk_internal.timer_executor" )] extern "C" fn timer_executor() { - use candid::utils::{decode_one, encode_one}; if ic_cdk::api::msg_caller() != ic_cdk::api::canister_self() { ic_cdk::trap("This function is internal to ic-cdk and should not be called externally."); } @@ -278,7 +270,8 @@ extern "C" fn timer_executor() { // timer_executor is only called by the canister itself (from global_timer), // so we can safely assume that the argument is a valid TimerId (u64). // And we don't need decode_one_with_config/DecoderConfig to defense against malicious payload. - let task_id: u64 = decode_one(&arg_bytes).unwrap(); + assert!(arg_bytes.len() == 8); + let task_id = u64::from_be_bytes(arg_bytes.try_into().unwrap()); let task_id = TimerId(KeyData::from_ffi(task_id)); // We can't be holding `TASKS` when we call the function, because it may want to schedule more tasks. @@ -299,5 +292,5 @@ extern "C" fn timer_executor() { } } } - ic_cdk::api::msg_reply(encode_one(()).unwrap()); + ic_cdk::api::msg_reply(&[]); } From 71c27c80aec55ae67f1725a0f332337c0fcd8363 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Jan 2025 10:17:16 -0500 Subject: [PATCH 07/12] SystemError includes error_code --- ic-cdk/src/call.rs | 193 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index 6865f3a3..7869a5e9 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -80,6 +80,189 @@ impl PartialEq for RejectCode { } } +/// The error codes provide additional details for rejected messages. +/// +/// See [Error codes](https://internetcomputer.org/docs/current/references/ic-interface-spec/#error-codes) for more details. +/// +/// # Note +/// +/// As of the current version of the IC, the error codes are not available in the system API. +/// There is a plan to add them in the short term. +/// To avoid breaking changes at that time, the [`SystemError`] struct start to include the error code. +/// Please DO NOT rely on the error codes until they are officially supported. +// +// The variants and their codes below are from [pocket-ic](https://docs.rs/pocket-ic/latest/pocket_ic/enum.ErrorCode.html). +#[allow(missing_docs)] +#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ErrorCode { + // 1xx -- `RejectCode::SysFatal` + SubnetOversubscribed = 101, + MaxNumberOfCanistersReached = 102, + // 2xx -- `RejectCode::SysTransient` + CanisterQueueFull = 201, + IngressMessageTimeout = 202, + CanisterQueueNotEmpty = 203, + IngressHistoryFull = 204, + CanisterIdAlreadyExists = 205, + StopCanisterRequestTimeout = 206, + CanisterOutOfCycles = 207, + CertifiedStateUnavailable = 208, + CanisterInstallCodeRateLimited = 209, + CanisterHeapDeltaRateLimited = 210, + // 3xx -- `RejectCode::DestinationInvalid` + CanisterNotFound = 301, + CanisterSnapshotNotFound = 305, + // 4xx -- `RejectCode::CanisterReject` + InsufficientMemoryAllocation = 402, + InsufficientCyclesForCreateCanister = 403, + SubnetNotFound = 404, + CanisterNotHostedBySubnet = 405, + CanisterRejectedMessage = 406, + UnknownManagementMessage = 407, + InvalidManagementPayload = 408, + // 5xx -- `RejectCode::CanisterError` + CanisterTrapped = 502, + CanisterCalledTrap = 503, + CanisterContractViolation = 504, + CanisterInvalidWasm = 505, + CanisterDidNotReply = 506, + CanisterOutOfMemory = 507, + CanisterStopped = 508, + CanisterStopping = 509, + CanisterNotStopped = 510, + CanisterStoppingCancelled = 511, + CanisterInvalidController = 512, + CanisterFunctionNotFound = 513, + CanisterNonEmpty = 514, + QueryCallGraphLoopDetected = 517, + InsufficientCyclesInCall = 520, + CanisterWasmEngineError = 521, + CanisterInstructionLimitExceeded = 522, + CanisterMemoryAccessLimitExceeded = 524, + QueryCallGraphTooDeep = 525, + QueryCallGraphTotalInstructionLimitExceeded = 526, + CompositeQueryCalledInReplicatedMode = 527, + QueryTimeLimitExceeded = 528, + QueryCallGraphInternal = 529, + InsufficientCyclesInComputeAllocation = 530, + InsufficientCyclesInMemoryAllocation = 531, + InsufficientCyclesInMemoryGrow = 532, + ReservedCyclesLimitExceededInMemoryAllocation = 533, + ReservedCyclesLimitExceededInMemoryGrow = 534, + InsufficientCyclesInMessageMemoryGrow = 535, + CanisterMethodNotFound = 536, + CanisterWasmModuleNotFound = 537, + CanisterAlreadyInstalled = 538, + CanisterWasmMemoryLimitExceeded = 539, + ReservedCyclesLimitIsTooLow = 540, + // 6xx -- `RejectCode::SysUnknown` + DeadlineExpired = 601, + ResponseDropped = 602, +} + +/// Error type for [`ErrorCode`] conversion. +/// +/// An error code is invalid if it is not one of the known error codes. +#[derive(Clone, Copy, Debug)] +pub struct InvalidErrorCode(pub u32); + +impl std::fmt::Display for InvalidErrorCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "invalid error code: {}", self.0) + } +} + +impl Error for InvalidErrorCode {} + +impl TryFrom for ErrorCode { + type Error = InvalidErrorCode; + fn try_from(code: u32) -> Result { + match code { + // 1xx -- `RejectCode::SysFatal` + 101 => Ok(ErrorCode::SubnetOversubscribed), + 102 => Ok(ErrorCode::MaxNumberOfCanistersReached), + // 2xx -- `RejectCode::SysTransient` + 201 => Ok(ErrorCode::CanisterQueueFull), + 202 => Ok(ErrorCode::IngressMessageTimeout), + 203 => Ok(ErrorCode::CanisterQueueNotEmpty), + 204 => Ok(ErrorCode::IngressHistoryFull), + 205 => Ok(ErrorCode::CanisterIdAlreadyExists), + 206 => Ok(ErrorCode::StopCanisterRequestTimeout), + 207 => Ok(ErrorCode::CanisterOutOfCycles), + 208 => Ok(ErrorCode::CertifiedStateUnavailable), + 209 => Ok(ErrorCode::CanisterInstallCodeRateLimited), + 210 => Ok(ErrorCode::CanisterHeapDeltaRateLimited), + // 3xx -- `RejectCode::DestinationInvalid` + 301 => Ok(ErrorCode::CanisterNotFound), + 305 => Ok(ErrorCode::CanisterSnapshotNotFound), + // 4xx -- `RejectCode::CanisterReject` + 402 => Ok(ErrorCode::InsufficientMemoryAllocation), + 403 => Ok(ErrorCode::InsufficientCyclesForCreateCanister), + 404 => Ok(ErrorCode::SubnetNotFound), + 405 => Ok(ErrorCode::CanisterNotHostedBySubnet), + 406 => Ok(ErrorCode::CanisterRejectedMessage), + 407 => Ok(ErrorCode::UnknownManagementMessage), + 408 => Ok(ErrorCode::InvalidManagementPayload), + // 5xx -- `RejectCode::CanisterError` + 502 => Ok(ErrorCode::CanisterTrapped), + 503 => Ok(ErrorCode::CanisterCalledTrap), + 504 => Ok(ErrorCode::CanisterContractViolation), + 505 => Ok(ErrorCode::CanisterInvalidWasm), + 506 => Ok(ErrorCode::CanisterDidNotReply), + 507 => Ok(ErrorCode::CanisterOutOfMemory), + 508 => Ok(ErrorCode::CanisterStopped), + 509 => Ok(ErrorCode::CanisterStopping), + 510 => Ok(ErrorCode::CanisterNotStopped), + 511 => Ok(ErrorCode::CanisterStoppingCancelled), + 512 => Ok(ErrorCode::CanisterInvalidController), + 513 => Ok(ErrorCode::CanisterFunctionNotFound), + 514 => Ok(ErrorCode::CanisterNonEmpty), + 517 => Ok(ErrorCode::QueryCallGraphLoopDetected), + 520 => Ok(ErrorCode::InsufficientCyclesInCall), + 521 => Ok(ErrorCode::CanisterWasmEngineError), + 522 => Ok(ErrorCode::CanisterInstructionLimitExceeded), + 524 => Ok(ErrorCode::CanisterMemoryAccessLimitExceeded), + 525 => Ok(ErrorCode::QueryCallGraphTooDeep), + 526 => Ok(ErrorCode::QueryCallGraphTotalInstructionLimitExceeded), + 527 => Ok(ErrorCode::CompositeQueryCalledInReplicatedMode), + 528 => Ok(ErrorCode::QueryTimeLimitExceeded), + 529 => Ok(ErrorCode::QueryCallGraphInternal), + 530 => Ok(ErrorCode::InsufficientCyclesInComputeAllocation), + 531 => Ok(ErrorCode::InsufficientCyclesInMemoryAllocation), + 532 => Ok(ErrorCode::InsufficientCyclesInMemoryGrow), + 533 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryAllocation), + 534 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryGrow), + 535 => Ok(ErrorCode::InsufficientCyclesInMessageMemoryGrow), + 536 => Ok(ErrorCode::CanisterMethodNotFound), + 537 => Ok(ErrorCode::CanisterWasmModuleNotFound), + 538 => Ok(ErrorCode::CanisterAlreadyInstalled), + 539 => Ok(ErrorCode::CanisterWasmMemoryLimitExceeded), + 540 => Ok(ErrorCode::ReservedCyclesLimitIsTooLow), + // 6xx -- `RejectCode::SysUnknown` + 601 => Ok(ErrorCode::DeadlineExpired), + 602 => Ok(ErrorCode::ResponseDropped), + _ => Err(InvalidErrorCode(code)), + } + } +} + +/// Get an [`ErrorCode`] from a [`RejectCode`]. +/// +/// Currently, there is no system API to get the error code. +/// This function is a temporary workaround. +/// We set the error code to the first code in the corresponding reject code group. +/// For example, the reject code `SysFatal` (1) is mapped to the error code `SubnetOversubscribed` (101). +fn reject_to_error(reject_code: RejectCode) -> ErrorCode { + match reject_code { + RejectCode::SysFatal => ErrorCode::SubnetOversubscribed, + RejectCode::SysTransient => ErrorCode::CanisterQueueFull, + RejectCode::DestinationInvalid => ErrorCode::CanisterNotFound, + RejectCode::CanisterReject => ErrorCode::InsufficientMemoryAllocation, + RejectCode::CanisterError => ErrorCode::CanisterTrapped, + RejectCode::SysUnknown => ErrorCode::DeadlineExpired, + } +} + /// The error type for inter-canister calls and decoding the response. #[derive(thiserror::Error, Debug, Clone)] pub enum CallError { @@ -111,6 +294,13 @@ pub struct SystemError { /// When the call was rejected synchronously (`ic0.call_preform` returns non-zero code), /// this message is set to a fixed string ("failed to enqueue the call"). pub reject_message: String, + /// See [`ErrorCode`]. + /// + /// # Note + /// + /// As of the current version of the IC, the error codes are not available in the system API. + /// Please DO NOT rely on the error codes until they are officially supported. + pub error_code: ErrorCode, /// Whether the call was rejected synchronously (`ic0.call_perform` returned non-zero code) /// or asynchronously (IC rejects the call after it was enqueued). pub sync: bool, @@ -513,6 +703,7 @@ impl> Future for CallFuture { let result = Err(SystemError { reject_code, reject_message: "failed to enqueue the call".to_string(), + error_code: reject_to_error(reject_code), sync: true, }); state.result = Some(result.clone()); @@ -546,6 +737,7 @@ unsafe extern "C" fn callback>(state_ptr: *const RwLock>( Err(SystemError { reject_code, reject_message: "failed to enqueue the call".to_string(), + error_code: reject_to_error(reject_code), sync: true, }) } From c10db2c4a784e42ee6b6a08bda18a203083c1c7d Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Jan 2025 10:22:13 -0500 Subject: [PATCH 08/12] Clarify 0 timeout for change_timeout() --- ic-cdk/src/call.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index 7869a5e9..c88a3614 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -412,6 +412,16 @@ pub trait ConfigurableCall { /// If invoked multiple times, the last value takes effect. /// If [`with_guaranteed_response`](ConfigurableCall::with_guaranteed_response) is invoked after this method, /// the timeout will be ignored. + /// + /// # Note + /// + /// A timeout of 0 second DOES NOT mean guranteed response. + /// The call would most likely time out (result in a `SysUnknown` reject). + /// Unless it's a call to the canister on the same subnet, + /// and the execution manages to schedule both the request and the response in the same round. + /// + /// To make the call with a guaranteed response, + /// use the [`with_guaranteed_response`](ConfigurableCall::with_guaranteed_response) method. fn change_timeout(self, timeout_seconds: u32) -> Self; } From a481e62ca67b577ab47cda14bf6b859a62517c87 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Jan 2025 10:25:21 -0500 Subject: [PATCH 09/12] fix clippy --- ic-cdk-timers/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 85c79813..6af828b3 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -292,5 +292,5 @@ extern "C" fn timer_executor() { } } } - ic_cdk::api::msg_reply(&[]); + ic_cdk::api::msg_reply([]); } From 4d828c366d6a3c56260cf5baf1f019e71ebb040c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Jan 2025 12:08:25 -0500 Subject: [PATCH 10/12] simplify retry_later logic --- ic-cdk-timers/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 6af828b3..46a078b1 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -134,11 +134,7 @@ extern "C" fn global_timer() { let task_id = timer.task; if let Err(e) = res { ic_cdk::println!("[ic-cdk-timers] canister_global_timer: {e:?}"); - let mut retry_later = false; if e.reject_code == RejectCode::SysTransient { - retry_later = true; - } - if retry_later { // Try to execute the timer again later. TIMERS.with(|timers| { timers.borrow_mut().push(timer); From 47caeb0cbd15d93627f717895bdb60a6c6ebaf6f Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Jan 2025 13:54:49 -0500 Subject: [PATCH 11/12] rename SystemError to CallRejected and make fields private --- ic-cdk-timers/src/lib.rs | 2 +- ic-cdk/src/call.rs | 235 +++++---------------------------------- 2 files changed, 31 insertions(+), 206 deletions(-) diff --git a/ic-cdk-timers/src/lib.rs b/ic-cdk-timers/src/lib.rs index 46a078b1..26e2eb0c 100644 --- a/ic-cdk-timers/src/lib.rs +++ b/ic-cdk-timers/src/lib.rs @@ -134,7 +134,7 @@ extern "C" fn global_timer() { let task_id = timer.task; if let Err(e) = res { ic_cdk::println!("[ic-cdk-timers] canister_global_timer: {e:?}"); - if e.reject_code == RejectCode::SysTransient { + if e.reject_code() == RejectCode::SysTransient { // Try to execute the timer again later. TIMERS.with(|timers| { timers.borrow_mut().push(timer); diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index c88a3614..94636b68 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -80,189 +80,6 @@ impl PartialEq for RejectCode { } } -/// The error codes provide additional details for rejected messages. -/// -/// See [Error codes](https://internetcomputer.org/docs/current/references/ic-interface-spec/#error-codes) for more details. -/// -/// # Note -/// -/// As of the current version of the IC, the error codes are not available in the system API. -/// There is a plan to add them in the short term. -/// To avoid breaking changes at that time, the [`SystemError`] struct start to include the error code. -/// Please DO NOT rely on the error codes until they are officially supported. -// -// The variants and their codes below are from [pocket-ic](https://docs.rs/pocket-ic/latest/pocket_ic/enum.ErrorCode.html). -#[allow(missing_docs)] -#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum ErrorCode { - // 1xx -- `RejectCode::SysFatal` - SubnetOversubscribed = 101, - MaxNumberOfCanistersReached = 102, - // 2xx -- `RejectCode::SysTransient` - CanisterQueueFull = 201, - IngressMessageTimeout = 202, - CanisterQueueNotEmpty = 203, - IngressHistoryFull = 204, - CanisterIdAlreadyExists = 205, - StopCanisterRequestTimeout = 206, - CanisterOutOfCycles = 207, - CertifiedStateUnavailable = 208, - CanisterInstallCodeRateLimited = 209, - CanisterHeapDeltaRateLimited = 210, - // 3xx -- `RejectCode::DestinationInvalid` - CanisterNotFound = 301, - CanisterSnapshotNotFound = 305, - // 4xx -- `RejectCode::CanisterReject` - InsufficientMemoryAllocation = 402, - InsufficientCyclesForCreateCanister = 403, - SubnetNotFound = 404, - CanisterNotHostedBySubnet = 405, - CanisterRejectedMessage = 406, - UnknownManagementMessage = 407, - InvalidManagementPayload = 408, - // 5xx -- `RejectCode::CanisterError` - CanisterTrapped = 502, - CanisterCalledTrap = 503, - CanisterContractViolation = 504, - CanisterInvalidWasm = 505, - CanisterDidNotReply = 506, - CanisterOutOfMemory = 507, - CanisterStopped = 508, - CanisterStopping = 509, - CanisterNotStopped = 510, - CanisterStoppingCancelled = 511, - CanisterInvalidController = 512, - CanisterFunctionNotFound = 513, - CanisterNonEmpty = 514, - QueryCallGraphLoopDetected = 517, - InsufficientCyclesInCall = 520, - CanisterWasmEngineError = 521, - CanisterInstructionLimitExceeded = 522, - CanisterMemoryAccessLimitExceeded = 524, - QueryCallGraphTooDeep = 525, - QueryCallGraphTotalInstructionLimitExceeded = 526, - CompositeQueryCalledInReplicatedMode = 527, - QueryTimeLimitExceeded = 528, - QueryCallGraphInternal = 529, - InsufficientCyclesInComputeAllocation = 530, - InsufficientCyclesInMemoryAllocation = 531, - InsufficientCyclesInMemoryGrow = 532, - ReservedCyclesLimitExceededInMemoryAllocation = 533, - ReservedCyclesLimitExceededInMemoryGrow = 534, - InsufficientCyclesInMessageMemoryGrow = 535, - CanisterMethodNotFound = 536, - CanisterWasmModuleNotFound = 537, - CanisterAlreadyInstalled = 538, - CanisterWasmMemoryLimitExceeded = 539, - ReservedCyclesLimitIsTooLow = 540, - // 6xx -- `RejectCode::SysUnknown` - DeadlineExpired = 601, - ResponseDropped = 602, -} - -/// Error type for [`ErrorCode`] conversion. -/// -/// An error code is invalid if it is not one of the known error codes. -#[derive(Clone, Copy, Debug)] -pub struct InvalidErrorCode(pub u32); - -impl std::fmt::Display for InvalidErrorCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "invalid error code: {}", self.0) - } -} - -impl Error for InvalidErrorCode {} - -impl TryFrom for ErrorCode { - type Error = InvalidErrorCode; - fn try_from(code: u32) -> Result { - match code { - // 1xx -- `RejectCode::SysFatal` - 101 => Ok(ErrorCode::SubnetOversubscribed), - 102 => Ok(ErrorCode::MaxNumberOfCanistersReached), - // 2xx -- `RejectCode::SysTransient` - 201 => Ok(ErrorCode::CanisterQueueFull), - 202 => Ok(ErrorCode::IngressMessageTimeout), - 203 => Ok(ErrorCode::CanisterQueueNotEmpty), - 204 => Ok(ErrorCode::IngressHistoryFull), - 205 => Ok(ErrorCode::CanisterIdAlreadyExists), - 206 => Ok(ErrorCode::StopCanisterRequestTimeout), - 207 => Ok(ErrorCode::CanisterOutOfCycles), - 208 => Ok(ErrorCode::CertifiedStateUnavailable), - 209 => Ok(ErrorCode::CanisterInstallCodeRateLimited), - 210 => Ok(ErrorCode::CanisterHeapDeltaRateLimited), - // 3xx -- `RejectCode::DestinationInvalid` - 301 => Ok(ErrorCode::CanisterNotFound), - 305 => Ok(ErrorCode::CanisterSnapshotNotFound), - // 4xx -- `RejectCode::CanisterReject` - 402 => Ok(ErrorCode::InsufficientMemoryAllocation), - 403 => Ok(ErrorCode::InsufficientCyclesForCreateCanister), - 404 => Ok(ErrorCode::SubnetNotFound), - 405 => Ok(ErrorCode::CanisterNotHostedBySubnet), - 406 => Ok(ErrorCode::CanisterRejectedMessage), - 407 => Ok(ErrorCode::UnknownManagementMessage), - 408 => Ok(ErrorCode::InvalidManagementPayload), - // 5xx -- `RejectCode::CanisterError` - 502 => Ok(ErrorCode::CanisterTrapped), - 503 => Ok(ErrorCode::CanisterCalledTrap), - 504 => Ok(ErrorCode::CanisterContractViolation), - 505 => Ok(ErrorCode::CanisterInvalidWasm), - 506 => Ok(ErrorCode::CanisterDidNotReply), - 507 => Ok(ErrorCode::CanisterOutOfMemory), - 508 => Ok(ErrorCode::CanisterStopped), - 509 => Ok(ErrorCode::CanisterStopping), - 510 => Ok(ErrorCode::CanisterNotStopped), - 511 => Ok(ErrorCode::CanisterStoppingCancelled), - 512 => Ok(ErrorCode::CanisterInvalidController), - 513 => Ok(ErrorCode::CanisterFunctionNotFound), - 514 => Ok(ErrorCode::CanisterNonEmpty), - 517 => Ok(ErrorCode::QueryCallGraphLoopDetected), - 520 => Ok(ErrorCode::InsufficientCyclesInCall), - 521 => Ok(ErrorCode::CanisterWasmEngineError), - 522 => Ok(ErrorCode::CanisterInstructionLimitExceeded), - 524 => Ok(ErrorCode::CanisterMemoryAccessLimitExceeded), - 525 => Ok(ErrorCode::QueryCallGraphTooDeep), - 526 => Ok(ErrorCode::QueryCallGraphTotalInstructionLimitExceeded), - 527 => Ok(ErrorCode::CompositeQueryCalledInReplicatedMode), - 528 => Ok(ErrorCode::QueryTimeLimitExceeded), - 529 => Ok(ErrorCode::QueryCallGraphInternal), - 530 => Ok(ErrorCode::InsufficientCyclesInComputeAllocation), - 531 => Ok(ErrorCode::InsufficientCyclesInMemoryAllocation), - 532 => Ok(ErrorCode::InsufficientCyclesInMemoryGrow), - 533 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryAllocation), - 534 => Ok(ErrorCode::ReservedCyclesLimitExceededInMemoryGrow), - 535 => Ok(ErrorCode::InsufficientCyclesInMessageMemoryGrow), - 536 => Ok(ErrorCode::CanisterMethodNotFound), - 537 => Ok(ErrorCode::CanisterWasmModuleNotFound), - 538 => Ok(ErrorCode::CanisterAlreadyInstalled), - 539 => Ok(ErrorCode::CanisterWasmMemoryLimitExceeded), - 540 => Ok(ErrorCode::ReservedCyclesLimitIsTooLow), - // 6xx -- `RejectCode::SysUnknown` - 601 => Ok(ErrorCode::DeadlineExpired), - 602 => Ok(ErrorCode::ResponseDropped), - _ => Err(InvalidErrorCode(code)), - } - } -} - -/// Get an [`ErrorCode`] from a [`RejectCode`]. -/// -/// Currently, there is no system API to get the error code. -/// This function is a temporary workaround. -/// We set the error code to the first code in the corresponding reject code group. -/// For example, the reject code `SysFatal` (1) is mapped to the error code `SubnetOversubscribed` (101). -fn reject_to_error(reject_code: RejectCode) -> ErrorCode { - match reject_code { - RejectCode::SysFatal => ErrorCode::SubnetOversubscribed, - RejectCode::SysTransient => ErrorCode::CanisterQueueFull, - RejectCode::DestinationInvalid => ErrorCode::CanisterNotFound, - RejectCode::CanisterReject => ErrorCode::InsufficientMemoryAllocation, - RejectCode::CanisterError => ErrorCode::CanisterTrapped, - RejectCode::SysUnknown => ErrorCode::DeadlineExpired, - } -} - /// The error type for inter-canister calls and decoding the response. #[derive(thiserror::Error, Debug, Clone)] pub enum CallError { @@ -270,7 +87,7 @@ pub enum CallError { /// /// Please handle the error by matching on the rejection code. #[error("The call was rejected with code {0:?}")] - CallRejected(SystemError), + CallRejected(CallRejected), /// The response could not be decoded. /// @@ -283,31 +100,42 @@ pub enum CallError { /// The error type for inter-canister calls. #[derive(Debug, Clone)] -pub struct SystemError { - /// See [`RejectCode`]. - pub reject_code: RejectCode, - /// The reject message. +pub struct CallRejected { + // All fields are private so we will be able to change the implementation without breaking the API. + // Once we have `ic0.msg_error_code` system API, we will only store the error_code in this struct. + // It will still be possible to get the [`RejectCode`] using the public getter, + // becuase every error_code can map to a [`RejectCode`]. + reject_code: RejectCode, + reject_message: String, + sync: bool, +} + +impl CallRejected { + /// Returns the [`RejectCode`]. + pub fn reject_code(&self) -> RejectCode { + self.reject_code + } + + /// Returns the reject message. /// /// When the call was rejected asynchronously (IC rejects the call after it was enqueued), /// this message is set with [`msg_reject`](crate::api::msg_reject). /// /// When the call was rejected synchronously (`ic0.call_preform` returns non-zero code), /// this message is set to a fixed string ("failed to enqueue the call"). - pub reject_message: String, - /// See [`ErrorCode`]. - /// - /// # Note - /// - /// As of the current version of the IC, the error codes are not available in the system API. - /// Please DO NOT rely on the error codes until they are officially supported. - pub error_code: ErrorCode, - /// Whether the call was rejected synchronously (`ic0.call_perform` returned non-zero code) + pub fn reject_message(&self) -> &str { + &self.reject_message + } + + /// Returns whether the call was rejected synchronously (`ic0.call_perform` returned non-zero code) /// or asynchronously (IC rejects the call after it was enqueued). - pub sync: bool, + pub fn is_sync(&self) -> bool { + self.sync + } } /// Result of a inter-canister call. -pub type SystemResult = Result; +pub type SystemResult = Result; /// Result of a inter-canister call and decoding the response. pub type CallResult = Result; @@ -710,10 +538,9 @@ impl> Future for CallFuture { } _ => { let reject_code = RejectCode::try_from(code).unwrap(); - let result = Err(SystemError { + let result = Err(CallRejected { reject_code, reject_message: "failed to enqueue the call".to_string(), - error_code: reject_to_error(reject_code), sync: true, }); state.result = Some(result.clone()); @@ -744,10 +571,9 @@ unsafe extern "C" fn callback>(state_ptr: *const RwLock Ok(msg_arg_data()), code => { let reject_code = RejectCode::try_from(code).unwrap(); - Err(SystemError { + Err(CallRejected { reject_code, reject_message: msg_reject_msg(), - error_code: reject_to_error(reject_code), sync: false, }) } @@ -863,10 +689,9 @@ fn call_oneway_internal>( 0 => Ok(()), _ => { let reject_code = RejectCode::try_from(code).unwrap(); - Err(SystemError { + Err(CallRejected { reject_code, reject_message: "failed to enqueue the call".to_string(), - error_code: reject_to_error(reject_code), sync: true, }) } From e25bcf066c12470c996c62f024c5df67ea080523 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Jan 2025 14:10:56 -0500 Subject: [PATCH 12/12] fix typo Co-authored-by: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> --- ic-cdk/src/call.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ic-cdk/src/call.rs b/ic-cdk/src/call.rs index 94636b68..bd924568 100644 --- a/ic-cdk/src/call.rs +++ b/ic-cdk/src/call.rs @@ -104,7 +104,7 @@ pub struct CallRejected { // All fields are private so we will be able to change the implementation without breaking the API. // Once we have `ic0.msg_error_code` system API, we will only store the error_code in this struct. // It will still be possible to get the [`RejectCode`] using the public getter, - // becuase every error_code can map to a [`RejectCode`]. + // because every error_code can map to a [`RejectCode`]. reject_code: RejectCode, reject_message: String, sync: bool,