From 92eac86b9464c28eb64712601ca056cc7fdadf2a Mon Sep 17 00:00:00 2001 From: Claudio Russo Date: Wed, 26 Jul 2023 13:02:49 +0100 Subject: [PATCH] example: composite query demo (Rust + Motoko) (#587) This adds two example applications for the composite query feature, one in Rust and one in Motoko. --- .../motoko-composite-query-example.yaml | 44 +++++++ .../motoko-composite-query-skip.yaml | 17 +++ .../rust-composite_query-example.yml | 48 +++++++ .../workflows/rust-composite_query-skip.yml | 17 +++ motoko/classes/src/map/Buckets.mo | 6 +- motoko/classes/src/map/Map.mo | 2 +- motoko/composite_query/Makefile | 30 +++++ motoko/composite_query/README.md | 119 ++++++++++++++++++ motoko/composite_query/dfx.json | 14 +++ motoko/composite_query/src/map/Buckets.mo | 21 ++++ motoko/composite_query/src/map/Map.mo | 61 +++++++++ rust/composite_query/Cargo.toml | 5 + rust/composite_query/README.md | 48 +++++++ rust/composite_query/src/build.sh | 20 +++ rust/composite_query/src/data_partition.did | 4 + .../src/data_partition/Cargo.toml | 14 +++ .../composite_query/src/data_partition/lib.rs | 23 ++++ rust/composite_query/src/dfx.json | 29 +++++ rust/composite_query/src/kv_frontend.did | 6 + .../src/kv_frontend/Cargo.toml | 13 ++ rust/composite_query/src/kv_frontend/lib.rs | 116 +++++++++++++++++ 21 files changed, 653 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/motoko-composite-query-example.yaml create mode 100644 .github/workflows/motoko-composite-query-skip.yaml create mode 100644 .github/workflows/rust-composite_query-example.yml create mode 100644 .github/workflows/rust-composite_query-skip.yml create mode 100644 motoko/composite_query/Makefile create mode 100644 motoko/composite_query/README.md create mode 100644 motoko/composite_query/dfx.json create mode 100644 motoko/composite_query/src/map/Buckets.mo create mode 100644 motoko/composite_query/src/map/Map.mo create mode 100644 rust/composite_query/Cargo.toml create mode 100644 rust/composite_query/README.md create mode 100755 rust/composite_query/src/build.sh create mode 100644 rust/composite_query/src/data_partition.did create mode 100644 rust/composite_query/src/data_partition/Cargo.toml create mode 100644 rust/composite_query/src/data_partition/lib.rs create mode 100644 rust/composite_query/src/dfx.json create mode 100644 rust/composite_query/src/kv_frontend.did create mode 100644 rust/composite_query/src/kv_frontend/Cargo.toml create mode 100644 rust/composite_query/src/kv_frontend/lib.rs diff --git a/.github/workflows/motoko-composite-query-example.yaml b/.github/workflows/motoko-composite-query-example.yaml new file mode 100644 index 000000000..681646bf4 --- /dev/null +++ b/.github/workflows/motoko-composite-query-example.yaml @@ -0,0 +1,44 @@ +name: motoko-composite-query +on: + push: + branches: + - master + pull_request: + paths: + - motoko/composite_query/** + - .github/workflows/provision-darwin.sh + - .github/workflows/provision-linux.sh + - .github/workflows/motoko-composite-query-example.yaml + - .github/workflows/motoko-composite-query-skip.yaml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + motoko-composite-query-darwin: + runs-on: macos-12 + steps: + - uses: actions/checkout@v1 + - name: Provision Darwin + env: + DFX_VERSION: 0.15.0-beta.1 + run: bash .github/workflows/provision-darwin.sh + - name: Motoko Composite Query Darwin + run: | + dfx start --background + pushd motoko/composite_query + make test + popd + motoko-composite-query-linux: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v1 + - name: Provision Linux + env: + DFX_VERSION: 0.15.0-beta.1 + run: bash .github/workflows/provision-linux.sh + - name: Motoko Composite Query Linux + run: | + dfx start --background + pushd motoko/composite_query + make test + popd diff --git a/.github/workflows/motoko-composite-query-skip.yaml b/.github/workflows/motoko-composite-query-skip.yaml new file mode 100644 index 000000000..3de0d5fb6 --- /dev/null +++ b/.github/workflows/motoko-composite-query-skip.yaml @@ -0,0 +1,17 @@ +name: motoko-composite-query +on: + pull_request: + paths-ignore: + - motoko/composite_query/** + - .github/workflows/provision-darwin.sh + - .github/workflows/provision-linux.sh + - .github/workflows/motoko-composite-query-example.yaml + - .github/workflows/motoko-composite-query-skip.yaml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo 'Not needed - relevant folder not touched' diff --git a/.github/workflows/rust-composite_query-example.yml b/.github/workflows/rust-composite_query-example.yml new file mode 100644 index 000000000..855ad721b --- /dev/null +++ b/.github/workflows/rust-composite_query-example.yml @@ -0,0 +1,48 @@ +name: rust-composite_query +on: + push: + branches: + - master + pull_request: + paths: + - rust/composite_query/** + - .github/workflows/provision-darwin.sh + - .github/workflows/provision-linux.sh + - .github/workflows/rust-composite_query-example.yml + - .github/workflows/rust-composite_query-skip.yml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + rust-composite_query-darwin: + runs-on: macos-12 + steps: + - uses: actions/checkout@v1 + - name: Provision Darwin + run: bash .github/workflows/provision-darwin.sh + - name: Rust composite_query Darwin + run: | + dfx start --background + pushd rust/composite_query + dfx canister create kv_frontend + dfx build kv_frontend + dfx canister install kv_frontend + dfx canister call kv_frontend put '(1, 1337)' + dfx canister call kv_frontend get '(1)' | grep 1337 + popd + rust-composite_query-linux: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v1 + - name: Provision Linux + run: bash .github/workflows/provision-linux.sh + - name: Rust composite_query Linux + run: | + dfx start --background + pushd rust/composite_query + dfx canister create kv_frontend + dfx build kv_frontend + dfx canister install kv_frontend + dfx canister call kv_frontend put '(1, 1337)' + dfx canister call kv_frontend get '(1)' | grep 1337 + popd diff --git a/.github/workflows/rust-composite_query-skip.yml b/.github/workflows/rust-composite_query-skip.yml new file mode 100644 index 000000000..6c3078323 --- /dev/null +++ b/.github/workflows/rust-composite_query-skip.yml @@ -0,0 +1,17 @@ +name: rust-composite_query +on: + pull_request: + paths-ignore: + - rust/composite_query/** + - .github/workflows/provision-darwin.sh + - .github/workflows/provision-linux.sh + - .github/workflows/rust-composite_query-example.yml + - .github/workflows/rust-composite_query-skip.yml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo 'Not needed - relevant folder not touched' \ No newline at end of file diff --git a/motoko/classes/src/map/Buckets.mo b/motoko/classes/src/map/Buckets.mo index 0f487c893..97b25a78a 100644 --- a/motoko/classes/src/map/Buckets.mo +++ b/motoko/classes/src/map/Buckets.mo @@ -9,13 +9,13 @@ actor class Bucket(n : Nat, i : Nat) { let map = Map.RBTree(Nat.compare); public func get(k : Key) : async ?Value { - assert((k % n) == i); + assert ((k % n) == i); map.get(k); }; public func put(k : Key, v : Value) : async () { - assert((k % n) == i); - map.put(k,v); + assert ((k % n) == i); + map.put(k, v); }; }; diff --git a/motoko/classes/src/map/Map.mo b/motoko/classes/src/map/Map.mo index 5f5177b1c..7afa717f0 100644 --- a/motoko/classes/src/map/Map.mo +++ b/motoko/classes/src/map/Map.mo @@ -7,7 +7,7 @@ actor Map { let n = 4; // number of buckets // divide initial balance amongst self and buckets - let cycleShare = Cycles.balance()/(n + 1); + let cycleShare = Cycles.balance() / (n + 1); type Key = Nat; type Value = Text; diff --git a/motoko/composite_query/Makefile b/motoko/composite_query/Makefile new file mode 100644 index 000000000..c1deb5608 --- /dev/null +++ b/motoko/composite_query/Makefile @@ -0,0 +1,30 @@ +.PHONY: all +all: build + +.PHONY: build +.SILENT: build +build: + dfx canister create --all + dfx build + +.PHONY: install +.SILENT: install +install: build + dfx canister install --all + +.PHONY: upgrade +.SILENT: upgrade +upgrade: build + dfx canister install --all --mode=upgrade + +.PHONY: test +.SILENT: test +test: install + dfx canister call Map test '()' + dfx canister call --query Map get '(15)' \ + | grep '(opt "15")' && echo 'PASS' + +.PHONY: clean +.SILENT: clean +clean: + rm -fr .dfx diff --git a/motoko/composite_query/README.md b/motoko/composite_query/README.md new file mode 100644 index 000000000..31f14cb04 --- /dev/null +++ b/motoko/composite_query/README.md @@ -0,0 +1,119 @@ +# Composite Queries + +![Compatibility](https://img.shields.io/badge/compatibility-0.7.0-blue) +[![Build Status](https://github.com/dfinity/examples/workflows/motoko-classes-example/badge.svg)](https://github.com/dfinity/examples/actions?query=workflow%3Amotoko-classes-example) + +This example modifies the simple actor class example to demonstrate the implementation of composite queries. + +The original example demonstrates a simple use of actor classes, which allow a program to dynamically install new actors (that is, canisters). It also demonstrates a multi-canister project, and actors using inter-actor communication through `shared` functions. + +In the original example, shared functions `Map.get` and `Bucket.get` were both implemented as +update methods so that `Map.get` could call `Bucket.get`. + +In this version `Bucket.get` is implemented as a query function and `Map.get` as a composite query function. +Although queries and composite queries are fast, composite queries can only be invoked as ingress messages, either +using `dfx` (see below) or an agent through, for example, a browser front-end (not illustrated here). + +In detail, the example provides actor `Map`. +`Map` is a dead-simple, distributed key-value store, mapping `Nat` to `Text` values, with entries stored in a small number of separate `Bucket` actors, installed on demand. + +[Map.mo](./src/map/Map.mo) imports a Motoko _actor class_ `Bucket(i, n)` +from library [Buckets.mo](./src/map/Buckets.mo). +It also imports the `ExperimentalCycles` base library, naming it `Cycles` for short, in order to share its cycles amongst the bucket it creates. + +Each call to `Buckets.Bucket(n, i)` within `Map` instantiates a new `Bucket` instance (the `i`-th of `n`) dedicated to those entries of the `Map` whose key _hashes_ to `i` (by taking the remainder of the key modulo division by `n`). + +Each asynchronous instantiation of the `Bucket` actor class corresponds to the dynamic, programmatic installation of a new `Bucket` canister. + +Each new `Bucket` must be provisioned with enough cycles to pay for its installation and running costs. +`Map` achieves this by adding an equal share of `Map`'s initial cycle balance to each asynchronous call to `Bucket(n, i)`, using a call to `Cycles.add(cycleShare)`. + +`Map`'s `test` method simply `put`s 16 consecutive entries into `Map`. These entries are distributed evenly amongst the buckets making up the key-value store. Adding the first entry to a bucket takes longer than adding a subsequent one, since the bucket needs to be installed on first use. + + +## Security Considerations and Security 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. + +## Prerequisites + +Verify the following before running this demo: + +* You have downloaded and installed the + [DFINITY Canister SDK](https://sdk.dfinity.org). + +* You have stopped any Internet Computer or other network process that would + create a port conflict on 8000. + +## Demo + +1. Start a local internet computer. + + ```text + dfx start + ``` + + (Alternatively, the example will run faster if you use the emulator, not a full replica: + ``` + dfx start --emulator + ``` + ) + +2. Open a new terminal window. + +3. Deploy the `Map` canister: + + ```text + dfx deploy + ``` + +4. Invoke the `test` method of canister `Map` to add some entries + + ```text + dfx canister call Map test '()' + ``` + +5. Observe the following result. + + ```text + debug.print: putting: (0, "0") + debug.print: putting: (1, "1") + debug.print: putting: (2, "2") + debug.print: putting: (3, "3") + debug.print: putting: (4, "4") + debug.print: putting: (5, "5") + debug.print: putting: (6, "6") + debug.print: putting: (7, "7") + debug.print: putting: (8, "8") + debug.print: putting: (9, "9") + debug.print: putting: (10, "10") + debug.print: putting: (11, "11") + debug.print: putting: (12, "12") + debug.print: putting: (13, "13") + debug.print: putting: (14, "14") + debug.print: putting: (15, "15") + () + ``` + +6. Invoke the `get` composite query method of canister `Main` + + ```text + dfx canister call --query Map get '(15)' + ``` + +7. Observe the following result. + + ``` + (opt "15") + ``` + + + +# Links + +Specific links: + +- [Actor classes](https://internetcomputer.org/docs/current/motoko/main/actor-classes.html) +- [Managing Cycles](https://internetcomputer.org/docs/current/motoko/main/cycles.html) +- [Composite Queries](https://internetcomputer.org/docs/current/motoko/main/actors-async#composite-query-functions.html) + diff --git a/motoko/composite_query/dfx.json b/motoko/composite_query/dfx.json new file mode 100644 index 000000000..464588ee8 --- /dev/null +++ b/motoko/composite_query/dfx.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "canisters": { + "Map": { + "type": "motoko", + "main": "src/map/Map.mo" + } + }, + "defaults": { + "build": { + "packtool": "" + } + } +} diff --git a/motoko/composite_query/src/map/Buckets.mo b/motoko/composite_query/src/map/Buckets.mo new file mode 100644 index 000000000..f38683215 --- /dev/null +++ b/motoko/composite_query/src/map/Buckets.mo @@ -0,0 +1,21 @@ +import Nat "mo:base/Nat"; +import Map "mo:base/RBTree"; + +actor class Bucket(n : Nat, i : Nat) { + + type Key = Nat; + type Value = Text; + + let map = Map.RBTree(Nat.compare); + + public query func get(k : Key) : async ?Value { + assert ((k % n) == i); + map.get(k); + }; + + public func put(k : Key, v : Value) : async () { + assert ((k % n) == i); + map.put(k, v); + }; + +}; diff --git a/motoko/composite_query/src/map/Map.mo b/motoko/composite_query/src/map/Map.mo new file mode 100644 index 000000000..16eaac9bb --- /dev/null +++ b/motoko/composite_query/src/map/Map.mo @@ -0,0 +1,61 @@ +import Debug "mo:base/Debug"; +import Array "mo:base/Array"; +import Cycles "mo:base/ExperimentalCycles"; +import Buckets "Buckets"; + +actor Map { + + let n = 4; // number of buckets + + // divide initial balance amongst self and buckets + let cycleShare = Cycles.balance() / (n + 1); + + type Key = Nat; + type Value = Text; + + type Bucket = Buckets.Bucket; + + let buckets : [var ?Bucket] = Array.init(n, null); + + public func getUpdate(k : Key) : async ?Value { + switch (buckets[k % n]) { + case null null; + case (?bucket) await bucket.get(k); + }; + }; + + public composite query func get(k : Key) : async ?Value { + switch (buckets[k % n]) { + case null null; + case (?bucket) await bucket.get(k); + }; + }; + + public func put(k : Key, v : Value) : async () { + let i = k % n; + let bucket = switch (buckets[i]) { + case null { + // provision next send, i.e. Bucket(n, i), with cycles + Cycles.add(cycleShare); + let b = await Buckets.Bucket(n, i); // dynamically install a new Bucket + buckets[i] := ?b; + b; + }; + case (?bucket) bucket; + }; + await bucket.put(k, v); + }; + + public func test() : async () { + var i = 0; + while (i < 16) { + let t = debug_show(i); + assert (null == (await getUpdate(i))); + Debug.print("putting: " # debug_show(i, t)); + await Map.put(i, t); + assert (?t == (await getUpdate(i))); + i += 1; + }; + }; + +}; diff --git a/rust/composite_query/Cargo.toml b/rust/composite_query/Cargo.toml new file mode 100644 index 000000000..d2ca14827 --- /dev/null +++ b/rust/composite_query/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "src/data_partition", + "src/kv_frontend" +] \ No newline at end of file diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md new file mode 100644 index 000000000..02bc004fa --- /dev/null +++ b/rust/composite_query/README.md @@ -0,0 +1,48 @@ +# Installing + +We first need to build the data partition backend canister. + +``` +cd rust/composite_query/src +dfx start +dfx canister create kv_frontend +``` + +During compilation of the fronted canister, the canister's wasm code will be inlined in the frontend canister's wasm code. + +``` +dfx build kv_frontend +dfx canister install kv_frontend +``` + +# Using the canister + +Now we add some key value pairs to via the frontend canister. + +``` +$ dfx canister call kv_frontend put '(1:nat, 1337:nat)' +(null) +$ dfx canister call kv_frontend put '(1:nat, 42:nat)' +(opt (1_337 : nat)) +``` + +Note that the first call to `put` is slow, since the data partitions have to be created first. + +``` +$ dfx canister call kv_frontend get '(1:nat)' +(opt (42 : nat)) +``` + +We can also query it via a (duplicate) method doing update calls +``` +$ dfx canister call kv_frontend get_update '(1:nat)' +(opt (1_337 : nat)) +``` + +It's also possible to do *two* query calls, first into the frontend and then into the data partition: +``` +$ dfx canister call kv_frontend lookup '(1: nat)' +(1 : nat, "dmalx-m4aaa-aaaaa-qaanq-cai") +$ dfx canister call dmalx-m4aaa-aaaaa-qaanq-cai get '(1: nat)' --query +(1_337 : nat) +``` \ No newline at end of file diff --git a/rust/composite_query/src/build.sh b/rust/composite_query/src/build.sh new file mode 100755 index 000000000..2987253c4 --- /dev/null +++ b/rust/composite_query/src/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +BASE="../target/wasm32-unknown-unknown/release" + +echo "building data partition canister" + +cargo build --target wasm32-unknown-unknown --release -p data_partition --locked +ic-cdk-optimizer ${BASE}/data_partition.wasm --output ${BASE}/data_partition.wasm + +( + echo "compressing data partition canister" + cd ${BASE} + gzip -c data_partition.wasm > data_partition.wasm.gz +) + +echo "building kv frontend canister" +cargo build --target wasm32-unknown-unknown --release -p kv_frontend --locked +ic-cdk-optimizer ${BASE}/kv_frontend.wasm --output ./kv_frontend.wasm \ No newline at end of file diff --git a/rust/composite_query/src/data_partition.did b/rust/composite_query/src/data_partition.did new file mode 100644 index 000000000..5b0d35618 --- /dev/null +++ b/rust/composite_query/src/data_partition.did @@ -0,0 +1,4 @@ +service : { + "put" : (nat, nat) -> (opt nat); + "get" : (nat) -> (opt nat) query; +}; diff --git a/rust/composite_query/src/data_partition/Cargo.toml b/rust/composite_query/src/data_partition/Cargo.toml new file mode 100644 index 000000000..cac80e0f7 --- /dev/null +++ b/rust/composite_query/src/data_partition/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "data_partition" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +candid = "0.8.4" +ic-cdk = "0.8.1" +ic-cdk-macros = "0.6.10" +ic-stable-structures = "0.5.4" \ No newline at end of file diff --git a/rust/composite_query/src/data_partition/lib.rs b/rust/composite_query/src/data_partition/lib.rs new file mode 100644 index 000000000..c7c697ff9 --- /dev/null +++ b/rust/composite_query/src/data_partition/lib.rs @@ -0,0 +1,23 @@ +use ic_cdk_macros::{query, update}; +use ic_stable_structures::{BTreeMap, DefaultMemoryImpl}; +use std::cell::RefCell; + +thread_local! { + static STORE: RefCell> = RefCell::new( + BTreeMap::init(DefaultMemoryImpl::default())); +} + +#[update] +fn put(key: u128, value: u128) -> Option { + ic_cdk::println!("Set in backend for key={} with value={}", key, value); + STORE.with(|store| store.borrow_mut().insert(key, value)) +} + +#[query] +fn get(key: u128) -> Option { + STORE.with(|store| { + let r = store.borrow().get(&key); + ic_cdk::println!("Get in backend for key={} - result={:?}", key, r); + r + }) +} diff --git a/rust/composite_query/src/dfx.json b/rust/composite_query/src/dfx.json new file mode 100644 index 000000000..9997cd8e0 --- /dev/null +++ b/rust/composite_query/src/dfx.json @@ -0,0 +1,29 @@ +{ + "canisters": { + "data_partition": { + "candid": "data_partition.did", + "package": "data_partition", + "type": "rust" + }, + "kv_frontend": { + "candid": "kv_frontend.did", + "package": "kv_frontend", + "wasm": "kv_frontend.wasm", + "build": "./build.sh", + "type": "custom" + } + }, + "defaults": { + "build": { + "args": "", + "packtool": "" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + }, + "version": 1 +} \ No newline at end of file diff --git a/rust/composite_query/src/kv_frontend.did b/rust/composite_query/src/kv_frontend.did new file mode 100644 index 000000000..a25616310 --- /dev/null +++ b/rust/composite_query/src/kv_frontend.did @@ -0,0 +1,6 @@ +service : { + "put" : (nat, nat) -> (opt nat); + "get" : (nat) -> (opt nat) composite_query; + "get_update" : (nat) -> (opt nat); + "lookup" : (nat) -> (nat, text) query; +}; diff --git a/rust/composite_query/src/kv_frontend/Cargo.toml b/rust/composite_query/src/kv_frontend/Cargo.toml new file mode 100644 index 000000000..51cf913f3 --- /dev/null +++ b/rust/composite_query/src/kv_frontend/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "kv_frontend" +version = "1.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +candid = "0.8.4" +ic-cdk = "0.8.1" +ic-cdk-macros = "0.6.10" \ No newline at end of file diff --git a/rust/composite_query/src/kv_frontend/lib.rs b/rust/composite_query/src/kv_frontend/lib.rs new file mode 100644 index 000000000..85dfe51b3 --- /dev/null +++ b/rust/composite_query/src/kv_frontend/lib.rs @@ -0,0 +1,116 @@ +use ic_cdk::api::call::{call}; +use ic_cdk::api::management_canister::main::{CreateCanisterArgument, create_canister, InstallCodeArgument, install_code, CanisterInstallMode}; +use ic_cdk::api::management_canister::provisional::CanisterSettings; +use ic_cdk_macros::{query, update}; +use candid::Principal; + +use std::sync::Arc; +use std::sync::RwLock; + +const NUM_PARTITIONS: usize = 5; + +// Inline wasm binary of data partition canister +pub const WASM: &[u8] = + include_bytes!("../../target/wasm32-unknown-unknown/release/data_partition.wasm.gz"); + +thread_local! { + // A list of canister IDs for data partitions + static CANISTER_IDS: Arc>> = Arc::new(RwLock::new(vec![])); +} + +#[update] +async fn put(key: u128, value: u128) -> Option { + + // Create partitions if they don't exist yet + if CANISTER_IDS.with(|canister_ids| { + let canister_ids = canister_ids.read().unwrap(); + canister_ids.len() == 0 + }) { + for _ in 0..NUM_PARTITIONS { + create_data_partition_canister_from_wasm().await; + } + } + + let canister_id = get_partition_for_key(key); + ic_cdk::println!("Put in frontend for key={} .. using backend={}", key, canister_id.to_text()); + match call(canister_id, "put", (key, value), ).await { + Ok(r) => { + let (res,): (Option,) = r; + res + }, + Err(_) => None, + } +} + +#[query(composite = true)] +async fn get(key: u128) -> Option { + let canister_id = get_partition_for_key(key); + ic_cdk::println!("Get in frontend for key={} .. using backend={}", key, canister_id.to_text()); + match call(canister_id, "get", (key, ), ).await { + Ok(r) => { + let (res,): (Option,) = r; + res + }, + Err(_) => None, + } +} + +#[update] +async fn get_update(key: u128) -> Option { + let canister_id = get_partition_for_key(key); + ic_cdk::println!("Get as update in frontend for key={} .. using backend={}", key, canister_id.to_text()); + match call(canister_id, "get", (key, ), ).await { + Ok(r) => { + let (res,): (Option,) = r; + res + }, + Err(_) => None, + } +} + +fn get_partition_for_key(key: u128) -> Principal { + let canister_id = CANISTER_IDS.with(|canister_ids| { + let canister_ids = canister_ids.read().unwrap(); + canister_ids[lookup(key).0 as usize] + }); + canister_id +} + +#[query(composite = true)] +fn lookup(key: u128) -> (u128, String) { + let r = key % NUM_PARTITIONS as u128; + (r, CANISTER_IDS.with(|canister_ids| { + let canister_ids = canister_ids.read().unwrap(); + canister_ids[r as usize].to_text() + })) +} + +async fn create_data_partition_canister_from_wasm() { + let create_args = CreateCanisterArgument { + settings: Some(CanisterSettings { + controllers: Some(vec![ic_cdk::id()]), + compute_allocation: Some(0.into()), + memory_allocation: Some(0.into()), + freezing_threshold: Some(0.into()), + }) + }; + + let canister_record = create_canister(create_args).await.unwrap(); + let canister_id = canister_record.0.canister_id; + + ic_cdk::println!("Created canister {}", canister_id); + + let install_args = InstallCodeArgument { + mode: CanisterInstallMode::Install, + canister_id, + wasm_module: WASM.to_vec(), + arg: vec![], + }; + + install_code(install_args).await.unwrap(); + + CANISTER_IDS.with(|canister_ids| { + let mut canister_ids = canister_ids.write().unwrap(); + canister_ids.push(canister_id); + }); +}