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

Basic example for best-effort response calls #1069

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
/rust/basic_bitcoin/ @dfinity/execution
/rust/basic_dao/ @dfinity/testing-verification
/rust/basic_ethereum/ @dfinity/cross-chain-team
/rust/best-effort-response-calls/ @dfinity/ic-message-routing-owners
/rust/canister-info/ @dfinity/testing-verification
/rust/canister-snapshots/ @dfinity/execution
/rust/canister_logs/ @dfinity/execution
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/rust-best-effort-response-calls.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: rust-best-effort-response-calls
on:
push:
branches:
- master
pull_request:
paths:
- rust/best-effort-response_calls/**
- .github/workflows/provision-darwin.sh
- .github/workflows/provision-linux.sh
- .github/workflows/rust-best-effort-response-calls-example.yml
- .ic-commit
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
rust-best-effort-response-calls-darwin:
runs-on: macos-15
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Provision Darwin
run: bash .github/workflows/provision-darwin.sh
- name: Rust Parallel-Calls Darwin
run: |
pushd rust/best-effort-response-calls
dfx start --background
make test
popd
rust-best-effort-response-calls-linux:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Provision Linux
run: bash .github/workflows/provision-linux.sh
- name: Rust Parallel-Calls Linux
run: |
pushd rust/best-effort-response-calls
dfx start --background
make test
popd
5 changes: 5 additions & 0 deletions rust/best-effort-response-calls/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[workspace]
members = [
"src/ber_backend"
]
resolver = "2"
21 changes: 21 additions & 0 deletions rust/best-effort-response-calls/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.PHONY: all
all: test

.PHONY: deploy
.SILENT: deploy
build:
dfx deploy

.PHONY: test
.SILENT: test
test: build
dfx canister call ber_backend demonstrate_deadlines true | grep -E '\([1-9][0-9_]* : nat64\)' && echo 'PASS'
dfx canister call ber_backend demonstrate_deadlines false | grep '(0 : nat64)' && echo 'PASS'
dfx canister call ber_backend test_deadlines_in_composite_query | grep '(0 : nat64, 0 : nat64)' && echo 'PASS'
dfx canister call ber_backend deadline_in_replicated_query | grep '(0 : nat64)' && echo 'PASS'
dfx canister call ber_backend demonstrate_timeouts | grep '(true)' && echo 'PASS'

.PHONY: clean
.SILENT: clean
clean:
rm -rf .dfx
61 changes: 61 additions & 0 deletions rust/best-effort-response-calls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Best-effort response calls

## Overview

These are basic examples that demonstrate how to issue and handle calls with best-effort responses.

## Prerequisites

This example requires an installation of:

- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx), version 0.24.3 or higher
- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples`

## First example: deadlines in best-effort response calls

In this example you can instruct the canister to issue a downstream call, which you can choose to be a best-effort response call, or a guaranteed response call. The canister will then return the deadline as observed by the receiver.

```bash
$ dfx start
$ dfx deploy
# "true" below indicates that the call should be a best-effort response call
$ dfx canister call ber_backend demonstrate_deadlines true
(1_634_120_000_000 : nat64)
# "false" below indicates that the call should be a guaranteed response call
$ dfx canister call ber_backend demonstrate_deadlines false
(0 : nat64)
```

The following examples demonstrate that best-effort response calls can be issued in composite and replicated queries, but the downstream canisters always observe the deadline as 0.

```bash
# Makes best-effort response calls to the downstream canister in a replicated query
# One call is made to a query method, the other to a composite query, and the observed deadlines are returned.
$ dfx canister call ber_backend test_deadlines_in_composite_query
(0 : nat64, 0 : nat64)
# Calls an update method which makes a replicated call to a query method which reports its observed deadline.
$ dfx canister call ber_backend deadline_in_replicated_query
(0 : nat64)
```
As an alternative to using `dfx canister call`, you can also use the Candid UI to interact with the canister.

## Second example: timeouts in best-effort response calls

This example shows that the system will generate a timeout response if the downstream call takes too long to respond. In this example, the `demonstrate_timeouts` function will issue a downstream call with a 1 second timeout to a `busy` method that takes 5 rounds to respond. The canister will then return true if the downstream call timed out, and false otherwise.

```bash

If `dfx` is not yet running, start it and deploy the canister:
```bash
$ dfx start
$ dfx deploy
```

Then, run:

```
$ dfx canister call ber_backend demonstrate_timeouts
(true)
```

The above command returning true indicates that the downstream call timed out.
18 changes: 18 additions & 0 deletions rust/best-effort-response-calls/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"dfx": "0.24.3",
"canisters": {
"ber_backend": {
"candid": "src/ber_backend/ber_backend.did",
"package": "ber_backend",
"type": "rust"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
14 changes: 14 additions & 0 deletions rust/best-effort-response-calls/src/ber_backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "ber_backend"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
candid = "0.10"
ic-cdk = { git = "https://github.com/dfinity/cdk-rs.git", rev = "c2ba198447deb422b16c2034274b7416b6e0134a", package = "ic-cdk" }
ic0 = { git = "https://github.com/dfinity/cdk-rs.git", rev = "c2ba198447deb422b16c2034274b7416b6e0134a", package = "ic0" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
service : {
"demonstrate_deadlines" : (bool) -> (nat64);
"demonstrate_timeouts" : () -> (bool);
"busy" : () -> (nat64);
"deadline_in_replicated_query" : () -> (nat64);
"test_deadlines_in_composite_query" : () -> (nat64, nat64) composite_query;
"deadline_in_query" : () -> (nat64) query;
"deadline_in_composite_query" : () -> (nat64) composite_query;
};
112 changes: 112 additions & 0 deletions rust/best-effort-response-calls/src/ber_backend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use ic_cdk::call::CallError;
use ic_cdk::prelude::*;
use ic0::msg_deadline as unsafe_msg_deadline;

fn msg_deadline() -> u64 {
unsafe {
unsafe_msg_deadline()
}
}

/// Demonstrates that the system accepts best-effort calls, and that the receiver observes a deadline.
///
/// Invokes another method either with a best-effort response or a guaranteed response call,
/// depending on the value of `use_best_effort_response`. Returns the deadline as observed by the
/// receiver method.
#[ic_cdk::update]
async fn demonstrate_deadlines(use_best_effort_response: bool) -> u64 {
let call_builder = Call::new(ic_cdk::api::canister_self(), "deadline_in_update").with_args(());
let call_builder = if use_best_effort_response {
call_builder.change_timeout(1)
} else {
call_builder.with_guaranteed_response()
};
let res: u64= call_builder
.call()
.await
.expect("Didn't expect the call to fail");
res
}

/// Endpoint that demonstrates that timeouts can trigger with best-effort responses.
/// It calls busy() with a timeout of 1 second, which is not enough to complete the
/// execution, triggering a SYS_UNKNOWN error on the call. We return a bool to
/// the caller to indicate whether a timeout has occured (true = yes, false = no)
#[ic_cdk::update]
async fn demonstrate_timeouts() -> bool {
let res: CallResult<u64> = Call::new(ic_cdk::api::canister_self(), "busy")
.change_timeout(1)
.with_args(((),))
.call()
.await;
match res {
Err(CallError::CallRejected(RejectCode::SysUnknown, s)) => {
ic_cdk::println!("SysUnknown: {:?}", s);
true
}
Err(e) => {
ic_cdk::println!("Unexpected error returned by the call: {:?}", e);
false
}
Ok(r) => {
ic_cdk::println!("Unexpected successful result returned by the call: {:?}", r);
false
}
}
}

/// Busy endpoint that just wastes a lot of instruction to trigger multi-round
/// execution, which in turn can trigger timeouts on best-effort response calls
/// to this endpoint
#[ic_cdk::update]
async fn busy() -> u64 {
const ROUNDS: u32 = 5;
const INSTRUCTIONS_PER_SLICE: u32 = 2_000_000_000;
const TOTAL_INSTRUCTIONS: u64 = ROUNDS as u64 * INSTRUCTIONS_PER_SLICE as u64;

while ic_cdk::api::performance_counter(0) < TOTAL_INSTRUCTIONS {}
ic_cdk::api::performance_counter(0)
}

#[ic_cdk::update]
async fn deadline_in_update() -> u64 {
msg_deadline()
}

#[ic_cdk::query]
async fn deadline_in_query() -> u64 {
msg_deadline()
}

#[ic_cdk::query(composite = true)]
async fn deadline_in_composite_query() -> u64 {
msg_deadline()
}

#[ic_cdk::query(composite = true)]
async fn test_deadlines_in_composite_query() -> (u64, u64) {
let deadline_in_query: u64 = Call::new(ic_cdk::api::canister_self(), "deadline_in_query")
.with_args(())
.change_timeout(1)
.call()
.await
.expect("Failed to call deadline_in_query");
let deadline_of_query_in_composite_query: u64 =
Call::new(ic_cdk::api::canister_self(), "deadline_in_composite_query")
.with_args(())
.change_timeout(1)
.call()
.await
.expect("Failed to call deadline_in_composite_query");
(deadline_in_query, deadline_of_query_in_composite_query)
}

#[ic_cdk::update]
async fn deadline_in_replicated_query() -> u64 {
Call::new(ic_cdk::api::canister_self(), "deadline_in_query")
.with_args(())
.change_timeout(1)
.call::<u64>()
.await
.expect("Failed to call deadline_in_query")
}
Loading