Motoko šŸ§¬ Module+ Easily upgradable stable memory modules standard

While the Motoko language provides a way for us to upgrade our stable memory, for a long time it seemed like something was missing. Developers put third-party modules like BTrees, HashMaps, HTTP servers, ICRC3 Logging, Databases, etc, but then each module provides its own upgrade system or doesnā€™t provide any at all while leaving it to the developers using these to figure things out.

We came up with the following standard which if adopted by library developers will improve developer experience a lot.

There seem to be two types of upgrades.

1. Module types upgrade - A new version of the module needs to upgrade its stable memory. For example, If this is an ICRC3 logging library and it needs to change its data structure. Developers can also write their canisters as multiple modules and changes in their memory will also be such.

2. Custom types upgrade - The module version stays the same, but users need to change their types. For example when we make changes in these types HashMap<Principal, Profile>

Module+ is expanding on this pattern which we called Class+ Writing Motoko stable libraries

Letā€™s start with how things will look from the perspective of a developer using Module+ libraries.

Notice that we are not doing

let mem_avatars_1 = HashMap.new<Text, Nat>();

If we do that, whenever the HashMap library changes version and its stable memory type is different, our mem_avatars_1 will have a different type. We need to be able to fix the type no matter how the library changed and thatā€™s why weā€™ve added (.Mem.HashMap.V1) which is part of the standard.

Letā€™s see what happens if the HashMap library needs a ā€˜Module types upgradeā€™

Itā€™s a one-step upgrade process in which mem_avatars_1 will be upgraded to mem_avatars_2 and then mem_avatars_1 will be emptied.
The class avatars has to now point to the new memory.
If we forget to remove this code in consequent upgrades, nothing bad will happen, it will keep working and wont do any additional upgrading.
We can clean it up by removing mem_avatars_1 and changing upgrade to new
image

Now letā€™s perform a ā€œCustom types upgradeā€
First we add mem_avatars_3 with the same module types version (V2)
Then we execute ā€˜upgradeā€™ given us by the library developers.
Its task is to upgrade mem_avatars_2, place it in mem_avatars_3 and delete the old memory.
To do that it expects us to give it a custom function where we change the types of each item.

If we forget about the code and upgrade the canister, again nothing bad will happen, no upgrades will run. We can clean it up by deleting the upgrade code
image

If Module+ accepts custom types and stores them in memory, it has to provide such upgrade functions.

Chaining upgrades is also possible. At the end of this mem_avatars_1 and 2 will be empty. Each version only provides upgrades from the previous one.

Nested stable memory module upgrades are also possible and a lot easier to do.

Writing Module+

The module is required to provide ā€œMemā€ and then the stable memory types like ā€œOneā€ followed by their versions V1 and V2. It picks the last version and its class only works with it.

Letā€™s see V1

The import line in production will be import MU "mo:mosup"
(MO)took (S)table (UP)grades
It is pretty simple and has helper functions and types.

The moduleā€™s memory is stored inside {var inner : ?M} and thatā€™s how it can get deleted in a one-step process and provide protections when leaving/forgetting the upgrade code while doing additional canister upgrades.

This is memory V2
We have added two more record fields ā€˜nameā€™ and ā€˜ageā€™.
Now we also need to write an upgrade function from V1 to V2

One last thing to look at is how ā€œCustom types upgradeā€ works.
Inside the Hashmap module, we add upgrade functions like this one

In a single canister, we typically include several mops libraries along with locally defined modules to organize the codebase. Weā€™ll be using Module+ to manage all of these.

If library developers adopt such a standard, upgrading will be easier, safer and the developer experience much better.
@skilesare @timo @kpeacock @icme @nomeata @tomijaga @claudio @luc-blaeser

Examples here:
[mosup/mo/entry.mo at master Ā· Neutrinomic/mosup Ā· GitHub]

It looks like the Motoko language team is also working on a language feature that will simplify it a bit and this standard will adapt to it. The language feature however wonā€™t free module developers from having to provide upgrade functions, so I believe it will mostly stay the same. The goal here is to not ask canister developers to write their own ā€˜module type upgradesā€™ and to simplify ā€˜custom type upgradesā€™

3 Likes

I like the end result here, although the syntax it requires is unfortunate

1 Like