Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Effective Rust Canisters #8

Closed
paulyoung opened this issue May 26, 2022 · 5 comments
Closed

Effective Rust Canisters #8

paulyoung opened this issue May 26, 2022 · 5 comments

Comments

@paulyoung
Copy link

paulyoung commented May 26, 2022

Hi @roman-kashitsyn!

I was re-reading Effective Rust Canisters and I wanted to say thank you for writing it. It’s a great resource.

I had some questions/suggestions, particularly around generating sequential user numbers that I hoped you could help me with.

Thank you in advance!


Here, an example is given that uses Cell for NEXT_USER_ID

static NEXT_USER_ID: Cell<u64> = Cell::new(0);

But later it is explained that variables using Cell “flexible” and are discarded on upgrades.

/* ◊b{flexible} ◊circled-ref[2] */ static LAST_ACTIVE: Cell<UserId> = ...;

◊em{flexible} variables are globals that the system discards on code upgrade.

Would it make more sense to use RefCell in the original example?


There are some examples of functions that take UserId:

async fn update_avatar(user_id: UserId, ◊b{pic: ByteBuf} ◊circled-ref[1] ) {

async fn refresh_profile_bad(user_id: UserId) {

But there isn’t an example of how one might properly manage the creation of new user IDs (assuming they are incremental, like in NEXT_USER_ID)

Could you provide an example of that?

Are there any pitfalls with something like the following?

thread_local! {
    static NEXT_USER_ID: RefCell<u128> = RefCell::new(0);
}

pub struct User {
    pub id: UserId,
    pub name: Name,
}

impl User {
    pub fn new(name: Name) -> Self {
        NEXT_USER_ID.with(|next_user_id| {
            let user = Self {
                id: UserId(*next_user_id.borrow()),
                name,
            };
            *next_user_id.borrow_mut() += 1;
            user
        })
    }
}

pub struct UserId(pub u128);

What do you recommend?


There’s a link to Internet Identity:

◊li{◊a[#:href "https://github.com/dfinity/internet-identity/tree/main/src/internet_identity"]{Internet Identity Backend} is a good example of a canister that uses stable memory as the main storage, obtains secure randomness from the system, and exposes Prometheus metrics.}

Internet Identity also has a sequential user number:

https://github.com/dfinity/internet-identity/blob/b434acea3831e4317cf20c26488f1d31570dd35b/src/internet_identity/src/storage.rs#L128-L140

However, it uses stable memory which is much more involved than using a stable variable. Could you explain why that is?

@paulyoung
Copy link
Author

I found this: https://github.com/dfinity/examples/blob/b8acf2341d7856a6fc3df3e12a66c2bbf4061232/motoko/encrypted-notes-dapp/src/encrypted_notes_rust/src/lib.rs#L260-L264

    let note_id = NEXT_NOTE.with(|counter_ref| {
        let mut writer = counter_ref.borrow_mut();
        *writer += 1;
        *writer
    });

@paulyoung
Copy link
Author

paulyoung commented May 31, 2022

On the question of Cell vs RefCell, maybe I made an incorrect assumption there’s nothing about the types themselves that implicitly make them stable or flexible.

Either way, I think that would be good to clarify.

The leads me to other questions about the upgrade process. I’ll try to report back with my findings here.

@paulyoung
Copy link
Author

paulyoung commented Jun 13, 2022

Some further questions:

  1. What do you think about having a single stable variable and struct for state to avoid nesting and having to manual thread lots of arguments to functions?
  2. Are you aware of any examples that don't use the deprecated storage API for pre_upgrade and post_upgrade?

@paulyoung
Copy link
Author

paulyoung commented Jun 13, 2022

For 2, I misunderstood. I thought that dfinity/cdk-rs#215 completely removed the storage API, but it didn't.

It looks like storage::stable_save and storage::stable_restore are still available: https://github.com/dfinity/cdk-rs/pull/215/files#diff-3c2aeeb74a082f6b1d004977688e887a7496565e415611a73dc487e2d95e002dR49-R57

@roman-kashitsyn
Copy link
Owner

Hi @paulyoung!

Thanks a lot for your feedback!

Here, an example is given that uses Cell for NEXT_USER_ID
But later it is explained that variables using Cell “flexible” and are discarded on upgrades.
Would it make more sense to use RefCell in the original example?

The difference between Cell and RefCell is not that one is always flexible and the other is stable, the difference lies in the kind of values we keep inside. It makes sense to use Cell for immutable values like integers and RefCell — for mutable values like collections (HashMaps, Vectors, etc.).

But there isn’t an example of how one might properly manage the creation of new user IDs (assuming they are incremental, like in NEXT_USER_ID)

Surprisingly, I hear this question very often, I'll add a piece of advice on the topic. Basically, a simple counter should work in 90% of cases, just like database ID sequences. Many people opt for UUID, which is often an unnecessary complication.

Internet Identity also has a sequential user number:
However, it uses stable memory which is much more involved than using a stable variable. Could you explain why that is?

Internet Identity was designed to store large amounts of data (currently, it uses ~4GiB of storage).
Serializing the whole state on upgrades is impractical for large datasets, so the II canister uses stable memory as the main storage, which makes upgrades safe and blazingly fast.
However, this design implies that II needs to control the stable memory layout precisely, and invoking stable_save can (and will) corrupt all the stable data structures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants