-
Notifications
You must be signed in to change notification settings - Fork 2
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
Local storage v3 migration #172
Conversation
5ccff3b
to
3b71bbd
Compare
side note; isn't it still the case that the recommended rpc entries in the prax registry contain outdated embedded frontends? I think we should remove any frontends that aren't using cron. |
3b71bbd
to
e83a83d
Compare
Environment:
User flows to test:
For all of these, confirm no corruption to the other keys (all get bumped to v3 after accessing (when migration takes place)). ===== Edit: Tested all flows. Working successfully. Two things to note:
|
RPCs will continually be behind for their embedded frontends. It's not something that can be updated without a point release (where we update it on core).
All in our frontend list except for one are using the cron. I've messaged UnityChaos about options in keeping their frontend up to date automatically. |
The version will never decrement as the user does not have a way to transition to older versions of the extension. After using V3 code, the reason you may see a few V2 keys is because those keys have not yet been accessed (the point at which those fields get migrated). In dev mode, if you go back to a previous version with v3 fields, it should just see there is not a migration for those v3 fields and do nothing. |
apps/extension/src/storage/base.ts
Outdated
type Migrations<K extends string | number | symbol> = Partial<Record<K, Migration>>; | ||
export type Migrations<T> = Partial<Record<Version, MigrationMap<any, T>>>; | ||
|
||
export type VersionSteps = Record<Version, Version>; | ||
|
||
export class ExtensionStorage<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not a suggested code change, but rather a general statement for reference if we restructure our migration mechanism in the future.
I personally think migrations feel more transactional in nature, where migrating multiple fields should ideally be handled atomically.
one potential approach is a batch migration design, where you define migrations at the version level. It ensures atomicity by checking that all changes associated with a particular version are applied together in a single transaction – so either all migrations succeed or none do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an important comment. It's unfortunate that chrome.local.storage does not come with any support for versioning or migrations. For this reason, we had to create our own custom migration code. It was probably a bad design that each field has their own version key. A possibly better design is to save a global version number, check for needed migrations when someone attempts a get()
, and if so, migrate all keys at once.
Going to spend today trying this out. The hardest part of this will be migrating from the old data structure to the new.
apps/extension/src/storage/migrations/local-v2-migration.test.ts
Outdated
Show resolved
Hide resolved
update: needs addressing can you speak on partial storage object migrations? suppose we're migrating through the storage versions from I think we should think through what constitutes the possible failure domains. |
Will pick up the missing code documentation tomorrow. Just pushed the update+tests that adopts the better global data structure for migrations in local storage. All is working on my end, but want to keep testing user flows given how sensitive of a change this is. |
I still haven't retested these workflows post refactor |
Tested on all user flows, things in my view are looking good ✅ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left a minor comment; double-checked the locking system logic and that we're propagating the errors up the caller stack properly, and everything looks good to me.
apps/extension/src/storage/base.ts
Outdated
private async withDbLock<R>(fn: () => Promise<R>): Promise<R> { | ||
if (this.dbLock) { | ||
await this.dbLock; | ||
} | ||
|
||
this.dbLock = this.migrateOrInitializeIfNeeded(); | ||
|
||
try { | ||
await this.dbLock; | ||
return await fn(); | ||
} finally { | ||
this.dbLock = undefined; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a subtle timing-related race condition here that may slip past the locking mechanism.
this.dbLock = this.migrateOrInitializeIfNeeded()
effectively sets the lock by assigning the promise returned by themigrateOrInitializeIfNeeded
. The migration process hasn't started yet.
-------------------------------------------- (small window of time) ---------------------------------------
- The migration technically starts when we reach first
await
insidemigrateOrInitializeIfNeeded
.
there's a brief window there where the race condition can be exploited – we're prematurely setting the lock to a promise. If another operation runs during this window, it would see the lock as set and mistakenly assume that the migration has already started, even though it hasn’t. Instead, we should set the lock only after the migration process is actively in progress.
private async withDbLock<R>(fn: () => Promise<R>): Promise<R> {
if (this.dbLock) {
await this.dbLock;
} else {
// ensure lock is only set after migration has started
const migrationPromise = this.migrateOrInitializeIfNeeded();
this.dbLock = migrationPromise;
}
try {
await migrationPromise;
return await fn();
} finally {
this.dbLock = undefined;
}
}
de23cdf
to
664fc2a
Compare
Closes #166 & quite a bit more. We use chrome.local.storage for persisting some state for the extension. This includes things like wallets, grpc urls, frontend selection, etc. Given local storage is simply a key-value store, it does not come with any tooling to handle migrations. This has made this task a bit more complex than expected.
Our custom migration code for local storage was not quite robust enough to reliably handle the task of the issue. For that reason, I refactored some mechanics. Will add more color inline below.
Note: This is PR is definitely within the "risky" territory. Migrating user state on the fly can have unintended consequences. This should be throughly tested before we ship another Prax version. For example, how does changing the user's rpc they saved in local storage affect an existing block sync in progress? It's very likely it won't take effect until the service worker is restarted (even though it shows otherwise in the extension). Hence the reason why changing RPCs triggers an extension restart.