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

feat(backend): User data migration #1967

Merged
merged 143 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
143 commits
Select commit Hold shift + click to select a range
752ff36
Add migration state
bitdivine Aug 5, 2024
fdfa3a5
Detail migration steps
bitdivine Aug 5, 2024
0e5b02c
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 5, 2024
b81b4d4
comment
bitdivine Aug 5, 2024
c77f63a
File for tests
bitdivine Aug 5, 2024
c58cf2e
Start happy path tests
bitdivine Aug 5, 2024
a25d707
API method to create a migration
bitdivine Aug 5, 2024
0dab5ca
API method to craete a migration
bitdivine Aug 5, 2024
523443c
++
bitdivine Aug 5, 2024
27e6a38
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 6, 2024
76a51b3
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 6, 2024
68a97ad
Update bindings
bitdivine Aug 6, 2024
2166b03
fix
bitdivine Aug 6, 2024
7081e51
Update bindings
bitdivine Aug 6, 2024
2b12c2b
++
bitdivine Aug 6, 2024
aa74aa0
++
bitdivine Aug 6, 2024
0e18602
++
bitdivine Aug 6, 2024
41defe2
++
bitdivine Aug 6, 2024
1c6326c
++
bitdivine Aug 6, 2024
7ba4254
Merge remote-tracking branch 'origin/main' into setup_struct
bitdivine Aug 6, 2024
5ea2c85
Smaller diff
bitdivine Aug 6, 2024
5721d59
++
bitdivine Aug 6, 2024
24f4ee0
++
bitdivine Aug 6, 2024
c25acae
rename
bitdivine Aug 6, 2024
0bd7b09
++
bitdivine Aug 6, 2024
289e2e7
++
bitdivine Aug 6, 2024
16f5bb9
simplify
bitdivine Aug 6, 2024
bc9f6db
Merge branch 'setup_struct' into data-upload-download-apis
bitdivine Aug 7, 2024
ede49a7
Variable controllers
bitdivine Aug 7, 2024
f9d1036
Add cycles only if non-zero
bitdivine Aug 7, 2024
a1acecf
Pub deploy-to
bitdivine Aug 7, 2024
40a1b44
Merge branch 'setup_struct' into data-upload-download-apis
bitdivine Aug 7, 2024
0affb89
Get wasm path from env if specified
bitdivine Aug 7, 2024
38b72c4
Rm unneeded mut
bitdivine Aug 7, 2024
4ea35a9
Merge branch 'setup_struct' into data-upload-download-apis
bitdivine Aug 7, 2024
4954fad
Make defaults public
bitdivine Aug 7, 2024
0865dfd
++
bitdivine Aug 7, 2024
dc9812e
Merge branch 'setup_struct' into data-upload-download-apis
bitdivine Aug 7, 2024
210d830
++
bitdivine Aug 7, 2024
2e67058
++
bitdivine Aug 7, 2024
6ea186f
++
bitdivine Aug 7, 2024
f7089a2
++
bitdivine Aug 7, 2024
bcc627c
Test more
bitdivine Aug 7, 2024
2a723b1
Add comment
bitdivine Aug 7, 2024
8cedc9b
comments
bitdivine Aug 7, 2024
ad8947d
consistency
bitdivine Aug 7, 2024
232c51d
spelling
bitdivine Aug 7, 2024
6a00d1f
bump
bitdivine Aug 7, 2024
7ce9ece
Merge branch 'setup_struct' into data-upload-download-apis
bitdivine Aug 7, 2024
fa9f89c
++
bitdivine Aug 7, 2024
f693595
++
bitdivine Aug 7, 2024
04492af
++
bitdivine Aug 7, 2024
3db4c35
++
bitdivine Aug 7, 2024
67fe0f0
Add method to lock writes
bitdivine Aug 7, 2024
546457f
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 7, 2024
d618610
rename
bitdivine Aug 7, 2024
d16859d
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 8, 2024
29974a6
regenerate-bindings
bitdivine Aug 8, 2024
05469d4
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 8, 2024
61bf47c
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 9, 2024
28870a2
Add spellcheck
bitdivine Aug 9, 2024
c1c6156
Fix merge
bitdivine Aug 9, 2024
755b87f
++
bitdivine Aug 12, 2024
ed2ace4
++
bitdivine Aug 12, 2024
90f7989
++
bitdivine Aug 12, 2024
2e41636
++
bitdivine Aug 12, 2024
e1e1a01
comment
bitdivine Aug 12, 2024
8f6c993
Add stats to binding
bitdivine Aug 12, 2024
e7b8038
Check target empty
bitdivine Aug 12, 2024
3807a3e
Check target empty
bitdivine Aug 12, 2024
9f49c1c
++
bitdivine Aug 12, 2024
fa3e350
Start migrating user tokens
bitdivine Aug 12, 2024
1f2796e
++
bitdivine Aug 12, 2024
de2ac41
++
bitdivine Aug 12, 2024
786f5dd
Upload user data
bitdivine Aug 12, 2024
ab37aee
++
bitdivine Aug 12, 2024
487a7fb
++
bitdivine Aug 12, 2024
adcf8d5
++
bitdivine Aug 12, 2024
30232af
++
bitdivine Aug 12, 2024
0212b35
++
bitdivine Aug 12, 2024
1edb370
++
bitdivine Aug 12, 2024
a0bfaf5
++
bitdivine Aug 12, 2024
5b0286b
lint
bitdivine Aug 12, 2024
a3a5b13
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 12, 2024
34e66df
++
bitdivine Aug 12, 2024
4c2f34d
++
bitdivine Aug 12, 2024
b7bb5e3
++
bitdivine Aug 12, 2024
e46dc86
++
bitdivine Aug 12, 2024
9722353
++
bitdivine Aug 12, 2024
89ff8f4
macro
bitdivine Aug 12, 2024
93d56ac
++
bitdivine Aug 12, 2024
1007600
++
bitdivine Aug 12, 2024
bedc1d7
++
bitdivine Aug 12, 2024
3b4b7b4
++
bitdivine Aug 12, 2024
e97abdd
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 14, 2024
0a6bb37
Updates and comments
bitdivine Aug 14, 2024
d90c276
Add strum EnumIter
bitdivine Aug 14, 2024
363b81e
Remove custom next()
bitdivine Aug 14, 2024
8d245a1
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 14, 2024
94a9188
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 15, 2024
3a89b0f
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 18, 2024
fb145a2
++
bitdivine Aug 20, 2024
d80d3e2
++
bitdivine Aug 20, 2024
c035175
++
bitdivine Aug 20, 2024
8a26936
++
bitdivine Aug 20, 2024
f31c9a6
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 20, 2024
497bfa7
++
bitdivine Aug 20, 2024
bf35cfc
Update bindings
bitdivine Aug 20, 2024
3faf818
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 20, 2024
9a81587
fmt
bitdivine Aug 20, 2024
90a35f0
Use migration error types
bitdivine Aug 20, 2024
982c26b
++
bitdivine Aug 20, 2024
7c017ea
++
bitdivine Aug 20, 2024
eecf1b9
++
bitdivine Aug 20, 2024
27303c8
++
bitdivine Aug 20, 2024
acc8b4f
++
bitdivine Aug 20, 2024
03b041a
++
bitdivine Aug 20, 2024
bdc0199
++
bitdivine Aug 20, 2024
9bd5a81
++
bitdivine Aug 20, 2024
f90f2fc
++
bitdivine Aug 20, 2024
4111d83
More tests
bitdivine Aug 20, 2024
0581341
Tweak states
bitdivine Aug 20, 2024
c68cb28
Better visibility
bitdivine Aug 20, 2024
003c3d6
++
bitdivine Aug 20, 2024
5e91a36
Improve logging
bitdivine Aug 20, 2024
a3b0a4d
++
bitdivine Aug 20, 2024
83508c1
++
bitdivine Aug 20, 2024
387414f
++
bitdivine Aug 20, 2024
200d559
++
bitdivine Aug 20, 2024
c4524d4
Update bindings
bitdivine Aug 20, 2024
8f003c9
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 20, 2024
c131652
clippy
bitdivine Aug 20, 2024
4738e5d
Make smaller
bitdivine Aug 20, 2024
b04a254
steps
bitdivine Aug 20, 2024
b39aa93
++
bitdivine Aug 20, 2024
2e27f4d
++
bitdivine Aug 20, 2024
1d79a70
++
bitdivine Aug 20, 2024
bac26de
++
bitdivine Aug 20, 2024
8dcd5df
fix
bitdivine Aug 20, 2024
8ee070e
++
bitdivine Aug 20, 2024
79fb60b
++
bitdivine Aug 20, 2024
7cb713b
tweak
bitdivine Aug 20, 2024
12a478f
Merge remote-tracking branch 'origin/main' into data-upload-download-…
bitdivine Aug 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use ic_cdk::api::management_canister::ecdsa::{
SignWithEcdsaArgument,
};
use ic_cdk::api::time;
use ic_cdk::eprintln;
use ic_cdk_macros::{export_candid, init, post_upgrade, query, update};
use ic_cdk_timers::{clear_timer, set_timer_interval};
use ic_stable_structures::{
Expand Down Expand Up @@ -625,10 +626,22 @@ fn migration_stop_timer() -> Result<(), String> {
})
}

