-
Notifications
You must be signed in to change notification settings - Fork 616
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
Conversation
boolean doneListening = l.onStateReached(persistedFidEntry); | ||
if (doneListening) { | ||
it.remove(); | ||
break; |
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.
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?
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.
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.
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.
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?
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.
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.
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.
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.
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 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.
firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java
Outdated
Show resolved
Hide resolved
...ase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java
Outdated
Show resolved
Hide resolved
…Token() calls are triggered simultaneously.
|
||
// Validate that registration is complete with updated auth token | ||
PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); | ||
assertThat(updatedFidEntry).isEqualTo(UPDATED_AUTH_TOKEN_FID_ENTRY); |
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 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
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.
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(); |
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.
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 ==.
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 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
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.
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()) |
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.
we can't hold the synchronized lock while doing network I/O because it'll block arbitrary threads that are calling getId / getAuthToken
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.
PTAL.
// 1. Invalid auth token | ||
// 2. If FORCE_REFRESH with a pending listener | ||
synchronized (lock) { | ||
if ((authTokenOption == FORCE_REFRESH && !listeners.isEmpty()) |
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.
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)
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.
Makes sense. Updated. PTAL. Thanks
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.
tried to demo suggestions in code: https://github.com/firebase/firebase-android-sdk/pull/860/files
...ase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java
Show resolved
Hide resolved
...ase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java
Outdated
Show resolved
Hide resolved
...ase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java
Outdated
Show resolved
Hide resolved
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); |
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.
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"
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.
Updated where appropriate for tests to be clear.
|
||
@Test | ||
public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { | ||
when(mockUtils.isAuthTokenExpired(any())).thenReturn(true); |
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.
why does this happen first thing in this test but like 3rd in the previous test?
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.
No specific reason. Does the order matter?
(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); |
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.
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.
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.
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() { |
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.
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)
).
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.
Done. PTAL.
persistedFidEntry = persistedFid.readPersistedFidEntryValue(); | ||
} | ||
|
||
// Notify the listeners only after force refreshing auth token if shouldRefreshAuthToken is |
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.
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?
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.
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); |
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.
why do the fetch after setting shouldRefreshAuthToken
to false instead of in reverse?
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.
Updated.
(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); |
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.
that's fine, I think the rest of the cases have benefited from the change
firebase-installations/src/main/java/com/google/firebase/installations/Utils.java
Outdated
Show resolved
Hide resolved
firebase-installations/src/main/java/com/google/firebase/installations/Utils.java
Outdated
Show resolved
Hide resolved
firebase-installations/src/main/java/com/google/firebase/installations/Utils.java
Outdated
Show resolved
Hide resolved
return supplier.get(); | ||
}; | ||
} | ||
// Always notify the GetIdListeners. For GetAuthTokenListeners, only notify if force |
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.
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?
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.
We need not be worried. getId call will never be called with force-refresh.
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.
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.
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.
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.
/test check-changed |
* 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.
* 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)
No description provided.