Skip to content

Latest commit

 

History

History
125 lines (100 loc) · 4.1 KB

migration.md

File metadata and controls

125 lines (100 loc) · 4.1 KB

How to migrate running canisters from std collections to ic-stable-memory

Warning!

It is strongly suggested to NOT migrate canisters, which already have at least 1GB memory occupied by their state. Migration happens in the #[post_upgrade] canister method, so any bad thing that may happen will BREAK your canister permanently.

Let them work as they work now. Spin up fresh canisters, that were written with ic-stable-memory from scratch and find a way to include them into the rest of your app.

But do not migrate old heavy canisters.

First of all, ic-stable-memory collections have a very similar API to std collections. So, you should be able to migrate with as few changes as possible.

Let's imagine we have a canister like this

thread_local! {
    static STATE: RefCell<Option<Vec<Principal>>> = RefCell::default();
}

#[init]
fn init() {
    STATE.with(|state| {
        *state.borrow_mut() = Some(Vec::new()); 
    });
}

#[post_upgrade]
fn post_upgrade() {
    STATE.with(|state| {
        *state.borrow_mut() = Some(retrieve_state_from_stable_memory());
    })
}

#[pre_upgrade]
fn pre_upgrade() {
    STATE.with(|state| {
        let state = state.borrow_mut().take().unwrap();
        
        store_state_to_stable_memory(state);
    })
}

Migration to ic-stable-memory takes two canister upgrades. During the first one you transfer all your data from std collections into stable ones. During the second upgrade you clean up the migration code.

First upgrade

thread_local! {
    static STATE: RefCell<Option<SVec<Principal>>> = RefCell::default(); // <- notice Vec changed to SVec
}

// <- notice there is no #[init], you can keep it, but there is no point in that in production

#[post_upgrade]
fn post_upgrade() {
    stable_memory_init(); // <- init stable memory allocator as the FIRST line

    // get the old state from stable memory as usual
    let old_state: Vec<Principal> = retrieve_state_from_stable_memory();
    
    // create a stable collection to move the state there
    let new_state = SVec::<Principal>::new_with_capacity(old_state.len())
        .expect("Out of memory"); // <- this is very dangerous, since it can break your canister

    // move the state to stable collection
    for entry in old_state {
        new_state.push(entry).unwrap();
    }

    // use stable collection instead of the old one
    STATE.with(|state| {
        *state.borrow_mut() = Some(new_state);
    });
}

#[pre_upgrade]
fn pre_upgrade() {
    // take the collection from static as usual
    let state = STATE.with(|state| {
        state.borrow_mut().take().unwrap()
    });
    
    // put it in a SBox
    let boxed_state = SBox::new(state).expect("Out of memory");
    
    // persist the pointer to that SBox between upgrades
    store_custom_data(1, boxed_state);
    
    // persist the memory allocator between upgrades
    stable_memory_pre_upgrade().expect("Out of memory");
}

Second upgrade

For the next canister upgrade you have to only update the #[post_upgrade] hook - everything else stays the same:

#[post_upgrade]
fn post_upgrade() {
    stable_memory_post_upgrade(); // <- instead of stable_memory_init()

    // retrieve the pointer to the SBox back
    let boxed_state = retrieve_custom_data::<SVec<Principal>>(1).unwrap();
    
    // transform that SBox back into the state
    let state = boxed_state.into_inner();

    // put state into static variable
    STATE.with(|s| {
        *s.borrow_mut() = Some(state);
    });
}

This is a simple example, but essentially you will have to complete the same steps for any other situation:

  • Move data from std collections to ic-stable-memory collections, during first canister upgrade.
  • Replace the #[post_upgrade] method with the one that only works with stable structures, during the second canister upgrade.

Rest of the code

Everything else should work as usual. ic-stable-memory collections API is not as rich as std's one, so sometimes you'll have to find a way of how to transform a high-level method into a set of lower-level ones, but there is no difference between them besides that.