Skip to content

Commit

Permalink
Safe upgrade (#29)
Browse files Browse the repository at this point in the history
* splay tree

* upgrade

* test

* check init arg during upgrade

* fix

* use vessel
  • Loading branch information
chenyan-dfinity authored Jun 24, 2021
1 parent 44cb004 commit 9f8330a
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 26 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ yarn-debug.log*
yarn-error.log*

.eslintcache
.vessel

# IDEs
/.idea
/.vscode
/.history
/.history
31 changes: 24 additions & 7 deletions backend/Main.mo
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
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";

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()) {
Expand All @@ -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
};
Expand All @@ -53,8 +71,7 @@ actor class Self(opt_params : ?Types.InitParams) {
};
};

public query func dump() : async RBTree.Tree<Types.CanisterInfo, ()> {
//Iter.toArray(state.project.entries());
public query func dump() : async [Types.CanisterInfo] {
pool.share()
};
}
32 changes: 18 additions & 14 deletions backend/Types.mo
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -33,21 +33,21 @@ module {
};
public class CanisterPool(size: Nat, TTL: Nat) {
var len = 0;
var tree = RBTree.RBTree<CanisterInfo, ()>(canisterInfoCompare);
var tree = Splay.Splay<CanisterInfo>(canisterInfoCompare);
public type NewId = { #newId; #reuse:CanisterInfo; #outOfCapacity:Int };
public func getExpiredCanisterId() : NewId {
if (len < size) {
#newId
} 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)
Expand All @@ -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<Principal> {
let now = Time.now();
let result = Buffer.Buffer<Principal>(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 };
Expand All @@ -86,8 +86,12 @@ module {
};
result
};
public func share() : RBTree.Tree<CanisterInfo, ()> {
tree.share()
public func share() : [CanisterInfo] {
Iter.toArray(tree.entries())
};
public func unshare(list: [CanisterInfo]) {
len := list.size();
tree.fromArray(list);
};
};
}
13 changes: 10 additions & 3 deletions backend/tests/canisterPool.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ 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;

// Out of capacity
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 {
Expand All @@ -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 });
Expand Down
78 changes: 78 additions & 0 deletions backend/tests/upgrade.test.sh
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"defaults": {
"build": {
"output": "build",
"packtool": ""
"packtool": "vessel sources"
}
},
"networks": {
Expand Down
29 changes: 29 additions & 0 deletions package-set.dhall
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions vessel.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
dependencies = [ "base", "splay" ],
compiler = None Text
}

0 comments on commit 9f8330a

Please sign in to comment.