/// Steps the migration
/// Steps the migration.
///
/// On error, the migration is marked as failed and the timer is cleared.
#[update(guard = "caller_is_allowed")]
async fn step_migration() {
migrate::step_migration().await;
let result = migrate::step_migration().await;
eprintln!("Stepped migration: {:?}", result);
if let Err(err) = result {
mutate_state(|s| {
if let Some(migration) = &mut s.migration {
migration.progress = MigrationProgress::Failed(err);
clear_timer(migration.timer_id);
}
eprintln!("Migration failed: {err:?}");
});
};
}

/// Computes the parity bit allowing to recover the public key from the signature.
Expand Down
230 changes: 171 additions & 59 deletions src/backend/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ use crate::{
mutate_state, read_state,
types::{Candid, StoredPrincipal},
};
use candid::{decode_one, CandidType, Principal};
use candid::{decode_one, encode_one, CandidType, Principal};
use ic_cdk::eprintln;
use ic_cdk_timers::clear_timer;
use serde::Deserialize;
use shared::types::{
custom_token::CustomToken, token::UserToken, user_profile::StoredUserProfile,
MigrationProgress, Timestamp,
use shared::{
backend_api::Service,
types::{
custom_token::CustomToken, token::UserToken, user_profile::StoredUserProfile,
MigrationError, MigrationProgress, Timestamp,
},
};
use std::ops::Bound;
use steps::{
assert_target_empty, assert_target_has_all_data, lock_migration_target, make_this_readonly,
unlock_local, unlock_target,
};
pub mod steps;

/// A chunk of data to be migrated.
///
Expand Down Expand Up @@ -67,8 +77,110 @@ pub fn bulk_up(data: &[u8]) {
}
}

