Skip to content

Simplifying FirebaseInstallations class by adding listeners. #847

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

Merged
merged 16 commits into from
Oct 10, 2019

Conversation

ankitaj224
Copy link
Contributor

No description provided.

@ankitaj224 ankitaj224 requested a review from ciarand September 25, 2019 20:22
@googlebot googlebot added the cla: yes Override cla label Sep 25, 2019
boolean doneListening = l.onStateReached(persistedFidEntry);
if (doneListening) {
it.remove();
break;
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 sure whether we should keep the break or not. If we don't, listeners will finish earlier than they would normally which seems reasonable to me. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I dint think of the break; until my testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() test failed.

Reason: Multiple getAuthToken(FOREC_REFRESH) calls - if first call is completed it triggers onStateReached for both calls that are in the list .

GetAuthTokenListener onStateReached condition will pass (Registered & Auth Toke is valid) and remove both listeners from the list.

However, the second call execution continues, generates new auth token and updates the shared pref with new values - resulting in discrepancy in what we report and we persist in FIS DB & local storage.

Copy link
Contributor

Choose a reason for hiding this comment

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

ignoring what the code actually does atm, what's the expected behavior of calling getAuthToken(FORCE_REFRESH) twice? Are we expecting the first Task to be completed with a soon-to-be invalid auth token and the second task will be completed with the newly-valid token? Or do we want both tasks to be completed with the same auth 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.

AFIK we are expected to return the first Task on completion with a soon-to-be invalid auth token and the second task will be completed with the newly-valid token. I verified this flow with Di and yet to verify it with Rayo.

IMO it would be good to have both tasks be completed with the same auth token. But wont we complicate the logic ( like Force Refresh behaves differently when one call is made vs multiple calls, or when multiple calls with do-not-force-refresh and force-refresh options are called).

Your thoughts.

Copy link
Contributor

Choose a reason for hiding this comment

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

If that's the expected and desired outcome then I guess this is fine. I think it's a little error prone, but that's a product decision for your team and not a technical one that should be resolved here.

I think it would be clearer if we completed both tasks with the same result where possible, but to do that we'd have to keep the current auth token in memory and read it as part of our listener creation routine. I don't think that's prohibitively complicated (and it means we could simplify elsewhere by removing the break here and the conditional below), but that's a decision for your team.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is one corner case. I am yet to discuss and decide the outcome with the team.

However, I tried incorporating your suggestion. Can you PTAL if the changes are correct now.

@ankitaj224 ankitaj224 requested a review from ciarand September 25, 2019 21:28

// Validate that registration is complete with updated auth token
PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue();
assertThat(updatedFidEntry).isEqualTo(UPDATED_AUTH_TOKEN_FID_ENTRY);
Copy link
Contributor

Choose a reason for hiding this comment

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

this assertion (assertThat(updatedFidEntry).isEqualTo(UPDATED_AUTH_TOKEN_FID_ENTRY)) comes out of nowhere for me. Why are we expecting it to be the UPDATED_AUTH_TOKEN_FID_ENTRY? Could we somehow make that link more explicit in the code? Reminds me of the second negative example in http://go/tott/529 - this is a lil too magical

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 it to assert on the updated auth token value. Please review to see if it makes sense now. Else I ll try to be more descriptive.

String oldAuthToken =
persistedFid.readPersistedFidEntryValue().getAuthToken() == null
? ""
: persistedFid.readPersistedFidEntryValue().getAuthToken();
Copy link
Contributor

Choose a reason for hiding this comment

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

you've got the right idea, but we can't do this file I/O here because it could be called from any thread (we haven't hopped onto the executor yet).

How about instead of doing the file I/O here we maintain two int fields; the "current revision" and the "desired revision".

  • On first boot both IDs are 0 (or 1, whatever)
  • Whenever getAuthToken(FORCE_REFRESH) is called we make sure that desired revision is current revision + 1
  • Whenever we hit the "should we refresh our auth token?" conditional in the runnable body we just have to check whether current revision == desired revision. If they're the same, we don't have to refresh. If they're different, we need to refresh and then set them ==.

Copy link
Contributor

Choose a reason for hiding this comment

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

this doesn't even have to be ints, it could probably just be a boolean ("shouldRefresh"?) that's accessed in the same synchronized block as the listeners additions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

About the file I/O : shouldn't it be okay even if its called by any thread? As long as our onStateReached conditional can handle it correctly. I tested it with my test cases and it seemed to work fine. However, you are right about #847 (comment) .

I tried implementing your suggestion based on my understanding of the comment. Also, I have tested the changes. I will be honest, I dint understand this part

("shouldRefresh"?) that's accessed in the same synchronized block as the listeners additions

please let me know wdyt.

// 1. Invalid auth token
// 2. If FORCE_REFRESH with a pending listener
synchronized (lock) {
if ((authTokenOption == FORCE_REFRESH && !listeners.isEmpty())
Copy link
Contributor

Choose a reason for hiding this comment

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

we can't hold the synchronized lock while doing network I/O because it'll block arbitrary threads that are calling getId / getAuthToken

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PTAL.

// 1. Invalid auth token
// 2. If FORCE_REFRESH with a pending listener
synchronized (lock) {
if ((authTokenOption == FORCE_REFRESH && !listeners.isEmpty())
Copy link
Contributor

Choose a reason for hiding this comment

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

I get why we have this !listeners.isEmpty() check, but it feels like it's a symptom of what we're trying to check (is anyone still waiting for a state change?) instead of the actual thing we want to check (whether our current auth token needs to be refreshed)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Updated. PTAL. Thanks

@ankitaj224 ankitaj224 requested a review from ciarand September 27, 2019 22:06
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.

@ankitaj224 ankitaj224 requested a review from ciarand September 29, 2019 07:14
@ankitaj224
Copy link
Contributor Author

tried to demo suggestions in code: https://github.com/firebase/firebase-android-sdk/pull/860/files

Thanks for the demo. I picked up few suggestions from your demo.

when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1);
when(mockClock.currentTimeMillis()).thenReturn(TEST_CREATION_TIMESTAMP_1);
when(mockUtils.currentTimeInSecs()).thenReturn(TEST_CREATION_TIMESTAMP_2);
when(mockUtils.isAuthTokenExpired(any())).thenReturn(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the tests would be clearer if we clearly scoped these isAuthTokenExpired stubs to a specific auth token. So instead of just accepting any(), we could say "this auth token is expired but this one is not"

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 where appropriate for tests to be clear.


@Test
public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception {
when(mockUtils.isAuthTokenExpired(any())).thenReturn(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

why does this happen first thing in this test but like 3rd in the previous test?

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 specific reason. Does the order matter?

@ankitaj224 ankitaj224 requested a review from ciarand September 30, 2019 21:39
(unused) ->
InstallationTokenResult.builder()
.setToken(TEST_AUTH_TOKEN_4)
.setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP)
.build()))
.when(backendClientReturnsOk)
.generateAuthToken(anyString(), anyString(), anyString(), anyString());
when(mockUtils.isAuthTokenExpired(any())).thenReturn(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.

I skipped updating any() to a specific FID entry for mockUtils.isAuthTokenExpired() only here. This was to hide the details that don't improve the test. Hope that's fine.

Copy link
Contributor

Choose a reason for hiding this comment

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

that's fine, I think the rest of the cases have benefited from the change

// Waiting for Task that registers FID on the FIS Servers
listener.await(AWAIT_TIMEOUT_IN_SECS, TimeUnit.SECONDS);
return supplier.get();
private final Runnable doRegistration() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't bother wrapping this in a method like this. Either inline it at the field declaration site (Runnable doRegistration = () -> { /* code here */ } or make doRegistration the actual runnable and just chuck a method reference on the executor (execute(this::doRegistration)).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. PTAL.

persistedFidEntry = persistedFid.readPersistedFidEntryValue();
}

// Notify the listeners only after force refreshing auth token if shouldRefreshAuthToken is
Copy link
Contributor

Choose a reason for hiding this comment

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

trouble is that this means the getId listeners won't be notified until registration happens, which is something we wanted to avoid.

The initial draft version didn't run into this because it called triggerOnStateReached(NOT_REGISTERED) instead of triggerOnStateReached(persistedFidEntry) which meant the listeners were simpler because each step of the state machine always gets called. Could we change it so that it's calling triggerOnStateReached(newState, persistedFidEntry) in order to get the step-by-step simplification benefits of the state machine?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

shouldRefreshAuthToken will be false by default for getId. Hence, getId listener will be notified before registration happens. (Matches the initial draft version).

It is a matter of doing the checks here vs doing the checks in listeners.

With triggerOnStateReached(newState, persistedFidEntry) we have to change the definition of both the listeners (because interface changes). GetIdListener doesn't need newState.

synchronized (lock) {
shouldRefreshAuthToken = false;
}
fetchAuthTokenFromServer(persistedFidEntry);
Copy link
Contributor

Choose a reason for hiding this comment

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

why do the fetch after setting shouldRefreshAuthToken to false instead of in reverse?

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.

@ankitaj224 ankitaj224 requested a review from ciarand October 1, 2019 01:03
(unused) ->
InstallationTokenResult.builder()
.setToken(TEST_AUTH_TOKEN_4)
.setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP)
.build()))
.when(backendClientReturnsOk)
.generateAuthToken(anyString(), anyString(), anyString(), anyString());
when(mockUtils.isAuthTokenExpired(any())).thenReturn(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

that's fine, I think the rest of the cases have benefited from the change

return supplier.get();
};
}
// Always notify the GetIdListeners. For GetAuthTokenListeners, only notify if force
Copy link
Contributor

Choose a reason for hiding this comment

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

I am concerned that now a getId call won't complete as early as it could if there's a pending force-refresh call. Is that something we should be worried about?

Copy link
Contributor Author

@ankitaj224 ankitaj224 Oct 8, 2019

Choose a reason for hiding this comment

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

We need not be worried. getId call will never be called with force-refresh.

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 clarifying offline. I understand your concern now. I referred to your demo, made some modifications and updated the code. PTAL and let me wdyt.

Thank you.

@ankitaj224 ankitaj224 requested a review from ciarand October 8, 2019 18:59
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 think the onStateReached part might be a lil clearer using the state enum instead of a boolean, but this lgtm

* Fixing FISClient to correctly parse expiration timestamp.
* Updating getAuthToken to return creation timestamp along with auth token and
expiration timestamp.
* Propagating the exceptions to the clients.

* Adding javadocs for StateListener class.
@ankitaj224
Copy link
Contributor Author

/test check-changed

@ankitaj224 ankitaj224 merged commit 12ae11d into fis_sdk Oct 10, 2019
ankitaj224 added a commit that referenced this pull request Oct 14, 2019
* Implemneting retry for FIS Client.

* Simplifying FirebaseInstallations class by adding listeners. (#847)

* Simplifying FirebaseInstallations class by adding listeners.

* Addressing ciaran's comments to return same token if multiple getAuthToken()
calls are triggered simultaneously.

* Cleaning doRegistration method.

* Fixing FISClient to correctly parse expiration timestamp. (#848)

* Updating getAuthToken to return creation timestamp (#884)

* Propagating the exceptions to the clients. (#856)

* Addressing ciaran's comments.

* Merge conflicts.

* Nit fixes

* Fixing verify format issue.
ankitaj224 added a commit that referenced this pull request Oct 14, 2019
* Fixing FISClient to correctly parse expiration timestamp. (#848)

* Updating getAuthToken to return creation timestamp (#884)

* Propagating the exceptions to the clients. (#856)

* Extract FID from FIS createInstallation response (#888)

* Addressing Rayo's comment: Rename PersistedFidEntry to (#899)

* Implementing retry once for FIS Client. (#895)

* Simplifying FirebaseInstallations class by adding listeners. (#847)

* Fixing FISClient to correctly parse expiration timestamp. (#848)

* Updating getAuthToken to return creation timestamp (#884)

* Propagating the exceptions to the clients. (#856)
@firebase firebase locked and limited conversation to collaborators Nov 10, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants