roman-kashitsyn / mmapped.blog

My blog.
https://mmapped.blog
25 stars 1 forks source link

Effective Rust Canisters #8

Closed paulyoung closed 1 year ago

paulyoung commented 2 years ago

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

https://github.com/roman-kashitsyn/mmapped.blog/blob/7eaa01608b73cde2331a84f0ce645da020b05876/posts/01-effective-rust-canisters.html.pm#L44

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

https://github.com/roman-kashitsyn/mmapped.blog/blob/7eaa01608b73cde2331a84f0ce645da020b05876/posts/01-effective-rust-canisters.html.pm#L159

https://github.com/roman-kashitsyn/mmapped.blog/blob/7eaa01608b73cde2331a84f0ce645da020b05876/posts/01-effective-rust-canisters.html.pm#L170

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


There are some examples of functions that take UserId:

https://github.com/roman-kashitsyn/mmapped.blog/blob/7eaa01608b73cde2331a84f0ce645da020b05876/posts/01-effective-rust-canisters.html.pm#L217

https://github.com/roman-kashitsyn/mmapped.blog/blob/7eaa01608b73cde2331a84f0ce645da020b05876/posts/01-effective-rust-canisters.html.pm#L250

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:

https://github.com/roman-kashitsyn/mmapped.blog/blob/7eaa01608b73cde2331a84f0ce645da020b05876/posts/01-effective-rust-canisters.html.pm#L688

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

For 2, I misunderstood. I thought that https://github.com/dfinity/cdk-rs/pull/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 commented 2 years ago

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.