#[allow(clippy::unused_async)] // TODO: remove this once we make real function calls
pub async fn step_migration() {
/// The next chunk of user tokens to be migrated.
fn next_user_token_chunk(last_user_token: Option<Principal>) -> Vec<(Principal, Vec<UserToken>)> {
let chunk_size = 5;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very small chunk size. Will this be increase / made configurable in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oisy doesn't have many users. In future I expect this code to be gone, at which point all the migration code that exists will be infinitely configurable. 😉

let range = last_user_token.map_or((Bound::Unbounded, Bound::Unbounded), |token| {
(Bound::Excluded(StoredPrincipal(token)), Bound::Unbounded)
});
read_state(|state| {
state
.user_token
.range(range)
.take(chunk_size)
.map(|(stored_principal, token)| (stored_principal.0, token.0))
.collect::<Vec<_>>()
})
}

/// The next chunk of custom tokens to be migrated.
fn next_custom_token_chunk(
last_custom_token: Option<Principal>,
) -> Vec<(Principal, Vec<CustomToken>)> {
let chunk_size = 5;
let range = last_custom_token.map_or((Bound::Unbounded, Bound::Unbounded), |token| {
(Bound::Excluded(StoredPrincipal(token)), Bound::Unbounded)
});
read_state(|state| {
state
.custom_token
.range(range)
.take(chunk_size)
.map(|(stored_principal, token)| (stored_principal.0, token.0))
.collect::<Vec<_>>()
})
}

/// The next chunk of user profiles to be migrated.
fn next_user_profile_chunk(
last_user_profile: Option<(Timestamp, Principal)>,
) -> Vec<((Timestamp, Principal), StoredUserProfile)> {
let chunk_size = 5;
let range = last_user_profile.map_or(
(Bound::Unbounded, Bound::Unbounded),
|(timestamp, principal)| {
(
Bound::Excluded((timestamp, StoredPrincipal(principal))),
Bound::Unbounded,
)
},
);
read_state(|state| {
state
.user_profile
.range(range)
.take(chunk_size)
.map(|((timestamp, stored_principal), profile)| {
((timestamp, stored_principal.0), profile.0)
})
.collect::<Vec<_>>()
})
}

/// The next chunk of user timestamps to be migrated.
fn next_user_timestamp_chunk(user_maybe: Option<Principal>) -> Vec<(Principal, Timestamp)> {
let chunk_size = 5;
let range = user_maybe.map_or((Bound::Unbounded, Bound::Unbounded), |user| {
(Bound::Excluded(StoredPrincipal(user)), Bound::Unbounded)
});
read_state(|state| {
state
.user_profile_updated
.range(range)
.take(chunk_size)
.map(|(stored_principal, timestamp)| (stored_principal.0, timestamp))
.collect::<Vec<_>>()
})
}

/// Migrates a chunk of data.
///
/// # Returns
/// The updated progress.
///
/// # Errors
/// - Throws a `MigrationError::DataMigrationFailed` if the data transfer fails.
macro_rules! migrate {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I would have approached that using generics, but this is probably cleaner. 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think generics are easier to debug than macros and more familiar to most people. So I tend to avoid macros where possible. But I am not sure it is possible to pass say $chunk_variant in a generic function, so I think this is the only option. I didn't spend a long time deciding though; I could be wrong.

($migration:ident, $chunk:ident, $progress_variant:ident, $chunk_variant:ident) => {{
let last = $chunk.last().map(|(k, _)| k).cloned();
let next_state = last
.map(|last| MigrationProgress::$progress_variant(Some(last)))
.unwrap_or_else(|| $migration.progress.next());
let migration_data = MigrationChunk::$chunk_variant($chunk);
let migration_bytes = encode_one(migration_data).expect("failed to encode migration data");
Service($migration.to)
.bulk_up(migration_bytes)
.await
.map_err(|e| {
eprintln!("Failed to transfer data {e:?}");
MigrationError::DataMigrationFailed
})?;
next_state
}};
}
pub(crate) use migrate;

pub async fn step_migration() -> Result<MigrationProgress, MigrationError> {
fn set_progress(progress: MigrationProgress) {
mutate_state(|state| {
state.migration.iter_mut().for_each(|migration| {
Expand All @@ -78,60 +190,60 @@ pub async fn step_migration() {
}
let migration = read_state(|s| s.migration.clone());
let progress = match migration {
Some(migration) => {
match migration.progress {
MigrationProgress::Pending => {
// TODO: Lock the local canister APIs.
migration.progress.next()
}
MigrationProgress::LockingTarget => {
// TODO: Lock the target canister APIs.
migration.progress.next()
}
MigrationProgress::CheckingTarget => {
// TODO: Check that the target canister is empty.
migration.progress.next()
}
MigrationProgress::MigratedUserTokensUpTo(_last) => {
// TODO: Migrate user tokens, then move on to the next stage.
migration.progress.next()
}
MigrationProgress::MigratedCustomTokensUpTo(_last_custom_token) => {
// TODO: Migrate custom tokens, then move on to the next stage.
migration.progress.next()
}
MigrationProgress::MigratedUserTimestampsUpTo(_user_maybe) => {
// TODO: Migrate user timestamps, then move on to the next stage.
migration.progress.next()
}
MigrationProgress::MigratedUserProfilesUpTo(_last_user_profile) => {
// TODO: Migrate user profiles, then move on to the next stage.
migration.progress.next()
}
MigrationProgress::CheckingDataMigration => {
// TODO: Check that the target canister has all the data.
migration.progress.next()
}
MigrationProgress::UnlockingTarget => {
// TODO: Unlock user profiles in the target canister
migration.progress.next()
}
MigrationProgress::Unlocking => {
// TODO: Unlock signing in this canister
migration.progress.next()
}
// TODO: Add steps to unlock APIs.
MigrationProgress::Completed => {
// Migration is complete.
clear_timer(migration.timer_id);
migration.progress.next()
}
MigrationProgress::Failed(_) => ic_cdk::trap("Migration error."),
Some(migration) => match migration.progress {
MigrationProgress::Pending => {
make_this_readonly();
migration.progress.next()
}
}
None => {
ic_cdk::trap("migration is not in progress");
}
MigrationProgress::LockingTarget => {
lock_migration_target(&migration).await?;
migration.progress.next()
}
MigrationProgress::CheckingTarget => {
assert_target_empty(&migration).await?;
migration.progress.next()
}
MigrationProgress::MigratedUserTokensUpTo(last) => {
let chunk = next_user_token_chunk(last);
migrate!(migration, chunk, MigratedUserTokensUpTo, UserToken)
}
MigrationProgress::MigratedCustomTokensUpTo(last_custom_token) => {
let chunk = next_custom_token_chunk(last_custom_token);
migrate!(migration, chunk, MigratedCustomTokensUpTo, CustomToken)
}
MigrationProgress::MigratedUserTimestampsUpTo(user_maybe) => {
let chunk = next_user_timestamp_chunk(user_maybe);
migrate!(
migration,
chunk,
MigratedUserTimestampsUpTo,
UserProfileUpdated
)
}
MigrationProgress::MigratedUserProfilesUpTo(last_user_profile) => {
let chunk = next_user_profile_chunk(last_user_profile);
migrate!(migration, chunk, MigratedUserProfilesUpTo, UserProfile)
}
MigrationProgress::CheckingDataMigration => {
assert_target_has_all_data(&migration).await?;
migration.progress.next()
}
MigrationProgress::UnlockingTarget => {
unlock_target(&migration).await?;
migration.progress.next()
}
MigrationProgress::Unlocking => {
unlock_local();
migration.progress.next()
}
MigrationProgress::Completed => {
clear_timer(migration.timer_id);
migration.progress.next()
}
MigrationProgress::Failed(e) => return Err(e),
},
None => return Err(MigrationError::NoMigrationInProgress),
};
set_progress(progress);
Ok(progress)
}
94 changes: 94 additions & 0 deletions src/backend/src/migrate/steps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::{modify_state_config, mutate_state};
use ic_cdk::eprintln;
use shared::{
backend_api::Service,
types::{ApiEnabled, Guards, Migration, MigrationError, Stats},
};

/// Makes the local canister APIs readonly.
pub fn make_this_readonly() {
mutate_state(|state| {
modify_state_config(state, |config| {
config.api = Some(Guards {
threshold_key: ApiEnabled::ReadOnly,
user_data: ApiEnabled::ReadOnly,
});
});
});
}

/// Locks the migration target canister APIs.
pub async fn lock_migration_target(migration: &Migration) -> Result<(), MigrationError> {
Service(migration.to)
.set_guards(Guards {
threshold_key: ApiEnabled::Disabled,
user_data: ApiEnabled::Disabled,
})
.await
.map_err(|e| {
eprintln!("Failed to lock target canister: {:?}", e);
MigrationError::TargetLockFailed
})
}

/// Verifies that there is no data in the migration target canister.
pub async fn assert_target_empty(migration: &Migration) -> Result<(), MigrationError> {
let stats = Service(migration.to)
.stats()
.await
.map_err(|e| {
eprintln!("Failed to get stats from the target canister: {:?}", e);
MigrationError::CouldNotGetTargetPriorStats
})?
.0;
if stats != Stats::default() {
return Err(MigrationError::TargetCanisterNotEmpty(stats));
}
Ok(())
}

/// Verifies that the target canister has all the data.
pub async fn assert_target_has_all_data(migration: &Migration) -> Result<(), MigrationError> {
let source_stats = crate::stats();
let target_stats = Service(migration.to)
.stats()
.await
.map_err(|e| {
eprintln!("Failed to get stats from the target canister: {e:?}");
MigrationError::CouldNotGetTargetPostStats
})?
.0;
if source_stats != target_stats {
return Err(MigrationError::TargetStatsMismatch(
source_stats,
target_stats,
));
}
Ok(())
}

/// Unlocks the target canister APIs.
pub async fn unlock_target(migration: &Migration) -> Result<(), MigrationError> {
Service(migration.to)
.set_guards(Guards {
threshold_key: ApiEnabled::Disabled,
user_data: ApiEnabled::Enabled,
})
.await
.map_err(|e| {
eprintln!("Failed to unlock target canister: {e:?}");
MigrationError::TargetUnlockFailed
})
}

/// Unlocks the local signing APIs. Disables local user data APIs.
pub fn unlock_local() {
mutate_state(|state| {
modify_state_config(state, |config| {
config.api = Some(Guards {
threshold_key: ApiEnabled::Enabled,
user_data: ApiEnabled::Disabled,
});
});
});
}
Loading