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

Add offline use case for migrateFromSessionToken #2481

Closed
wants to merge 2 commits into from

Conversation

vladikoff
Copy link
Contributor

Fixes #2396

Pull Request checklist

  • Quality: This PR builds and tests run cleanly
    • cargo test --all produces no test failures
    • cargo clippy --all --all-targets --all-features runs without emitting any warnings
    • cargo fmt does not produce any changes to the code
    • ./gradlew ktlint detekt runs without emitting any warnings
    • swiftformat --swiftversion 4 megazords components/*/ios && swiftlint runs without emitting any warnings or producing changes
    • Note: For changes that need extra cross-platform testing, consider adding [ci full] to the PR title.
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry in CHANGES_UNRELEASED.md or an explanation of why it does not need one
    • Any breaking changes to Swift or Kotlin binding APIs are noted explicitly
  • Dependencies: This PR follows our dependency management guidelines
    • Any new dependencies are accompanied by a summary of the due dilligence applied in selecting them.

@vladikoff vladikoff requested review from rfk and eoger January 16, 2020 15:13
if self.state.in_flight_migration.is_some() {
// if there's a pending migration state, then try to provision a refresh_token
// by exchanging the session token for the refresh token
self.helper_migration_network_methods();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rfk @eoger any smart ideas here how to deal with the mutable here?

error[E0596]: cannot borrow `*self` as mutable, as it is behind a `&` reference
  --> components/fxa-client/src/device.rs:24:29
   |
23 |     pub fn get_devices(&self) -> Result<Vec<Device>> {
   |                        ----- help: consider changing this to be a mutable reference: `&mut self`
24 |         let refresh_token = self.get_refresh_token()?;
   |                             ^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error[E0596]: cannot borrow `*self` as mutable, as it is behind a `&` reference
   --> components/fxa-client/src/device.rs:104:29
    |
99  |         &self,
    |         ----- help: consider changing this to be a mutable reference: `&mut self`
...
104 |         let refresh_token = self.get_refresh_token()?;
    |                             ^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error[E0502]: cannot borrow `self.client` as immutable because it is also borrowed as mutable
   --> components/fxa-client/src/device.rs:146:13
    |
144 |         let refresh_token = self.get_refresh_token()?;
    |                             ---- mutable borrow occurs here
145 |         let pending_commands =
146 |             self.client
    |             ^^^^^^^^^^^ immutable borrow occurs here
147 |                 .pending_commands(&self.state.config, refresh_token, index, limit)?;
    |                                                       ------------- mutable borrow later used here

error[E0502]: cannot borrow `self.state.config` as immutable because it is also borrowed as mutable
   --> components/fxa-client/src/device.rs:147:35
    |
144 |         let refresh_token = self.get_refresh_token()?;
    |                             ---- mutable borrow occurs here
...
147 |                 .pending_commands(&self.state.config, refresh_token, index, limit)?;
    |                                   ^^^^^^^^^^^^^^^^^^  ------------- mutable borrow later used here
    |                                   |
    |                                   immutable borrow occurs here

error[E0596]: cannot borrow `*self` as mutable, as it is behind a `&` reference
   --> components/fxa-client/src/device.rs:264:29
    |
263 |     fn update_device(&self, update: DeviceUpdateRequest<'_>) -> Result<UpdateDeviceResponse> {
    |                      ----- help: consider changing this to be a mutable reference: `&mut self`
264 |         let refresh_token = self.get_refresh_token()?;
    |                             ^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

Or do we need to rewrite all the cases where the refresh_token is used:

Copy link
Contributor

Choose a reason for hiding this comment

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

No way around it, we are modifying the state.
You also need to keep in mind that these methods calling refresh_token() can now potentially mutate the persisted state, which means you need to call tryPersistState in the Kotlin/Swift wrappers after calling these. (also please add the /// **💾 This method may alter the persisted account state.** doc)

Copy link
Contributor

@eoger eoger left a comment

Choose a reason for hiding this comment

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

Thanks, this is more or less what I was expecting to happen in this PR, however you may have found an important thing regarding the persisted state, so it's important to fix :)

### Features

* `migrateFromSessionToken` now handles offline use cases. It caches the data the consumers originally provide.
If there's no network connectivity then the migration.
Copy link
Contributor

Choose a reason for hiding this comment

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

then the migration? 🤣

if self.state.in_flight_migration.is_some() {
// if there's a pending migration state, then try to provision a refresh_token
// by exchanging the session token for the refresh token
self.helper_migration_network_methods();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this wrong to assume we always want to do an in-flight-migration? I think we should do the if is_some() migrate before the match self.state.refresh_token {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeap there's a if self.state.in_flight_migration.is_some() {

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I wasn't really clear, I meant we could execute that block before the rest of the function.

if self.state.in_flight_migration.is_some() {
// if there's a pending migration state, then try to provision a refresh_token
// by exchanging the session token for the refresh token
self.helper_migration_network_methods();
Copy link
Contributor

Choose a reason for hiding this comment

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

No way around it, we are modifying the state.
You also need to keep in mind that these methods calling refresh_token() can now potentially mutate the persisted state, which means you need to call tryPersistState in the Kotlin/Swift wrappers after calling these. (also please add the /// **💾 This method may alter the persisted account state.** doc)

components/fxa-client/src/migrator.rs Show resolved Hide resolved
}
}

pub fn helper_migration_network_methods(&mut self) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: how about do_migrate or something else? helper_migration_network_methods was a bad temporary name I came up with

let refresh_token = self.get_refresh_token()?;
self.client.devices(&self.state.config, &refresh_token)
client.devices(config, &refresh_token)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hm i think we figured it out! the refresh_token was a ref

Copy link
Contributor

@rfk rfk left a comment

Choose a reason for hiding this comment

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

Looks like a good start!

As noted below, I think we need more nuanced handling around the type of error, because there are things other than network errors that we might run in to here.

I'd also like to see some explicit testcases to cover at least the following scenarios:

  • that we correctly handle a network error, and subsequent operations will complete the migration.
  • that we correctly handle an invalid session token, and subsequent operations don't keep retrying the migration
  • that we correctly handle a network error during migration, and an invalid sessionToken discovered only after retrying it

let k_xcs = hex::decode(k_xcs)?;
let k_xcs = base64::encode_config(&k_xcs, base64::URL_SAFE_NO_PAD);
let scoped_key_data = self.client.scoped_key_data(
// Gather the scope key data
Copy link
Contributor

Choose a reason for hiding this comment

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

nit "scope" => "scoped">

### Features

* `migrateFromSessionToken` now handles offline use cases. It caches the data the consumers originally provide.
If there's no network connectivity then the migration could be retried using the new `retryMigration` method.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see a retryMigration method, is this changelog entry from an older version of the code?

* This performs network requests, and should not be used on the main thread.
*/
fun migrateFromSessionToken(sessionToken: String, kSync: String, kXCS: String) {
fun migrateFromSessionToken(sessionToken: String, kSync: String, kXCS: String): Boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not opposed to adding it, but I'm also not sure what the caller is supposed to do with the information in the return value here. Are they supposed to change behaviour based on whether it's true or false, or is it for including in migration-related metrics? If the later than a json blob of metrics data may be a little clearer for consumers, by analogy with the other migration-related components.

@@ -96,14 +98,16 @@ impl FirefoxAccount {
}

pub(crate) fn invoke_command(
&self,
&mut self,
Copy link
Contributor

Choose a reason for hiding this comment

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

So just to check that I understand...all this mutable-self is because these methods might now alter the persisted account state by completing the migration, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

I recall a bug a while ago where Fenix was deserializing multiple copies of the FirefoxAccount object from JSON, one in the main app and then others in background worker threads. Do we still have any risk of that here? If so then I wonder if we might have some strange behaviours lurking where we try to complete the migration multiple times, possibly causing the FxA server's device-registration logic to get confused.

/cc @grigoryk

Copy link
Contributor

Choose a reason for hiding this comment

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

So just to check that I understand...all this mutable-self is because these methods might now alter the persisted account state by completing the migration, right?

Yes it makes me nervous too. I almost wonder if we should go with a different approach:

  • return boolean on fn migrate
  • let a-c call the same method later if we returned false.

None => Err(ErrorKind::NoRefreshToken.into()),
None => {
log::info!("No refresh_token, attempting to recover via migration state");
// if we have no refresh token, check if this is the case of a failed migration
Copy link
Contributor

Choose a reason for hiding this comment

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

nit "the case of" -> "a case of" or "due to" or similar?

// if we have no refresh token, check if this is the case of a failed migration
if self.state.in_flight_migration.is_some() {
// if there's a pending migration state, then try to provision a refresh_token
// by exchanging the session token for the refresh token
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this is not an "exchange" because you keep the sessionToken as well, you are using the sessionToken to grant the refresh token.

@@ -257,7 +287,7 @@ impl FirefoxAccount {
///
/// **💾 This method alters the persisted account state.**
pub fn disconnect(&mut self) {
if let Some(ref refresh_token) = self.state.refresh_token {
if let Some(ref refresh_token) = self.state.refresh_token.clone() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, why did this clone become necessary?

}
Err(err) => {
log::info!("Failed to perform network requests: {}", err);
// don't throw hard error here, let the consumers try again
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment may be out of date, since it's not the consumer that tries again, we try again ourselves internally.

}
}

pub fn helper_migration_network_methods(&mut self, client: Arc<dyn FxAClient>) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

There are two ways that this could fail, and I think they need to be treated differently.

In one case, it might fail due to some sort of transient network error. That's fine, trying again later will probably resolve the issue.

In the other case, it might fail due to some sort of permanently-fatal error. For example the provided sessionToken might be invalid, and we don't find that out until we hit the network and get an auth error back from the server. We must avoid getting stuck in an endless retry loop in the case of such permanent errors.

I think it's fine to re-throw the error in both cases, but you're going to need to clear the in-flight migration state in the case of a permanent error.

@vladikoff
Copy link
Contributor Author

Rewrite in #2492

@vladikoff vladikoff closed this Jan 20, 2020
@eoger eoger deleted the migration-offline-v3 branch April 21, 2020 21:54
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

Successfully merging this pull request may close these issues.

Graceful handling of the offline case during a sessionToken migration
3 participants