From 79772d311e6baa580500e83426e92d66630a40a9 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 16 Jun 2026 19:58:41 +0200 Subject: [PATCH] chore: migrate rust/periodic_tasks to icp-cli Replace dfx.json with icp.yaml using @dfinity/rust@v3.3.0 for both the timer and heartbeat canisters, update ic-cdk to 0.20 and ic-cdk-timers to 1.0 with the new macro syntax, add polling-based Makefile tests, update README to use icp-cli commands, add CI workflow, and remove dfx.json, BUILD.md and the per-example .devcontainer. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/periodic_tasks.yml | 28 + .../.devcontainer/devcontainer.json | 20 - rust/periodic_tasks/BUILD.md | 113 ---- rust/periodic_tasks/Cargo.lock | 484 ++++++++++-------- rust/periodic_tasks/Makefile | 82 +-- rust/periodic_tasks/README.md | 254 ++------- rust/periodic_tasks/dfx.json | 17 - rust/periodic_tasks/heartbeat/Cargo.toml | 5 +- rust/periodic_tasks/heartbeat/src/lib.rs | 22 +- rust/periodic_tasks/icp.yaml | 16 + rust/periodic_tasks/rust-toolchain.toml | 2 - rust/periodic_tasks/timer/Cargo.toml | 8 +- rust/periodic_tasks/timer/src/lib.rs | 20 +- 13 files changed, 410 insertions(+), 661 deletions(-) create mode 100644 .github/workflows/periodic_tasks.yml delete mode 100644 rust/periodic_tasks/.devcontainer/devcontainer.json delete mode 100644 rust/periodic_tasks/BUILD.md delete mode 100644 rust/periodic_tasks/dfx.json create mode 100644 rust/periodic_tasks/icp.yaml diff --git a/.github/workflows/periodic_tasks.yml b/.github/workflows/periodic_tasks.yml new file mode 100644 index 000000000..c3c47e94b --- /dev/null +++ b/.github/workflows/periodic_tasks.yml @@ -0,0 +1,28 @@ +name: periodic_tasks + +on: + push: + branches: [master] + pull_request: + paths: + - rust/periodic_tasks/** + - .github/workflows/periodic_tasks.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust-periodic_tasks: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.0 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: rust/periodic_tasks + run: | + icp network start -d + icp deploy + make test diff --git a/rust/periodic_tasks/.devcontainer/devcontainer.json b/rust/periodic_tasks/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/rust/periodic_tasks/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/rust/periodic_tasks/BUILD.md b/rust/periodic_tasks/BUILD.md deleted file mode 100644 index 24cfcb754..000000000 --- a/rust/periodic_tasks/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 20 minutes before they are removed. The command-line tool `dfx` can be used to continue building your ICP Ninja project locally and deploy it to the mainnet. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 1. Install developer tools. - -You can install the developer tools natively or use Dev Containers. - -#### Option 1: Natively install developer tools - -> Installing `dfx` natively is currently only supported on macOS and Linux systems. On Windows, it is recommended to use the Dev Containers option. - -1. Install `dfx` with the following command: - -``` - -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" - -``` - -> On Apple Silicon (e.g., Apple M1 chip), make sure you have Rosetta installed (`softwareupdate --install-rosetta`). - -2. [Install NodeJS](https://nodejs.org/en/download/package-manager). - -3. For Rust projects, you will also need to: - -- Install [Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo): `curl https://sh.rustup.rs -sSf | sh` - -- Install [candid-extractor](https://crates.io/crates/candid-extractor): `cargo install candid-extractor` - -4. For Motoko projects, you will also need to: - -- Install the Motoko package manager [Mops](https://docs.mops.one/quick-start#2-install-mops-cli): `npm i -g ic-mops` - -Lastly, navigate into your project's directory that you downloaded from ICP Ninja. - -#### Option 2: Dev Containers - -Continue building your projects locally by installing the [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code and [Docker](https://docs.docker.com/engine/install/). - -Make sure Docker is running, then navigate into your project's directory that you downloaded from ICP Ninja and start the Dev Container by selecting `Dev-Containers: Reopen in Container` in VS Code's command palette (F1 or Ctrl/Cmd+Shift+P). - -> Note that local development ports (e.g. the ports used by `dfx` or `vite`) are forwarded from the Dev Container to your local machine. In the VS code terminal, use Ctrl/Cmd+Click on the displayed local URLs to open them in your browser. To view the current port mappings, click the "Ports" tab in the VS Code terminal window. - -### 2. Start the local development environment. - -``` -dfx start --background -``` - -### 3. Create a local developer identity. - -To manage your project's canisters, it is recommended that you create a local [developer identity](https://internetcomputer.org/docs/building-apps/getting-started/identities) rather than use the `dfx` default identity that is not stored securely. - -To create a new identity, run the commands: - -``` - -dfx identity new IDENTITY_NAME - -dfx identity use IDENTITY_NAME - -``` - -Replace `IDENTITY_NAME` with your preferred identity name. The first command `dfx start --background` starts the local `dfx` processes, then `dfx identity new` will create a new identity and return your identity's seed phase. Be sure to save this in a safe, secure location. - -The third command `dfx identity use` will tell `dfx` to use your new identity as the active identity. Any canister smart contracts created after running `dfx identity use` will be owned and controlled by the active identity. - -Your identity will have a principal ID associated with it. Principal IDs are used to identify different entities on ICP, such as users and canisters. - -[Learn more about ICP developer identities](https://internetcomputer.org/docs/building-apps/getting-started/identities). - -### 4. Deploy the project locally. - -Deploy your project to your local developer environment with: - -``` -npm install -dfx deploy - -``` - -Your project will be hosted on your local machine. The local canister URLs for your project will be shown in the terminal window as output of the `dfx deploy` command. You can open these URLs in your web browser to view the local instance of your project. - -### 5. Obtain cycles. - -To deploy your project to the mainnet for long-term public accessibility, first you will need [cycles](https://internetcomputer.org/docs/building-apps/getting-started/tokens-and-cycles). Cycles are used to pay for the resources your project uses on the mainnet, such as storage and compute. - -> This cost model is known as ICP's [reverse gas model](https://internetcomputer.org/docs/building-apps/essentials/gas-cost), where developers pay for their project's gas fees rather than users pay for their own gas fees. This model provides an enhanced end user experience since they do not need to hold tokens or sign transactions when using a dapp deployed on ICP. - -> Learn how much a project may cost by using the [pricing calculator](https://internetcomputer.org/docs/building-apps/essentials/cost-estimations-and-examples). - -Cycles can be obtained through [converting ICP tokens into cycles using `dfx`](https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-cycles#dfx-cycles-convert). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -After your project has been deployed to the mainnet, it will continuously require cycles to pay for the resources it uses. You will need to [top up](https://internetcomputer.org/docs/building-apps/canister-management/topping-up) your project's canisters or set up automatic cycles management through a service such as [CycleOps](https://cycleops.dev/). - -> If your project's canisters run out of cycles, they will be removed from the network. - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/rust/periodic_tasks/Cargo.lock b/rust/periodic_tasks/Cargo.lock index 68c4dd3ca..860fdd3f0 100644 --- a/rust/periodic_tasks/Cargo.lock +++ b/rust/periodic_tasks/Cargo.lock @@ -1,12 +1,21 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +dependencies = [ + "object", +] [[package]] name = "arrayvec" @@ -16,9 +25,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "binread" @@ -60,9 +69,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "candid" -version = "0.10.0" +version = "0.10.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2525ab7a58543c132da8c780abe3aa1ba394ddcc1888a4ad2ba4f5100906ebe" +checksum = "f8f781afa4a1303e3eab4ada0720a874942bcfa936ce01b816ac6378945c43a9" dependencies = [ "anyhow", "binread", @@ -78,174 +87,126 @@ dependencies = [ "serde", "serde_bytes", "stacker", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "candid_derive" -version = "0.6.5" +version = "0.10.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970c220da8aa2fa6f7ef5dbbf3ea5b620a59eb3ac107cfb95ae8c6eebdfb7a08" +checksum = "ad6ae8e7944dd0035651bc0e7b3a3e4cb16f5fc43f8ae4fd76b36ff2cd52759f" dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.118", ] [[package]] name = "cc" -version = "1.0.83" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ - "libc", + "find-msvc-tools", + "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] [[package]] -name = "data-encoding" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" - -[[package]] -name = "digest" -version = "0.10.7" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - -[[package]] -name = "futures" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "darling_core", + "darling_macro", ] [[package]] -name = "futures-channel" -version = "0.3.29" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "futures-core", - "futures-sink", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", ] [[package]] -name = "futures-core" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" - -[[package]] -name = "futures-executor" -version = "0.3.29" +name = "darling_macro" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "darling_core", + "quote", + "syn 2.0.118", ] [[package]] -name = "futures-io" -version = "0.3.29" +name = "data-encoding" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] -name = "futures-macro" -version = "0.3.29" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", + "block-buffer", + "crypto-common", ] [[package]] -name = "futures-sink" -version = "0.3.29" +name = "either" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] -name = "futures-task" -version = "0.3.29" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" - -[[package]] -name = "futures-util" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "generic-array" @@ -259,13 +220,18 @@ dependencies = [ [[package]] name = "heartbeat" -version = "1.0.0" +version = "0.1.0" dependencies = [ "candid", "ic-cdk", - "ic-cdk-macros", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -274,97 +240,121 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "ic-cdk" -version = "0.12.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ec8231f413b8a4d74b99d7df26d6e917d6528a6245abde27f251210dcf9b72" +checksum = "6a7971f4983db147afbbc4e7f87f60b09fcd60ac707af37ff3d2468dcddbf551" dependencies = [ "candid", + "ic-cdk-executor", "ic-cdk-macros", + "ic-error-types", "ic0", + "pin-project-lite", "serde", - "serde_bytes", + "thiserror 2.0.18", +] + +[[package]] +name = "ic-cdk-executor" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33716b730ded33690b8a704bff3533fda87d229e58046823647d28816e9bcee7" +dependencies = [ + "ic0", + "slotmap", + "smallvec", ] [[package]] name = "ic-cdk-macros" -version = "0.8.2" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba23aedf7b8f89201dd778ad1bc8a04c35e3fda6fa6402f51da2cd9bb21045e6" +checksum = "a7c20c002200c720958f321bb78b4d4987fc2c380bf9ef69ed4404730810f45f" dependencies = [ "candid", + "darling", "proc-macro2", "quote", - "serde", - "serde_tokenstream", - "syn 1.0.109", + "syn 2.0.118", ] [[package]] name = "ic-cdk-timers" -version = "0.6.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c43b9706fef3ad10c4192a14801d16bd9539068239f0f06f257857441364329" +checksum = "6852b9c1d4a82ff50fc7318599298aee8bfb082bd7e9fe7e5c1420692b2170f7" dependencies = [ - "futures", - "ic-cdk", + "ic-cdk-executor", "ic0", - "serde", - "serde_bytes", "slotmap", ] +[[package]] +name = "ic-error-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" +dependencies = [ + "serde", + "strum", + "strum_macros", +] + [[package]] name = "ic0" -version = "0.21.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a54b5297861c651551676e8c43df805dad175cc33bc97dbd992edbbb85dcbcdf" +checksum = "c77c8932bff1f09502d0d8c079d5a206a06afe523e35e816162cf4d30b5bf80d" [[package]] name = "ic_principal" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899a4e8ddada85b91a2fe32b4dc6c0d475ef7bfef3f51cf2aecb26ee4ac4724f" +checksum = "1aea751965eaf92990be8c79c64b4f3174ff22dd3604e6696a06a494afbaba2a" dependencies = [ "crc32fast", "data-encoding", - "hex", "serde", - "serde_bytes", "sha2", - "thiserror", + "thiserror 1.0.69", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "memchr" -version = "2.6.4" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", "serde", @@ -372,46 +362,48 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "paste" -version = "1.0.14" +name = "object" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] [[package]] -name = "pin-project-lite" -version = "0.2.13" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pretty" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" dependencies = [ "arrayvec", "typed-arena", @@ -420,82 +412,83 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.21" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ + "ar_archive_writer", "cc", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.12" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", ] [[package]] -name = "serde_derive" -version = "1.0.193" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", + "serde_derive", ] [[package]] -name = "serde_tokenstream" -version = "0.1.7" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797ba1d80299b264f3aac68ab5d12e5825a561749db4df7cd7c8083900c5d4e9" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", - "serde", - "syn 1.0.109", + "quote", + "syn 2.0.118", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -503,34 +496,65 @@ dependencies = [ ] [[package]] -name = "slab" -version = "0.4.9" +name = "shlex" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "stacker" -version = "0.1.15" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ "cc", "cfg-if", "libc", "psm", - "winapi", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.118", ] [[package]] @@ -546,9 +570,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -557,31 +581,50 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.118", ] [[package]] name = "timer" -version = "1.0.0" +version = "0.1.0" dependencies = [ "candid", "ic-cdk", - "ic-cdk-macros", "ic-cdk-timers", ] @@ -593,46 +636,39 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/rust/periodic_tasks/Makefile b/rust/periodic_tasks/Makefile index 4c5ec0122..a9a7a3b56 100644 --- a/rust/periodic_tasks/Makefile +++ b/rust/periodic_tasks/Makefile @@ -1,42 +1,52 @@ -.PHONY: all -all: build +.PHONY: test -.PHONY: build -.SILENT: build -build: - dfx canister create --all - dfx build +test: + @echo "=== Test 1: Polling until timer counter is non-zero (up to 60s) ===" + @secs=0; \ + while [ $$secs -lt 60 ]; do \ + result=$$(icp canister call --query timer counter '()'); \ + echo "$$result"; \ + echo "$$result" | grep -qv '(0 : nat32)' && echo "PASS" && break; \ + sleep 3; secs=$$((secs + 3)); \ + done; \ + echo "$$result" | grep -qv '(0 : nat32)' || (echo "FAIL: timer counter did not increment within 60s" && exit 1) -.PHONY: install -.SILENT: install -install: build - dfx canister install heartbeat --argument 1 - dfx canister install timer --argument 1 + @echo "=== Test 2: Polling until heartbeat counter is non-zero (up to 60s) ===" + @secs=0; \ + while [ $$secs -lt 60 ]; do \ + result=$$(icp canister call --query heartbeat counter '()'); \ + echo "$$result"; \ + echo "$$result" | grep -qv '(0 : nat32)' && echo "PASS" && break; \ + sleep 3; secs=$$((secs + 3)); \ + done; \ + echo "$$result" | grep -qv '(0 : nat32)' || (echo "FAIL: heartbeat counter did not increment within 60s" && exit 1) -.PHONY: upgrade -.SILENT: upgrade -upgrade: build - dfx canister install heartbeat --mode=upgrade --argument 1 - dfx canister install timer --mode=upgrade --argument 1 + @echo "=== Test 3: Polling until timer cycles_used is non-zero (up to 60s) ===" + @secs=0; \ + while [ $$secs -lt 60 ]; do \ + result=$$(icp canister call --query timer cycles_used '()'); \ + echo "$$result"; \ + echo "$$result" | grep -qv '(0 : nat64)' && echo "PASS" && break; \ + sleep 3; secs=$$((secs + 3)); \ + done; \ + echo "$$result" | grep -qv '(0 : nat64)' || (echo "FAIL: timer cycles_used did not update within 60s" && exit 1) -.PHONY: deploy -.SILENT: deploy -deploy: - # Deploy the canisters and run periodic tasks with 1s interval. - dfx deploy heartbeat --argument 1 - dfx deploy timer --argument 1 + @echo "=== Test 4: Polling until heartbeat cycles_used is non-zero (up to 60s) ===" + @secs=0; \ + while [ $$secs -lt 60 ]; do \ + result=$$(icp canister call --query heartbeat cycles_used '()'); \ + echo "$$result"; \ + echo "$$result" | grep -qv '(0 : nat64)' && echo "PASS" && break; \ + sleep 3; secs=$$((secs + 3)); \ + done; \ + echo "$$result" | grep -qv '(0 : nat64)' || (echo "FAIL: heartbeat cycles_used did not update within 60s" && exit 1) -.PHONY: test -.SILENT: test -test: deploy - # Validate the counters are non-zero. - while [ "$$(dfx canister call heartbeat counter --output json)" = "0" ]; do sleep 1; done && dfx canister call heartbeat counter --output json | grep -vw "0" && echo 'PASS' - while [ "$$(dfx canister call timer counter --output json)" = "0" ]; do sleep 1; done && dfx canister call timer counter --output json | grep -vw "0" && echo 'PASS' - # Validate the cycles used for periodic tasks are non-zero. - while [ "$$(dfx canister call heartbeat cycles_used --output json)" = "\"0\"" ]; do sleep 1; done && dfx canister call heartbeat cycles_used --output json | grep -vw "0" && echo 'PASS' - while [ "$$(dfx canister call timer cycles_used --output json)" = "\"0\"" ]; do sleep 1; done && dfx canister call timer cycles_used --output json | grep -vw "0" && echo 'PASS' + @echo "=== Test 5: Timer stop returns successfully ===" + @result=$$(icp canister call timer stop '()') && \ + echo "$$result" && \ + echo "PASS" -.PHONY: clean -.SILENT: clean -clean: - rm -fr .dfx + @echo "=== Test 6: Heartbeat stop returns successfully ===" + @result=$$(icp canister call heartbeat stop '()') && \ + echo "$$result" && \ + echo "PASS" diff --git a/rust/periodic_tasks/README.md b/rust/periodic_tasks/README.md index f0acb9c2d..725d949eb 100644 --- a/rust/periodic_tasks/README.md +++ b/rust/periodic_tasks/README.md @@ -7,262 +7,92 @@ There are two ways to schedule an automatic canister execution on the IC: 1. **Timers**: one-shot or periodic canister calls with specified minimum timeout or interval. 2. **Heartbeats**: legacy periodic canister invocations with intervals close to the blockchain finalization rate (1s). Heartbeats are supported by the IC for backward compatibility and some very special use cases. Newly developed canisters should prefer using timers over the heartbeats. -This example demonstrates different ways of scheduling periodic tasks on the Internet Computer: timers and heartbeats. The example shows the difference between the two and helps to decide which method suits you the best. +This example demonstrates both scheduling approaches. It consists of two canisters, `heartbeat` and `timer`, both implementing the same functionality: schedule a periodic task to increase a counter. -The example consists of two canisters named `heartbeat` and `timer`, both implementing the same functionality: schedule a periodic task to increase a counter. +## Build and deploy from the command line -## Deploying from ICP Ninja +### Prerequisites -When viewing this project in ICP Ninja, you can deploy it directly to the mainnet for free by clicking "Run" in the upper right corner. Open this project in ICP Ninja: +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/periodic_tasks) - -## Build and deploy from the command-line - -- [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) -- Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/) - -## Example 1: heartbeats and timers - -- ### Step 1: Setup project environment - -Navigate into the folder containing the project's files and start a local instance of the replica with the command: +### Install ```sh +git clone https://github.com/dfinity/examples cd examples/rust/periodic_tasks -dfx start --clean ``` -This terminal will stay blocked, printing log messages, until the `Ctrl+C` is pressed or `dfx stop` command is run. - -Example output: +### Deploy and test ```sh -dfx start --clean -[...] -Dashboard: http://localhost:63387/_/dashboard +icp network start -d +icp deploy +make test +icp network stop ``` -- #### Step 2: Open another terminal window in the same directory: +Both canisters are deployed with an initial interval of 10 seconds. After deployment, the counters start incrementing automatically — the `make test` command polls until the counters and cycles-usage values are non-zero. -```sh -cd examples/rust/periodic_tasks -``` +## Comparing timers and heartbeats -- #### Step 3: Compile and deploy the `heartbeat` and `timer` canisters, setting the interval for periodic tasks to 10s: +### Cycles usage at 10-second intervals -```sh -dfx deploy heartbeat --argument 10 -dfx deploy timer --argument 10 -``` - -The counter will start increasing every ~10s in both canisters. - -Example output: +For periodic tasks with a 10-second interval, the `heartbeat` canister uses *more* cycles than the `timer` canister: ```sh -% dfx deploy heartbeat --argument 10 -[...] -Deployed canisters. -URLs: - Backend canister via Candid interface: - heartbeat: http://127.0.0.1/... - timer: http://127.0.0.1/... - -% dfx deploy timer --argument 10 -[...] -Deployed canisters. -URLs: - Backend canister via Candid interface: - heartbeat: http://127.0.0.1/... - timer: http://127.0.0.1/... -``` +icp canister call --query timer cycles_used '()' +# (2_112_067 : nat64) -- #### Step 4: After 10s, observe similar non-zero counters in both canisters: - -```sh -dfx canister call heartbeat counter -dfx canister call timer counter +icp canister call --query heartbeat cycles_used '()' +# (10_183_957 : nat64) ``` -Note: As the canisters deployed one by one, there might be a minor discrepancy in the counters. +Not only do timers use fewer cycles, but they are also more composable. As there is no global state or methods to export, different libraries with timers can be used in the same project. -Example output: - -```sh -% dfx canister call heartbeat counter -(8 : nat32) -% dfx canister call timer counter -(7 : nat32) -``` - -- #### Step 5: Compare the amount of cycles used to schedule the periodic task with 10s interval: - -```sh -dfx canister call heartbeat cycles_used -dfx canister call timer cycles_used -``` - -Example output: - -```sh -% dfx canister call heartbeat cycles_used -(10_183_957 : nat64) -% dfx canister call timer cycles_used -(2_112_067 : nat64) -``` - -For periodic tasks with 10 sec intervals, the `heartbeat` canister uses *more* cycles than the `timer` canister. - -Not only do timers use fewer cycles, but they are also more composable. As there is no global state or methods to export, different libraries with timers could be easily used in the same project. - -Also, timers provide isolation between the scheduling logic and the periodic task. If the periodic task fails, all the changes made by this task will be reverted, but the timers library state will be updated, i.e., the failed task will be removed from the list of timers to execute. - -For such isolation of execution and scheduling contexts, the internal timers library uses self-canister calls: +Timers also provide isolation between the scheduling logic and the periodic task. If the periodic task fails, all changes made by the task are reverted, but the timers library state is updated — the failed task is removed from the timer list. The internal timers library achieves this isolation via self-canister calls: ```rust -# This is a pseudo-code of a self call: +// Pseudo-code of the internal self-call: ic_cdk::call(ic_cdk::id(), "periodic_task", ()); ``` -Despite the [costs](https://internetcomputer.org/docs/current/developer-docs/gas-cost) associated with such self-canister calls, the timers library still uses fewer cycles than the heartbeats. - -## Example 2: Cycles usage for tasks with 1s interval - - -- ### Step 1: Setup project environment - -Navigate into the folder containing the project's files and start a local instance of the replica with the command: - -```sh -cd examples/rust/periodic_tasks -dfx start --clean -``` - -This terminal will stay blocked, printing log messages, until the `Ctrl+C` is pressed or `dfx stop` command is run. - -Example output: - -```sh -dfx start --clean -[...] -Dashboard: http://localhost:63387/_/dashboard -``` - -- #### Step 2: Open another terminal window in the same directory: +### Cycles usage at 1-second intervals -```sh -cd examples/rust/periodic_tasks -``` - -Example output: +At 1-second intervals the picture inverts: the `heartbeat` canister uses *fewer* cycles than `timer`: ```sh -dfx start --clean -[...] -Dashboard: http://localhost:63387/_/dashboard -``` +icp canister call --query timer cycles_used '()' +# (4_545_326 : nat64) -- #### Step 3:. Compile and deploy `heartbeat` and `timer` canisters, setting the interval for periodic tasks to 1s: - -```sh -dfx deploy --argument 1 heartbeat -dfx deploy --argument 1 timer +icp canister call --query heartbeat cycles_used '()' +# (2_456_567 : nat64) ``` -The counter will start increasing every second in both canisters. +Despite the `heartbeat` using fewer cycles at very high frequencies, this solution is hard to compose. If two libraries both export `canister_heartbeat`, they cannot be used in the same project. There is also no isolation: if the periodic task fails, the `canister_heartbeat` changes are reverted and the task fires again on every subsequent heartbeat. -Example output: - -```sh -% dfx deploy --argument 1 heartbeat -[...] -Deployed canisters. -URLs: - Backend canister via Candid interface: - heartbeat: http://127.0.0.1/... - timer: http://127.0.0.1/... - -% dfx deploy --argument 1 timer -[...] -Deployed canisters. -URLs: - Backend canister via Candid interface: - heartbeat: http://127.0.0.1/... - timer: http://127.0.0.1/... -``` - -- #### Step 4: After a few seconds, observe similar non-zero counters in both canisters: - -```sh -dfx canister call heartbeat counter -dfx canister call timer counter -``` - -Note: As the canisters deployed one by one, there might be a minor discrepancy in the counters. - -Example output: - -```sh -% dfx canister call heartbeat counter -(8 : nat32) -% dfx canister call timer counter -(9 : nat32) -``` - -- #### Step 5: Compare the number of cycles used to schedule the periodic task with 1s interval: - -```sh -dfx canister call heartbeat cycles_used -dfx canister call timer cycles_used -``` - -Example output: - -```sh -% dfx canister call heartbeat cycles_used -(2_456_567 : nat64) -% dfx canister call timer cycles_used -(4_545_326 : nat64) -``` - -For periodic tasks with 1 sec interval, the `heartbeat` canister uses *less* cycles than the `timer` canister. - -Despite the `heartbeat` using fewer cycles in this case, this solution is hard to compose within a big project. If there are two libraries using heartbeats internally, they won't even compile together, as they both would be trying to export the global `canister_heartbeat` method required for the heartbeats. - -Also, there is no isolation between the scheduling logic and the periodic task. If the periodic task fails, all the changes made by the task and by the `canister_heartbeat` method will be reverted. So the failed task will be executed over and over again every heartbeat. - -For such isolation of execution and scheduling contexts, the timers library uses internal self-canister calls as described in `Demo 1`. Due to the [costs](https://internetcomputer.org/docs/current/developer-docs/production/computation-and-storage-costs) associated with such self-canister calls, `timer` canister uses more cycles for very frequent periodic tasks. - -## Further learning - -1. Have a look at the locally running dashboard. The URL is at the end of the `dfx start` command: `Dashboard: http://localhost/...` -2. Check out `heartbeat` and `timer` canisters Candid user interface. The URLs are at the end of the `dfx deploy` command: `heartbeat: http://127.0.0.1/...` -3. Find which interval makes even the costs of running periodic tasks in the `timer` and `heartbeat` canisters: `dfx deploy heartbeat --argument 5 && dfx deploy timer --argument 5` +The breakeven interval — where timer and heartbeat costs are approximately equal — is around 5 seconds. ### Canister interface -The `heartbeat` and `timer` canisters provide the following interface: +Both canisters expose: -* `counter` — returns the value of the `COUNTER`, i.e. how many times the periodic task was executed (query, both canisters) -* `start_with_interval_secs` — starts a new timer with the specified interval in seconds (timer canister) -* `set_interval_secs` — sets a new interval to call the periodic task in seconds (heartbeat canister) -* `stop` — stops executing periodic task (both canisters) -* `cycles_used` — returns the number of cycles observed in the periodic task (both canisters) +- `counter` — returns how many times the periodic task has been executed (query) +- `stop` — stops the periodic task (update) +- `cycles_used` — returns cycles consumed by the periodic task (query) -Example usage: +The `timer` canister also exposes: -```sh -dfx canister call timer start_with_interval_secs 5 -``` +- `start_with_interval_secs` — starts a new timer with the given interval in seconds (update) -## Conclusion +The `heartbeat` canister also exposes: -For code composability, execution context isolation, and cost efficiency, canister developers should prefer to use timers over heartbeats. +- `set_interval_secs` — adjusts the heartbeat-check interval in seconds (update) -As shown in `Example 2`, there might be still very specific use cases for the heartbeats. Those should be considered case by case, with composability and isolation issues in mind. +## Conclusion +For code composability, execution context isolation, and cost efficiency, canister developers should prefer timers over heartbeats. Heartbeats may still be useful in very specific cases requiring sub-second periodic execution — these should be evaluated individually with composability and isolation trade-offs in mind. ## Security considerations and best practices -If you base your application on this example, we recommend you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/current/references/security/) for developing on the Internet Computer. This example may not implement all the best practices. +If you base your application on this example, we recommend you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on the Internet Computer. This example may not implement all the best practices. diff --git a/rust/periodic_tasks/dfx.json b/rust/periodic_tasks/dfx.json deleted file mode 100644 index 4c0c066b8..000000000 --- a/rust/periodic_tasks/dfx.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "canisters": { - "timer": { - "candid": "timer/timer.did", - "package": "timer", - "type": "rust", - "init_arg": "(10 : nat64)" - }, - "heartbeat": { - "candid": "heartbeat/heartbeat.did", - "package": "heartbeat", - "type": "rust", - "init_arg": "(10 : nat64)" - } - }, - "version": 1 -} diff --git a/rust/periodic_tasks/heartbeat/Cargo.toml b/rust/periodic_tasks/heartbeat/Cargo.toml index d9f3263b9..848623f5f 100644 --- a/rust/periodic_tasks/heartbeat/Cargo.toml +++ b/rust/periodic_tasks/heartbeat/Cargo.toml @@ -1,12 +1,11 @@ [package] edition = "2021" name = "heartbeat" -version = "1.0.0" +version = "0.1.0" [lib] crate-type = ["cdylib"] [dependencies] candid = "0.10" -ic-cdk = "0.12" -ic-cdk-macros = "0.8" +ic-cdk = "0.20" diff --git a/rust/periodic_tasks/heartbeat/src/lib.rs b/rust/periodic_tasks/heartbeat/src/lib.rs index 861c1cf5c..f4049d20d 100644 --- a/rust/periodic_tasks/heartbeat/src/lib.rs +++ b/rust/periodic_tasks/heartbeat/src/lib.rs @@ -59,7 +59,7 @@ fn track_cycles_used() { /// The logic to schedule periodic tasks at the specific intervals must be /// implemented manually inside the heartbeat method. Below is a simplistic /// example implementation. -#[ic_cdk_macros::heartbeat] +#[ic_cdk::heartbeat] fn heartbeat() { let time_nanos = ic_cdk::api::time(); let now = SystemTime::UNIX_EPOCH + Duration::from_nanos(time_nanos); @@ -88,17 +88,13 @@ fn heartbeat() { //////////////////////////////////////////////////////////////////////// /// Returns the `COUNTER` value. -/// -/// Example usage: `dfx canister call heartbeat counter` -#[ic_cdk_macros::query] +#[ic_cdk::query] fn counter() -> u32 { COUNTER.with(|counter| *counter.borrow()) } /// Sets a periodic tasks interval in seconds to increment the `COUNTER`. -/// -/// Example usage: `dfx canister call heartbeat set_interval_secs 10` -#[ic_cdk_macros::update] +#[ic_cdk::update] fn set_interval_secs(secs: u64) { MIN_INTERVAL_SECS.store(secs, Ordering::Relaxed); ic_cdk::println!("Heartbeat canister: Setting interval to {secs}s..."); @@ -109,18 +105,14 @@ fn set_interval_secs(secs: u64) { } /// Stops incrementing the counter by setting a huge interval. -/// -/// Example usage: `dfx canister call heartbeat stop` -#[ic_cdk_macros::update] +#[ic_cdk::update] fn stop() { // Due to the huge interval the periodic task won't be called. set_interval_secs(1_000_000_000_000); } /// Returns the amount of cycles used since the beginning of the execution. -/// -/// Example usage: `dfx canister call heartbeat cycles_used` -#[ic_cdk_macros::query] +#[ic_cdk::query] fn cycles_used() -> u64 { CYCLES_USED.load(Ordering::Relaxed) } @@ -136,14 +128,14 @@ fn cycles_used() -> u64 { /// This is special `canister_init` method which is invoked by /// the Internet Computer when the canister is installed for the first time. -#[ic_cdk_macros::init] +#[ic_cdk::init] fn init(min_interval_secs: u64) { set_interval_secs(min_interval_secs); } /// This is special `canister_post_upgrade` method which is invoked by /// the Internet Computer after the canister is upgraded to a new version. -#[ic_cdk_macros::post_upgrade] +#[ic_cdk::post_upgrade] fn post_upgrade(min_interval_secs: u64) { init(min_interval_secs); } diff --git a/rust/periodic_tasks/icp.yaml b/rust/periodic_tasks/icp.yaml new file mode 100644 index 000000000..8fd0b2ee2 --- /dev/null +++ b/rust/periodic_tasks/icp.yaml @@ -0,0 +1,16 @@ +networks: + - name: local + mode: managed + +canisters: + - name: timer + recipe: + type: "@dfinity/rust@v3.3.0" + configuration: + package: timer + + - name: heartbeat + recipe: + type: "@dfinity/rust@v3.3.0" + configuration: + package: heartbeat diff --git a/rust/periodic_tasks/rust-toolchain.toml b/rust/periodic_tasks/rust-toolchain.toml index f8bb13962..990104f05 100644 --- a/rust/periodic_tasks/rust-toolchain.toml +++ b/rust/periodic_tasks/rust-toolchain.toml @@ -1,4 +1,2 @@ [toolchain] -channel = "1.74" targets = ["wasm32-unknown-unknown"] -components = ["rustfmt", "clippy"] diff --git a/rust/periodic_tasks/timer/Cargo.toml b/rust/periodic_tasks/timer/Cargo.toml index 3838a9650..033295c3e 100644 --- a/rust/periodic_tasks/timer/Cargo.toml +++ b/rust/periodic_tasks/timer/Cargo.toml @@ -1,15 +1,13 @@ [package] edition = "2021" name = "timer" -version = "1.0.0" +version = "0.1.0" [lib] crate-type = ["cdylib"] [dependencies] candid = "0.10" -# Base Rust CDK crate provides support for heartbeats only. -ic-cdk = "0.12" +ic-cdk = "0.20" # The timers library is required for multiple and periodic timers. -ic-cdk-macros = "0.8" -ic-cdk-timers = "0.6" +ic-cdk-timers = "1.0" diff --git a/rust/periodic_tasks/timer/src/lib.rs b/rust/periodic_tasks/timer/src/lib.rs index 44f7285de..1cf71fb87 100644 --- a/rust/periodic_tasks/timer/src/lib.rs +++ b/rust/periodic_tasks/timer/src/lib.rs @@ -49,9 +49,7 @@ fn track_cycles_used() { //////////////////////////////////////////////////////////////////////// /// Returns the `COUNTER` value. -/// -/// Example usage: `dfx canister call timer counter` -#[ic_cdk_macros::query] +#[ic_cdk::query] fn counter() -> u32 { COUNTER.with(|counter| *counter.borrow()) } @@ -61,9 +59,7 @@ fn counter() -> u32 { /// /// It is implementation-defined when exactly the timer handler will be called. /// The only explicit guarantee is that it won't be called earlier. -/// -/// Example usage: `dfx canister call timer start_with_interval_secs 10` -#[ic_cdk_macros::update] +#[ic_cdk::update] fn start_with_interval_secs(secs: u64) { let secs = Duration::from_secs(secs); ic_cdk::println!("Timer canister: Starting a new timer with {secs:?} interval..."); @@ -78,9 +74,7 @@ fn start_with_interval_secs(secs: u64) { } /// Stops incrementing the counter by clearing the last timer ID. -/// -/// Example usage: `dfx canister call timer stop` -#[ic_cdk_macros::update] +#[ic_cdk::update] fn stop() { TIMER_IDS.with(|timer_ids| { if let Some(timer_id) = timer_ids.borrow_mut().pop() { @@ -92,9 +86,7 @@ fn stop() { } /// Returns the amount of cycles used since the beginning of the execution. -/// -/// Example usage: `dfx canister call timer cycles_used` -#[ic_cdk_macros::query] +#[ic_cdk::query] fn cycles_used() -> u64 { CYCLES_USED.load(Ordering::Relaxed) } @@ -105,7 +97,7 @@ fn cycles_used() -> u64 { /// This is special `canister_init` method which is invoked by /// the Internet Computer when the canister is installed for the first time. -#[ic_cdk_macros::init] +#[ic_cdk::init] fn init(min_interval_secs: u64) { start_with_interval_secs(min_interval_secs); } @@ -117,7 +109,7 @@ fn init(min_interval_secs: u64) { /// The developer is responsible to track and serialize the timers into /// the stable memory in `canister_pre_upgrade` method and/or re-activate /// the timers in the `canister_post_upgrade`. -#[ic_cdk_macros::post_upgrade] +#[ic_cdk::post_upgrade] fn post_upgrade(min_interval_secs: u64) { init(min_interval_secs); }