Globally mutable state
Globally mutable state is data that exists globally, can be accessed from anywhere in your program, and can be altered at runtime.
By design, canisters on ICP are structured in a way that forces developers to use a globally mutable state. However, Rust's design makes it difficult to use globally mutable variables, forcing Rust developers to choose a method of code organization that takes ICP's design into consideration.
Using thread_local!
with Cell/RefCell
for state variables
Globally mutable state normally requires thread-safety. To use RefCell
, which is normally not thread-safe, you can use thread_local!
since thread_local!
globals do not require thread-safety:
thread_local! {
static NEXT_USER_ID: Cell<u64> = Cell::new(0);
static ACTIVE_USERS: RefCell<UserMap> = RefCell::new(UserMap::new());
}
Canister code should be target-independent
It is recommended that most of a canister's code is grouped into loosely coupled modules and packages, then tested independently. Most of the code that depends on the system API should go into the main file.
It is also possible to create a thin abstraction for the system API and test your code with a fake but faithful implementation. For example, you could use the following trait to abstract the stable memory API:
pub trait Memory {
fn size(&self) -> WasmPages;
fn grow(&self, pages: WasmPages) -> WasmPages;
fn read(&self, offset: u32, dst: &mut [u8]);
fn write(&self, offset: u32, src: &[u8]);
}
Observability
Metrics can be used to gain insight into your canister's statistics and productivity, such as:
- The size of the canister's stable memory.
- The size of the canister's internal data structures
- The sizes of objects allocated within the heap.
- The date and time the canister was last upgraded.
In Rust, you can expose a query call that returns a data structure containing your canister's metrics. If this data is not intended to be public, this query can be configured to be rejected based on the caller's principal. This approach provides a response that is structured and easy to parse.
pub struct MyMetrics {
pub stable_memory_size: u32,
pub allocated_bytes: u32,
pub my_user_map_size: u64,
pub last_upgraded_ts: u64,
}
#[query]
fn metrics() -> MyMetrics {
check_acl();
MyMetrics {
// ...
}
}
You can also expose a canister's metrics in a format that your monitoring system can ingest through the canister's HTTP gateway. For text-based exposition formats, the following example can be used:
fn http_request(req: HttpRequest) -> HttpResponse {
match path(&req) {
"/metrics" => HttpResponse {
status_code: 200,
body: format!("\
stable_memory_bytes {}
allocated_bytes {}
registered_users_total {}",
stable_memory_bytes, allocated_bytes, num_users),
// ...
}
}
}