diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 1698084f..079a8e2f 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -11,6 +11,7 @@ jobs: env: DFX_VERSION: 0.7.2 IC_REPL_VERSION: 2021-06-17 + VESSEL_VERSION: v0.6.2 steps: - uses: actions/checkout@v2 - name: Install @@ -20,6 +21,9 @@ jobs: wget https://github.com/chenyan2002/ic-repl/releases/download/$IC_REPL_VERSION/ic-repl-linux64 cp ./ic-repl-linux64 /home/runner/bin/ic-repl chmod a+x /home/runner/bin/ic-repl + wget https://github.com/dfinity/vessel/releases/download/$VESSEL_VERSION/vessel-linux64 + cp ./vessel-linux64 /home/runner/bin/vessel + chmod a+x /home/runner/bin/vessel - name: Build run: | dfx cache install diff --git a/.gitignore b/.gitignore index b1adea15..cb1a6fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,9 @@ yarn-debug.log* yarn-error.log* .eslintcache +.vessel # IDEs /.idea /.vscode -/.history \ No newline at end of file +/.history diff --git a/backend/Main.mo b/backend/Main.mo index 6bfda76c..6c1b74b9 100644 --- a/backend/Main.mo +++ b/backend/Main.mo @@ -1,8 +1,6 @@ -import Iter "mo:base/Iter"; import Cycles "mo:base/ExperimentalCycles"; import Time "mo:base/Time"; import Error "mo:base/Error"; -import RBTree "mo:base/RBTree"; import Option "mo:base/Option"; import Types "./Types"; import ICType "./IC"; @@ -10,9 +8,27 @@ import ICType "./IC"; actor class Self(opt_params : ?Types.InitParams) { let IC : ICType.Self = actor "aaaaa-aa"; let params = Option.get(opt_params, Types.defaultParams); - var pool = Types.CanisterPool(params.max_num_canisters, params.TTL); + stable var stablePool : [Types.CanisterInfo] = []; + stable var previousParam : ?Types.InitParams = null; + system func preupgrade() { + stablePool := pool.share(); + previousParam := ?params; + }; + system func postupgrade() { + switch previousParam { + case (?old) { + if (old.max_num_canisters > params.max_num_canisters) { + //throw Error.reject("Cannot reduce canisterPool for upgrade"); + assert false; + }; + }; + case null {}; + }; + pool.unshare(stablePool); + }; + // TODO: only playground frontend can call these functions public func getCanisterId() : async Types.CanisterInfo { switch (pool.getExpiredCanisterId()) { @@ -26,8 +42,10 @@ actor class Self(opt_params : ?Types.InitParams) { case (#reuse(info)) { let cid = { canister_id = info.id }; let status = await IC.canister_status(cid); - let top_up_cycles : Nat = params.cycles_per_canister - status.cycles; - Cycles.add(top_up_cycles); + if (status.cycles < params.cycles_per_canister) { + let top_up_cycles : Nat = params.cycles_per_canister - status.cycles; + Cycles.add(top_up_cycles); + }; await IC.uninstall_code(cid); info }; @@ -53,8 +71,7 @@ actor class Self(opt_params : ?Types.InitParams) { }; }; - public query func dump() : async RBTree.Tree { - //Iter.toArray(state.project.entries()); + public query func dump() : async [Types.CanisterInfo] { pool.share() }; } diff --git a/backend/Types.mo b/backend/Types.mo index 15ea0d15..1d9e96ee 100644 --- a/backend/Types.mo +++ b/backend/Types.mo @@ -1,8 +1,8 @@ import Principal "mo:base/Principal"; -import RBTree "mo:base/RBTree"; +import Splay "mo:splay"; import Time "mo:base/Time"; import Buffer "mo:base/Buffer"; -import Option "mo:base/Option"; +import Iter "mo:base/Iter"; module { public type InitParams = { @@ -33,7 +33,7 @@ module { }; public class CanisterPool(size: Nat, TTL: Nat) { var len = 0; - var tree = RBTree.RBTree(canisterInfoCompare); + var tree = Splay.Splay(canisterInfoCompare); public type NewId = { #newId; #reuse:CanisterInfo; #outOfCapacity:Int }; public func getExpiredCanisterId() : NewId { if (len < size) { @@ -41,13 +41,13 @@ module { } else { switch (tree.entries().next()) { case null #outOfCapacity(-1); - case (?(info,_)) { + case (?info) { let now = Time.now(); let elapsed = now - info.timestamp; if (elapsed >= TTL) { - Option.assertSome(tree.remove(info)); + tree.remove(info); let new_info = { timestamp = now; id = info.id }; - tree.put(new_info, ()); + tree.insert(new_info); #reuse(new_info) } else { #outOfCapacity(TTL - elapsed) @@ -61,22 +61,22 @@ module { assert false; }; len += 1; - tree.put(info, ()); + tree.insert(info); }; public func refresh(info: CanisterInfo) : CanisterInfo { - Option.assertSome(tree.remove(info)); + tree.remove(info); let new_info = { timestamp = Time.now(); id = info.id }; - tree.put(new_info, ()); + tree.insert(new_info); new_info }; public func retire(info: CanisterInfo) { - Option.assertSome(tree.remove(info)); - tree.put({ timestamp = 0; id = info.id }, ()); + tree.remove(info); + tree.insert({ timestamp = 0; id = info.id }); }; public func gcList() : Buffer.Buffer { let now = Time.now(); let result = Buffer.Buffer(len); - for ((info, _) in tree.entries()) { + for (info in tree.entries()) { if (info.timestamp > 0) { // assumes when timestamp == 0, uninstall_code is already done if (info.timestamp > now - TTL) { return result }; @@ -86,8 +86,12 @@ module { }; result }; - public func share() : RBTree.Tree { - tree.share() + public func share() : [CanisterInfo] { + Iter.toArray(tree.entries()) + }; + public func unshare(list: [CanisterInfo]) { + len := list.size(); + tree.fromArray(list); }; }; } diff --git a/backend/tests/canisterPool.test.sh b/backend/tests/canisterPool.test.sh index e0afb4e7..374b1fc8 100755 --- a/backend/tests/canisterPool.test.sh +++ b/backend/tests/canisterPool.test.sh @@ -25,6 +25,7 @@ let c3 = call S.getCanisterId(); c3; let c4 = call S.getCanisterId(); c4; +assert c1.id != c2.id; assert c1.id == c3.id; assert c2.id == c4.id; @@ -32,7 +33,7 @@ assert c2.id == c4.id; let init = opt record { cycles_per_canister = 105_000_000_000 : nat; max_num_canisters = 2 : nat; - TTL = 60_000_000_000 : nat; + TTL = 3600_000_000_000 : nat; }; call ic.install_code( record { @@ -42,10 +43,16 @@ call ic.install_code( canister_id = S; }, ); -call S.getCanisterId(); -call S.getCanisterId(); +let c3 = call S.getCanisterId(); +c3; +let c4 = call S.getCanisterId(); +c4; fail call S.getCanisterId(); assert _ ~= "No available canister id"; +call S.removeCode(c4); +call S.getCanisterId(); +assert _.id == c4.id; +assert _.timestamp != c4.timestamp; // Out of cycle let id = call ic.provisional_create_canister_with_cycles(record { settings = null; amount = opt 10_000_000 }); diff --git a/backend/tests/upgrade.test.sh b/backend/tests/upgrade.test.sh new file mode 100755 index 00000000..7d59c3d5 --- /dev/null +++ b/backend/tests/upgrade.test.sh @@ -0,0 +1,78 @@ +#!ic-repl + +let id = call ic.provisional_create_canister_with_cycles(record { settings = null; amount = null }); +let S = id.canister_id; + +let init = opt record { + cycles_per_canister = 105_000_000_000 : nat; + max_num_canisters = 2 : nat; + TTL = 1 : nat; +}; +call ic.install_code( + record { + arg = encode (init); + wasm_module = file "../../.dfx/local/canisters/backend/backend.wasm"; + mode = variant { install }; + canister_id = S; + }, +); +let c1 = call S.getCanisterId(); +c1; +let c2 = call S.getCanisterId(); +c2; + +call ic.install_code( + record { + arg = encode (init); + wasm_module = file "../../.dfx/local/canisters/backend/backend.wasm"; + mode = variant { upgrade }; + canister_id = S; + }, +); +let c3 = call S.getCanisterId(); +c3; +let c4 = call S.getCanisterId(); +c4; +assert c1.id != c2.id; +assert c1.id == c3.id; +assert c2.id == c4.id; + +// Okay to increase pool and TTL +let init = opt record { + cycles_per_canister = 105_000_000_000 : nat; + max_num_canisters = 3 : nat; + TTL = 3600_000_000_000 : nat; +}; +call ic.install_code( + record { + arg = encode (init); + wasm_module = file "../../.dfx/local/canisters/backend/backend.wasm"; + mode = variant { upgrade }; + canister_id = S; + }, +); +let c5 = call S.getCanisterId(); +c5; +assert c5.id != c1.id; +assert c5.id != c2.id; +fail call S.getCanisterId(); +assert _ ~= "No available canister id"; + +// Cannot reduce pool +let init = opt record { + cycles_per_canister = 105_000_000_000 : nat; + max_num_canisters = 1 : nat; + TTL = 1 : nat; +}; +fail call ic.install_code( + record { + arg = encode (init); + wasm_module = file "../../.dfx/local/canisters/backend/backend.wasm"; + mode = variant { upgrade }; + canister_id = S; + }, +); +assert _ ~= "assertion failed"; +// still old canister, new TTL does not apply +fail call S.getCanisterId(); +assert _ ~= "No available canister id"; diff --git a/dfx.json b/dfx.json index 10e93300..90739506 100644 --- a/dfx.json +++ b/dfx.json @@ -16,7 +16,7 @@ "defaults": { "build": { "output": "build", - "packtool": "" + "packtool": "vessel sources" } }, "networks": { diff --git a/package-set.dhall b/package-set.dhall new file mode 100644 index 00000000..9173b58d --- /dev/null +++ b/package-set.dhall @@ -0,0 +1,29 @@ +let upstream = https://github.com/kritzcreek/vessel-package-set/releases/download/mo-0.6.4-20210624/package-set.dhall sha256:3f4cffd315d8ee5d2b4b5b00dc03b2e02732345b565340b7cb9cc0001444f525 +let Package = + { name : Text, version : Text, repo : Text, dependencies : List Text } + +let + -- This is where you can add your own packages to the package-set + additions = + [{ name = "base" + , repo = "https://github.com/dfinity/motoko-base" + , version = "dfx-0.7.0" + , dependencies = [] : List Text + }] : List Package + +let + {- This is where you can override existing packages in the package-set + + For example, if you wanted to use version `v2.0.0` of the foo library: + let overrides = [ + { name = "foo" + , version = "v2.0.0" + , repo = "https://github.com/bar/foo" + , dependencies = [] : List Text + } + ] + -} + overrides = + [] : List Package + +in upstream # additions # overrides diff --git a/vessel.dhall b/vessel.dhall new file mode 100644 index 00000000..526d9343 --- /dev/null +++ b/vessel.dhall @@ -0,0 +1,4 @@ +{ + dependencies = [ "base", "splay" ], + compiler = None Text +}