Skip to content

Refresh Auth token if expired during getId call. #809

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

Closed
wants to merge 14 commits into from

Conversation

ankitaj224
Copy link
Contributor

Addressing Rayo's comment on another PR which was marked as a TODO.

#703 (comment)

}
}

private void updatePersistedFidWithPendingStatus(PersistedFidEntry persistedFidEntry) {
Copy link
Contributor

Choose a reason for hiding this comment

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

A general comment : I feel like you are doing functionalization too much in this class.

Functionalization is useful when you want to hide a chunk of detailed logic, or to avoid duplicated code. But for example for updatePersistedFidWithPendingStatus, this function is only used in one place(doesn't avoid duplicated code), and it's not hiding any detailed logic (use one line to replace another line)

A downside of over functionalization is, with too many small method, it's really hard to find the relationship between them.

Above all, for these cases, I prefer making the code inline than making as a function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the detailed explanation. Updated the code. PTAL.

@@ -355,7 +373,8 @@ private InstallationTokenResult getValidAuthToken(PersistedFidEntry persistedFid

private boolean isPersistedFidRegistered(PersistedFidEntry persistedFidEntry) {
return persistedFidEntry != null
&& persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED;
&& (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED
|| persistedFidEntry.getRegistrationStatus() == RegistrationStatus.PENDING);
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be removed now, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. We need this check. With this new flow - both getId and getAuthToken need this.

getAuthToken uses it with persistedFid in REGISTERED state.
getId uses it with persistedFid in PENDING state.

@diwu-arete diwu-arete self-requested a review September 17, 2019 20:55
}

// If auth token is expired, refresh FID auth token with FIS servers in a background thread and
// updates the listener on
Copy link
Contributor

Choose a reason for hiding this comment

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

nit : in one line.

executeFidRegistration(persistedFidEntry, listener);
} else {
updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener);
if (persistedFidEntry.getRegistrationStatus() != RegistrationStatus.UNREGISTERED
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a helper for this? PersistedFidEntry#isRegistered

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added, as this method could be reused in getId and delete APIs.

* completion.
*/
private void refreshAuthToken(AwaitListener listener) {
Task<InstallationTokenResult> task = Tasks.call(executor, () -> refreshAuthTokenIfNecessary(1));
Copy link
Contributor

Choose a reason for hiding this comment

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

never pass the raw value for an @intdef constant; treat it like an enum and always use the predefined constants

(Gradle has a check for this afair, but I don't know if this repo is configured to run them)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you so much. Updated. Any examples on how do I configure it?


// Update FID registration status to PENDING to avoid multiple network calls to FIS Servers.
persistedFid.insertOrUpdatePersistedFidEntry(
persistedFidEntry.toBuilder().setRegistrationStatus(RegistrationStatus.PENDING).build());
Copy link
Contributor

Choose a reason for hiding this comment

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

you need to keep "reading cache status" and "updating cache status" in one atomic section, to avoid duplicated network calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. PTAL.

@ankitaj224
Copy link
Contributor Author

/test device-check-changed

@ankitaj224
Copy link
Contributor Author

/test device-check-changed

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-installations:
error: Added method com.google.firebase.installations.local.PersistedFid.isFidRegistered(com.google.firebase.installations.local.PersistedFidEntry) [AddedMethod]

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

Copy link
Contributor

@ciarand ciarand left a comment

Choose a reason for hiding this comment

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

I am worried about our use of null values in this code. Given we already have a state machine sorta thing, could we eliminate the use of null by adding a NOT_EVEN_GENERATED_YET_LOL (w/e) state? That way everything could assume non-null and the state machine describes the whole state

updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener);
}
private Task<String> registerFidIfNecessary(AwaitListener listener) {
synchronized (persistedFid) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if you're going to start synchronizing these instead of piping the values through a Task chain then you should add @GuardedBy annotations http://errorprone.info/bugpattern/GuardedBy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, I am locking on persistedFid and not persistedFidEntry( was piping it earlier on this value through task).

Also, I cannot synchronize on persistedFid throughout this class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, my point was that synchronization isn't necessarily required if you're just piping them through a Task call stack.

Why are we synchronizing on it here if we can't do it consistently? What benefit does it provide here that it doesn't provide elsewhere? Can we document that more explicitly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PENDING state was introduced to avoid multiple network calls for getId(). Registration status changes from UNREGISTERED to PENDING before triggering the network call.

Need for synchronization: Reading the existing registration status and updating it to PENDING should be atomic. That's the block i tried to synchronize.

I think piping wont solve the atomicity issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

ugh, this is too complicated

I see two similar potential issues here:

  1. synchronizing this PENDING write doesn't do any good if the reads are happening in an unsynchronized block on a different thread because there's no guarantee that the readers will see writes from a separate thread go/ej3e-78

    If we can show that everything that touches the PersistedFid class is running in the same thread (not currently the case) then I can see why we wouldn't need synchronization on this class, but as it is this looks really dangerous to me

  2. the PersistedFid class is vulnerable to the same issues because the read method doesn't hold a lock on the shared prefs so there's no guarantee that a write isn't going to get applied halfway through reading the old values, leaving you with a super corrupt / nonsensical PersistedFidEntry. SharedPrefs reads are atomic with writes, but we're reading like 6 fields so there's nothing stopping us reading the first 3 fields and then a write changing it underneath us

(also sidenote: given that getAuthToken is jumping straight to a worker thread it seems weird to have it synchronized, esp. since it's the only block synchronizing on this)

@@ -409,7 +419,7 @@ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsExceptio

PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue();

if (isPersistedFidRegistered(persistedFidEntry)) {
if (persistedFid.isFidRegistered(persistedFidEntry)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this isn't right, is it? if persistedFidEntry is null then the isFidRegistered method will return true and then we'll call persistedFidEntry.getFirebaseInstallationId() right below which will NPE

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if persistedFidEntry is null then the isFidRegistered method will return false.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changes this.

@@ -134,4 +134,10 @@ public boolean clear() {
private String getSharedPreferencesKey(String key) {
return String.format("%s|%s", persistenceKey, key);
}

@NonNull
public boolean isFidRegistered(@Nullable PersistedFidEntry persistedFidEntry) {
Copy link
Contributor

Choose a reason for hiding this comment

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

imo this should go on PersistedFidEntry or some sort of static util class... Otherwise it feels like we're just putting it here for convenience

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. PTAL.

@ankitaj224 ankitaj224 requested a review from ciarand September 18, 2019 22:09
updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener);
}
private Task<String> registerFidIfNecessary(AwaitListener listener) {
synchronized (persistedFid) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, my point was that synchronization isn't necessarily required if you're just piping them through a Task call stack.

Why are we synchronizing on it here if we can't do it consistently? What benefit does it provide here that it doesn't provide elsewhere? Can we document that more explicitly?

.build());
// If fid registration for this firebase installation is incomplete without an expired auth
// token, execute Fid registration.
if (!isAuthTokenExpired(updatedPersistedFidEntry)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this being checked twice in the same body?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is. First time its checked to evaluate if we need network calls. Second check helps to decide what type of network call is required.

Network call should be made : If UNREGISTERED or AuthToken Expired
Before triggering the call, we update the status to PENDING.

So, we rely on AuthToken Expired check.

If AuthToken not Expired: REGISTER Fid
else: Refresh Auth Token

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 having a really really hard time understanding the flow of this method. I don't know if it's just that I lack the domain knowledge or familiarity with the terms, but the multiple return points, double checking in and out of the synch block, and sometimes-async kickoffs are breaking my brain.


// Update the listener if awaiting
if (listener != null) {
refreshAuthTokenTask.addOnCompleteListener((unused) -> listener.onSuccess());
Copy link
Contributor

Choose a reason for hiding this comment

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

you're turning an onComplete into an onSuccess, this probably needs an explanation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed it to onSuccess. This is to update the listener asap on success instead of making it wait for 10s.

@ankitaj224 ankitaj224 requested a review from ciarand September 19, 2019 17:32
@@ -123,8 +123,7 @@ public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) {
private Task<String> getId(AwaitListener awaitListener) {
return Tasks.call(executor, this::getPersistedFid)
.continueWith(orElse(this::createAndPersistNewFid))
.onSuccessTask(
persistedFidEntry -> registerFidIfNecessary(persistedFidEntry, awaitListener));
.onSuccessTask(unused -> registerFidIfNecessary(awaitListener));
Copy link
Contributor

Choose a reason for hiding this comment

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

both this and the continueWith need an executor or they'll run on the main thread


// Update the listener if awaiting
if (listener != null) {
refreshAuthTokenTask.addOnSuccessListener((unused) -> listener.onSuccess());
Copy link
Contributor

Choose a reason for hiding this comment

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

needs an executor or a comment explaining why it's chill that we're calling this on the main thread


// Update the listener if awaiting
if (listener != null) {
fidRegistrationTask.addOnCompleteListener(listener);
Copy link
Contributor

Choose a reason for hiding this comment

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

needs an executor or a comment explaining why it's ok to call this on the main thread

updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener);
}
private Task<String> registerFidIfNecessary(AwaitListener listener) {
synchronized (persistedFid) {
Copy link
Contributor

Choose a reason for hiding this comment

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

ugh, this is too complicated

I see two similar potential issues here:

  1. synchronizing this PENDING write doesn't do any good if the reads are happening in an unsynchronized block on a different thread because there's no guarantee that the readers will see writes from a separate thread go/ej3e-78

    If we can show that everything that touches the PersistedFid class is running in the same thread (not currently the case) then I can see why we wouldn't need synchronization on this class, but as it is this looks really dangerous to me

  2. the PersistedFid class is vulnerable to the same issues because the read method doesn't hold a lock on the shared prefs so there's no guarantee that a write isn't going to get applied halfway through reading the old values, leaving you with a super corrupt / nonsensical PersistedFidEntry. SharedPrefs reads are atomic with writes, but we're reading like 6 fields so there's nothing stopping us reading the first 3 fields and then a write changing it underneath us

(also sidenote: given that getAuthToken is jumping straight to a worker thread it seems weird to have it synchronized, esp. since it's the only block synchronizing on this)

EXPIRED_AUTH_TOKEN_ENTRY,
UPDATED_AUTH_TOKEN_FID_ENTRY);
// Update local storage with fid entry that has auth token expired. When 2 tasks are triggered
// simultaneously, Task2 waits for Task1 to complete. On Task1 completion, task2 reads the
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "reads the" has too many (two) spaces

@@ -508,7 +535,9 @@ public void testDelete_registeredFID_successful() throws Exception {
Tasks.await(firebaseInstallations.delete());

PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue();
assertWithMessage("Persisted Fid Entry is not null.").that(entryValue).isNull();
assertWithMessage("Persisted Fid Entry is not null.")
Copy link
Contributor

Choose a reason for hiding this comment

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

the "is not null" message isn't correct any more

.build());
// If fid registration for this firebase installation is incomplete without an expired auth
// token, execute Fid registration.
if (!isAuthTokenExpired(updatedPersistedFidEntry)) {
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 having a really really hard time understanding the flow of this method. I don't know if it's just that I lack the domain knowledge or familiarity with the terms, but the multiple return points, double checking in and out of the synch block, and sometimes-async kickoffs are breaking my brain.

Copy link
Contributor

@andirayo andirayo left a comment

Choose a reason for hiding this comment

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

Removing this from my list of review-requests.

@rlazo rlazo closed this Jan 30, 2024
@firebase firebase locked and limited conversation to collaborators Mar 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes Override cla size/L
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants