Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: call module followup #549

Merged
merged 5 commits into from
Jan 17, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 46 additions & 27 deletions ic-cdk/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ use std::sync::atomic::Ordering;
use std::sync::{Arc, RwLock, Weak};
use std::task::{Context, Poll, Waker};

const CALL_PERFORM_REJECT_MESSAGE: &str = "call_perform failed";

/// 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 {
/// Fatal system error, retry unlikely to be useful.
Expand All @@ -28,34 +31,47 @@ 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
///
/// This variant is not part of the IC interface spec, and is used to represent
/// reject codes that are not recognized by the library.
///
/// This variant is needed just in case the IC introduces new reject codes in the future.
/// If that happens, a Canister using existing library versions will still be able to convert
/// the new reject codes to this variant without panicking.
Unrecognized(u32),
}

/// Error type for [`RejectCode`] conversion.
///
/// A reject code is invalid if it is not one of the known reject codes.
/// The only case where this error can occur is when trying to convert a 0 to a [`RejectCode`].
#[derive(Clone, Copy, Debug)]
pub struct InvalidRejectCode(pub u32);
pub struct ZeroIsInvalidRejectCode;

impl std::fmt::Display for InvalidRejectCode {
impl std::fmt::Display for ZeroIsInvalidRejectCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid reject code: {}", self.0)
write!(f, "zero is invalid reject code")
}
}

impl Error for InvalidRejectCode {}
impl Error for ZeroIsInvalidRejectCode {}

impl TryFrom<u32> for RejectCode {
type Error = InvalidRejectCode;
type Error = ZeroIsInvalidRejectCode;

fn try_from(code: u32) -> Result<Self, Self::Error> {
match code {
0 => Err(ZeroIsInvalidRejectCode),
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)),
_ => Ok(RejectCode::Unrecognized(code)),
}
}
}
Expand All @@ -69,6 +85,7 @@ impl From<RejectCode> for u32 {
RejectCode::CanisterReject => 4,
RejectCode::CanisterError => 5,
RejectCode::SysUnknown => 6,
RejectCode::Unrecognized(code) => code,
}
}
}
Expand Down Expand Up @@ -119,10 +136,10 @@ impl CallRejected {
/// 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).
/// this message is get from [`msg_reject_msg`].
///
/// 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").
/// this message is set to a fixed string ("call_perform failed").
pub fn reject_message(&self) -> &str {
&self.reject_message
}
Expand Down Expand Up @@ -382,10 +399,7 @@ impl SendableCall for Call<'_> {

impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T> {
async fn call_raw(self) -> SystemResult<Vec<u8>> {
// 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));
let args_raw = encode_args(self.args).unwrap_or_else(panic_when_encode_fails);
call_raw_internal(
self.call.canister_id,
self.call.method,
Expand All @@ -397,10 +411,7 @@ impl<'a, T: ArgumentEncoder + Send + Sync> SendableCall for CallWithArgs<'a, T>
}

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 =
encode_args(self.args).unwrap_or_else(|e| panic!("Failed to encode args: {}", e));
let args_raw = encode_args(self.args).unwrap_or_else(panic_when_encode_fails);
call_oneway_internal(
self.call.canister_id,
self.call.method,
Expand All @@ -413,10 +424,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) -> SystemResult<Vec<u8>> {
// 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));
let args_raw = encode_one(self.arg).unwrap_or_else(panic_when_encode_fails);
call_raw_internal(
self.call.canister_id,
self.call.method,
Expand All @@ -428,10 +436,7 @@ impl<'a, T: CandidType + Send + Sync> SendableCall for CallWithArg<'a, T> {
}

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 =
encode_one(self.arg).unwrap_or_else(|e| panic!("Failed to encode arg: {}", e));
let args_raw = encode_one(self.arg).unwrap_or_else(panic_when_encode_fails);
call_oneway_internal(
self.call.canister_id,
self.call.method,
Expand Down Expand Up @@ -537,10 +542,11 @@ impl<T: AsRef<[u8]>> Future for CallFuture<T> {
// call_perform returns 0 means the call was successfully enqueued.
}
_ => {
// SAFETY: The conversion is safe because the code is not 0.
let reject_code = RejectCode::try_from(code).unwrap();
let result = Err(CallRejected {
reject_code,
reject_message: "failed to enqueue the call".to_string(),
reject_message: CALL_PERFORM_REJECT_MESSAGE.to_string(),
sync: true,
});
state.result = Some(result.clone());
Expand Down Expand Up @@ -570,6 +576,7 @@ unsafe extern "C" fn callback<T: AsRef<[u8]>>(state_ptr: *const RwLock<CallFutur
state.write().unwrap().result = Some(match msg_reject_code() {
0 => Ok(msg_arg_data()),
code => {
// SAFETY: The conversion is safe because the code is not 0.
let reject_code = RejectCode::try_from(code).unwrap();
Err(CallRejected {
reject_code,
Expand Down Expand Up @@ -688,10 +695,11 @@ fn call_oneway_internal<T: AsRef<[u8]>>(
match code {
0 => Ok(()),
_ => {
// SAFETY: The conversion is safe because the code is not 0.
let reject_code = RejectCode::try_from(code).unwrap();
Err(CallRejected {
reject_code,
reject_message: "failed to enqueue the call".to_string(),
reject_message: CALL_PERFORM_REJECT_MESSAGE.to_string(),
sync: true,
})
}
Expand All @@ -716,3 +724,14 @@ fn call_cycles_add(cycles: u128) {
fn decoder_error_to_call_error<T>(err: candid::error::Error) -> CallError {
CallError::CandidDecodeFailed(format!("{}: {}", std::any::type_name::<T>(), err))
}

/// When args encoding fails, we panic with an informative message.
///
/// Currently, Candid encoding only fails when heap memory is exhausted,
/// in which case execution would trap before reaching the unwrap.
///
/// However, since future implementations might introduce other failure cases,
/// we provide an informative panic message for better debuggability.
fn panic_when_encode_fails(err: candid::error::Error) -> Vec<u8> {
panic!("failed to encode args: {}", err)
}