1. Canister upgrades, storage, and persistence
The Internet Computer handles persistent data storage using a feature known as stable memory. Stable memory is a unique feature of the Internet Computer that defines a data store separate from the canister's regular Wasm memory data store, which is known as heap memory. A canister's heap memory is not persistent storage and does not persist across canister upgrades; canister data and state stored in heap memory is removed when a canister is upgraded or reinstalled. For immutable canisters that use less than the heap memory limit of 4GiB, heap memory can be used. For larger canisters, and especially those that intend to be upgraded and changed over time, stable memory is important since it persists across canister upgrades, has a much larger storage capacity than heap memory, and is very beneficial for vertically scaling a dapp.
To use stable memory requires anticipating and indicating which canister data you want to be retained after a canister upgrade. For some canisters, this data might be all of the canister's data, while for others it may be only certain parts or none. By default, stable memory starts empty, and can hold up to 400GiB as long as the subnet the canister is running on has the available space. If a canister uses more stable memory than 400GiB, the canister will trap and become unrecoverable.
Memory types and terms
There are several terms associated with memory and storage on ICP. To avoid confusion between them, let's define them here.
-
Stable memory: Stable memory refers to the Internet Computer's long-term data storage feature. Stable memory is not language specific, and can be utilized by canisters written in Typescript, Motoko, Rust, or any other language. Stable memory can hold up to 400GiB if the subnet can accommodate it. Stable memory persists automatically across upgrades, When a canister is upgraded (its code is changed after being initially deployed) anything stored in stable memory will be preserved.
-
Heap memory: Heap memory refers to the regular Wasm memory data store for each canister. This storage is temporary and is not persisted across canister upgrades. Data stored in heap memory is removed when the canister is upgraded or reinstalled. Heap memory is limited to 4GiB.
-
Stable Structures: Stable structures is an Azle-specific term that refers to a data structures with familiar APIs that allow write and read access to stable memory. Writing and reading to and from stable memory can be done with a low-level API, but it is generally easier and preferable to use stable structures.
To further understand stable memory and how to use it, let's learn about upgrading canisters.
Upgrading canisters
Once a canister has been deployed and is running, there may need to be changes made to the canister's code to fix bugs or introduce new features. To make these changes, the canister must be upgraded.
Azle stable structures
Azle currently provides one stable structure called StableBTreeMap
. It's similar to a JavaScript Map and has most of the common operations you'd expect such as reading, inserting, and removing values.
Here's how to define a simple StableBTreeMap
:
import { StableBTreeMap } from "azle";
let map = StableBTreeMap<string, string>(0);
This is a StableBTreeMap
with a key of type string
and a value of type string
. Unless you want a default type of any
for your key
and value
, then you must explicitly type your StableBTreeMap
with type arguments.
StableBTreeMap
works by encoding and decoding values under-the-hood, storing and retrieving these values in bytes in stable memory. When writing to and reading from a StableBTreeMap
, by default the stableJson Serializable object
is used to encode JS values into bytes and to decode JS values from bytes. stableJson
uses JSON.stringify
and JSON.parse
with a custom replacer
and reviver
to handle many Candid
and other values that you will most likely use in your canisters.
Interactive example
Let's take a look at creating an Azle project and using StableBTreeMap
to store data in stable memory.
Prerequisites
Before you start, verify that you have set up your developer environment according to the instructions in 3 Developer environment setup.
Creating a new project
To get started, create a new project in your working directory. Open a terminal window, navigate into your working directory (developer_journey
), then use the commands:
- azle v0.18.0 or newer
use npx azle new <project_name>
to create a new project
npx azle new counter
Then, navigate into the new project directory and install the dependencies
cd counter
npm install
Defining a stable structure
In the following code example, we'll define a function called incrementCounter
. This function is designed to manage a counter
value stored within a StableBTreeMap
, ensuring its stability across updates. Paste this code into the src/counter_backend/index.ts
file:
import { StableBTreeMap } from "azle";
import express, { Request } from "express";
let counter = 0;
let map = StableBTreeMap<string, number>(0);
const incrementCounter = () => {
const currentCounter = map.get("counter");
if ("None" in currentCounter) {
map.insert("counter", counter);
return { counter: map.get("counter") };
}
const incrementedValue = currentCounter.Some + 1;
map.insert("counter", incrementedValue);
return { counter: incrementedValue };
};
const app = express();
app.use(express.json());
app.post("/db/update", (req: Request<any, any>, res) => {
const result = incrementCounter();
return res.json(result);
});
app.listen();
By default, GET requests are query calls and are not intended to update the canister's state. POST, PUT, PATCH, DELETE, etc., are used for update calls, modifying the state of the canister.
Save this file.
Deploying your counter dapp
First step, make sure you initialize your replica by running the command:
dfx start --clean
To upgrade your canister, first you need to deploy the initial version of the canister. Deploy the canister with the command:
dfx deploy
For this tutorial, you're using the local replica environment to deploy the canisters. You can deploy yours on the mainnet with the flag --network ic
. Remember that deploying to the mainnet will cost cycles.
You can interact with the counter dapp by sending requests through Postman, utilizing the Candid UI URL provided in the output of the dfx deploy command, like this:
Deployed canisters.
URLs:
Backend canister via Candid interface:
backend: http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai
To interact with the counter, open Postman and make a POST request using the provided URL followed by /db/update
, the counter will be 1.
Since this value is being stored in a StableBTreeMap
, this value of 1 will persist through a canister upgrade. This is a great example of persistent storage using Stable structures.
Stable structures in action
Now, let's make some changes to upgrade the canister. Alter the code of your canister, To keep things simple, you're going to change the counter increment value from 1 to 3. Your altered code looks like this:
import { StableBTreeMap } from "azle";
import express, { Request } from "express";
let counter = 0;
let map = StableBTreeMap<string, number>(0);
const incrementCounter = () => {
const currentCounter = map.get("counter");
if ("None" in currentCounter) {
map.insert("counter", counter);
return { counter: map.get("counter") };
}
const incrementedValue = currentCounter.Some + 3;
map.insert("counter", incrementedValue);
return { counter: incrementedValue };
};
const app = express();
app.use(express.json());
app.post("/db/update", (req: Request<any, any>, res) => {
const result = incrementCounter();
return res.json(result);
});
app.listen();
Save the file. Now, to confirm the canister's code has been upgraded, re-deploy the canister again with the command:
dfx deploy
This time, the output should include information about the canister's upgrade, such as:
🎉 Canister backend will be available at http://bkyz2-fmaaa-aaaaa-qaaaq-cai.localhost:4943
Installing canisters...
Upgrading code for canister backend, with canister ID bkyz2-fmaaa-aaaaa-qaaaq-cai
Deployed canisters.
URLs:
Backend canister via Candid interface:
backend: http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai
Now, navigate back to Postman and make a POST request using the provided URL followed by /db/update
again. This time, the counter should increment by 3, maintaining consistency with the value stored prior to the upgrade.
Need help?
Did you get stuck somewhere in this tutorial, or feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:
-
Developer Discord community, which is a large chatroom for ICP developers to ask questions, get help, or chat with other developers asynchronously via text chat.
-
Weekly developer office hours to ask questions, get clarification, and chat with other developers live via voice chat. This is hosted on our developer Discord group.
-
Submit your feedback to the ICP Developer feedback board.
Next steps
Next, you'll look at advanced canister calls, such as inter-canister calls and canister query methods.