LazyUpgrades.mo -- A pattern for upgrading your objects over time to avoid the cycle limit

I put this together after a brief discussion during the Motoko working group:

https://github.com/icdevs/LazyUpgrade.mo

Play Ground link: Internet Computer Loading

You can call set_fname, upgrade, and your data will be persisted.

It proves out a pattern for upgrading a record using the Class+ pattern put forward by @infu here: Writing Motoko stable libraries

It is based on @ZhenyaUsenko 's migration pattern at GitHub - ZhenyaUsenko/motoko-migrations: Sample project structure to implement migrations in motoko but moves the migration to the object level instead of the canister level.

There is a lot left to be explored here and this is in no way a final solution, but I wanted to start the conversation as this pattern may be necessary with canisters that have lots of data that needs to be upgraded over time rather then at the time of upgrade.

A few issues that I ran into:

  1. Can a var at the actor level be passed into a function by ref? I had to use the following pattern:
stable var some_stored = Person.Init({first_name= "Alice"; last_name= "Jones"; age = ?1});
  let update_some_stored = func(x : Person.RawState){
    some_stored := x;
  };
  let some = Person.Person(#state some_stored);

public shared func set_fname(name : Text ) : async Person.SharedState{
    some.set({some.get() with first_name = name}, update_some_stored);
  };

Having to pass this function into the set so that I can reference some_stored was the only way I could get it to work…I was hoping I could pass in the object as a var Person.RawState and have the function update it. But it doesn’t look like you can do that.

  1. This pattern breaks down with generics. If you look at the Persons folder you’ll see that I started trying to apply this to a list so that it would be easy to do collections of upgradeable objects. Since variants aren’t extensible I’d imagine I’m going to have to jump through some hoops to do this in a generic way. I think I can get to an upgradable pattern, but I think I’m going to have to make it specific to the type and that will increase the maintenance. (although one thing I want to try is to use an array of migrations instead of using variants as I’d at least be able to know my index).

  2. Interfaces are going to get complicated. The candid for an object with a lot of migrations is going to be pretty ugly. Your queries may not want to do all the processing to update all the objects in a collection, so you can call get_raw and it will return you the variant versions. But handling all of this by a client may get ugly.

  3. Upgrades can’t do async things. Right now the upgrades are sync…what if a default value relies on some outside value in another canister? It would take some work to get this to do async and there are certainly some use cases out there where you would want that functionality.

I’m happy to poke around this some more if anyone has any suggestions…also, feel free to fork, expand and send pull requests.

3 Likes

Thats unfortunate. You could put your memory in something like

let stored = {
var some : Nat;
var another: Nat;
}

then you won’t need to pass a function.

{some.get() with first_name = name}

That rocks, I was looking for that, didn’t know you can do that!

1 Like

Ahhh…yes…I’ll try that. Stables can have vars? If so that would be the way to do it.

1 Like