-
Notifications
You must be signed in to change notification settings - Fork 624
Add dynamic module support to Firestore #2432
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
Note that testRestartFirestoreLeadsToNewInstance() fails with this change; fix this.
…and Firestore_Unit_Tests
…gration_Tests__Firestore_Emulator_.xml that weren't meant to be pushed up
…eferred interface
…synchronized' modifier to removeChangeListener()
Coverage ReportAffected SDKs
Test Logs
NotesHTML coverage reports can be produced locally with Head commit (3b36cfc4) is created by Prow via merging commits: f56a977 90403bc. |
Binary Size ReportAffected SDKs
Test Logs
NotesHead commit (3b36cfc4) is created by Prow via merging commits: f56a977 90403bc. |
/** Used by dependers to register their callbacks. */ | ||
interface DeferredHandler<T> { | ||
@DeferredApi | ||
void handle(Provider<T> provider); | ||
void handle(T instance); |
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 would like to avoid this, since for one it's a breaking change and also is less general and breaks laziness(if eagerness is not required by other products).
What is this achieving other than removing the need to call get()
once?
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.
The main advantage is indeed the removal of the need to call get()
: instead of giving you an indirect reference to the data it gives you the data directly. When I used this code I first thought "why isn't it just giving me the instance? why is it giving me a provider to the instance?" I see your points about lazy initialization though.
What if the argument to handle()
is changed from Provider<T>
to Deferred<T>
and the this
instance is passed as the argument? Since Deferred
now extends Provider
this provides one authoritative way to get the instance: call get()
on the Deferred
object.
Also, since this is an internal-only API (and I don't see any products using Deferred
yet), could you explain the concerns about this being a breaking change?
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.
Note: The discussion about modifying Deferred
to extend Provider
occurred here: #2418 (review)
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 feel like Deferred
and Provider
are completely disjoint concepts - even though related in the context of dependency injection. The fact that OptionalProvider
implements both is just an internal implementation detail that I would prefer not to leak into the interface.
What if the argument to handle() is changed from Provider to Deferred and the this instance is passed as the argument? Since Deferred now extends Provider this provides one authoritative way to get the instance: call get() on the Deferred object.
I guess we could do that, but I don't see the benefits of this approach, afaict it's completely isomorphic to the existing interface however:
- has the downside I described above
- provides 2 ways of doing the same thing, i.e. calling get on the deferred itself or on the provider passed into
handle()
- constrains the implementation of deferred as its contract becomes more complicated.
Looks like rtdb's change has no problem with deferred not extending provider and instead uses an atomic reference to determine if auth is present or not, can we do the same in firestore?
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've reverted the changes to Deferred
and OptionalProvider
. It is indeed possible to use Deferred
without the changes I suggested and I've made those changes as well.
…andle(T) to handle(Deferred<T>)
/test device-check-changed |
2 similar comments
/test device-check-changed |
/test device-check-changed |
internalAuthProviderProviderRef.set(provider); | ||
currentUser = getUser(internalAuthProviderProviderRef.get()); | ||
if (changeListener != null) { | ||
changeListener.onValue(currentUser); |
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.
The changeListener only enqueues work on our internal queue. I think it is okay to execute on the main thread.
|
||
InternalAuthProvider internalAuthProvider = internalAuthProviderProviderRef.get().get(); | ||
if (internalAuthProvider == null) { | ||
return Tasks.forException(new FirebaseApiNotAvailableException("auth is not available")); |
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 should fall back to "unauthenticated" auth and return null
.
} | ||
|
||
@Override | ||
public synchronized Task<String> getToken() { | ||
boolean doForceRefresh = forceRefresh; | ||
forceRefresh = false; | ||
Task<GetTokenResult> res = authProvider.getAccessToken(doForceRefresh); | ||
|
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.
Hm, do we know what this dance above is for?
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 mean, I understand what it's doing in this class but I'm not sure how it fits into the bigger picture of auth in firestore. Since it was pre-existing code, I'm leaving the semantics as-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.
Can you inline doForceRefresh
?
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.
What do you mean by "inline"?
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 can ignore this since this is existing code, but I can't figure out why we need this extra variable. By inlining I mean replace "doForceRefresh" with "forceRefresh" (and setting "forceRefresh" to false a bit later).
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 can't figure it out either. The only difference it would make would be if internalAuthProvider.getAccessToken()
throws an exception, which seems like a pretty extreme case against which I doubt we need to guard. I've implemented this "inlining" of doForceRefresh
.
@Nullable String uid = authProvider.getUid(); | ||
/** Returns the current {@link User} as obtained from the given InternalAuthProvider. */ | ||
private static User getUser(Provider<InternalAuthProvider> provider) { | ||
InternalAuthProvider internalAuthProvider = provider.get(); |
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.
Do we need to pass this in as a reference?
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 had changed the getUser()
method to a static method to make it explicit at the call sites that it depended on the instance of InternalAuthProvider
. I've since changed it back to a non-static method that implicitly depends on this.internalAuthProviderProvider
and got rid of the effectively-unused currentUser
variable.
/** Returns the current {@link User} as obtained from the given InternalAuthProvider. */ | ||
private static User getUser(Provider<InternalAuthProvider> provider) { | ||
InternalAuthProvider internalAuthProvider = provider.get(); | ||
@Nullable String uid = (internalAuthProvider == null) ? null : internalAuthProvider.getUid(); |
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.
Is the internalAuthProvider ever null? Looks like you initialize it to a null
returning provider above.
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.
Yes, this.internalAuthProviderProvider.get()
initially returns null
; therefore, this null guard is required.
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.
Never mind, I got confused because this is a ProviderProvider
. I think this will be more obvious when this is just a Provider
.
@@ -40,7 +44,7 @@ | |||
|
|||
private static final String LOG_TAG = "FirebaseAuthCredentialsProvider"; | |||
|
|||
private final InternalAuthProvider authProvider; | |||
private AtomicReference<Provider<InternalAuthProvider>> internalAuthProviderProviderRef; |
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.
Should we be mixing synchronized
and AtomicReference
in this file?
|
||
assertThat(task.isComplete()).isTrue(); | ||
assertThat(task.isSuccessful()).isFalse(); | ||
assertThat(task.getException()).isInstanceOf(FirebaseApiNotAvailableException.class); |
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.
As discussed above, this should return a successful Task with a result of null
.
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'll update this test based on the result of the discussion about FirebaseApiNotAvailableException
in another comment.
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 won't update the test because my code is correct".
Came up with an answer for you.
} | ||
|
||
/** An implementation of {@link Deferred} whose provider is always available. */ | ||
private static final class ImmediateDeferred implements Deferred<InternalAuthProvider> { |
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.
Would it make sense to move these classes to separate package or utility class, so that they can be used from both test files? We could have something similar to the Tasks
class with "Deferred.forX()" style factory methods.
I also wonder if ImmediateDeferred
should be part of firebase-common.
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. I've moved these 3 Deferred
implementations into the testUtil
package. I'm slightly against the Deferred.forX()
factory methods because they add a layer of indirection at the call site. But I could definitely be swayed since it would give consistency with the Tasks
class.
I also wonder if
ImmediateDeferred
should be part of firebase-common.
What would be your rationale for doing this move?
private final AtomicReference<Provider<InternalAuthProvider>> internalAuthProviderProviderRef = | ||
new AtomicReference<>(); | ||
|
||
@Override | ||
public void whenAvailable(@NonNull DeferredHandler<InternalAuthProvider> handler) { | ||
Provider<InternalAuthProvider> internalAuthProviderProvider; | ||
synchronized (lock) { |
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.
Again, I think we should not be mixing synchronized
and AtomicReference
.
Since this is a test class, you could also synchronize on the object itself and use public synchronized void whenAvailable()
as the method signature.
I also wonder if these methods need to be synchronized at all. Does Mockito invoke the methods from a different thread?
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.
Agreed. I've changed the methods to just be declared as "synchronized". The synchronization is probably not strictly required; however, for correctness and to cover future use cases that may call whenAvailable
from other threads, I'm making the class thread safe now.
/test smoke-tests |
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 added some more comments (after guessing what your reply on my last round was :) )
} | ||
|
||
@Override | ||
public synchronized Task<String> getToken() { | ||
boolean doForceRefresh = forceRefresh; | ||
forceRefresh = false; | ||
Task<GetTokenResult> res = authProvider.getAccessToken(doForceRefresh); | ||
|
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.
Can you inline doForceRefresh
?
|
||
InternalAuthProvider internalAuthProvider = internalAuthProviderProviderRef.get().get(); | ||
if (internalAuthProvider == null) { | ||
return Tasks.forException(new FirebaseApiNotAvailableException("auth is not available")); |
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.
Please fix.
* its {@link Provider#get} method returns {@code null}, but will be changed to a new {@link | ||
* Provider} once the "auth" module becomes available. | ||
*/ | ||
@Nullable |
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 currently not nullable
.
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.
Fixed.
*/ | ||
@Nullable | ||
@GuardedBy("this") | ||
private Provider<InternalAuthProvider> internalAuthProviderProvider = () -> null; |
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 you could simplify your code if this was just a InternalAuthProvider
. You can "unbox" the Provider in whenAvailable
.
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 are correct; however, there is a custom lint check that complains when a "deferred" object is saved to an instance variable. Storing the Provider
makes the lint check happy.
The lint error is "Provider.get() assignment to a field detected" (
firebase-android-sdk/tools/lint/src/main/kotlin/ProviderAssignmentDetector.kt
Lines 47 to 50 in f56a977
context.report( | |
INVALID_PROVIDER_ASSIGNMENT, | |
context.getCallLocation(node, includeReceiver = false, includeArguments = true), | |
"Provider.get() assignment to a field detected.") |
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.
hm, it seems that this lint check is too strict in this case, meaning that it is not unreasonable to assign to a field from a @DeferredApi
method - which the whenAvailable()
callback lambda is an instance of. Please file a bug at go/fireescape-bug and feel free to @SuppressLint
it or leave your code as is for now.
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. I've changed the instance variable to simply @Nullable private InternalAuthProvider internalAuthProvider
, added a SuppressLint
to the constructor, and filed b/181014061 about the the erroneous lint check.
/** Returns the current {@link User} as obtained from the given InternalAuthProvider. */ | ||
private static User getUser(Provider<InternalAuthProvider> provider) { | ||
InternalAuthProvider internalAuthProvider = provider.get(); | ||
@Nullable String uid = (internalAuthProvider == null) ? null : internalAuthProvider.getUid(); |
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.
Never mind, I got confused because this is a ProviderProvider
. I think this will be more obvious when this is just a Provider
.
…tation from internalAuthProviderProvider
/test device-check-changed |
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 LGTM, but I do think we should use SuppressLint
and get rid of the ProviderProvider.
} | ||
|
||
@Override | ||
public synchronized Task<String> getToken() { | ||
boolean doForceRefresh = forceRefresh; | ||
forceRefresh = false; | ||
Task<GetTokenResult> res = authProvider.getAccessToken(doForceRefresh); | ||
|
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 can ignore this since this is existing code, but I can't figure out why we need this extra variable. By inlining I mean replace "doForceRefresh" with "forceRefresh" (and setting "forceRefresh" to false a bit later).
|
||
InternalAuthProvider internalAuthProvider = internalAuthProviderProviderRef.get().get(); | ||
if (internalAuthProvider == null) { | ||
return Tasks.forException(new FirebaseApiNotAvailableException("auth is not available")); |
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.
Your answer wasn't quite what I predicted. You have changed my mind. I was expecting the consumer to be a bit simpler than it actually is. Please disregard.
|
||
assertThat(task.isComplete()).isTrue(); | ||
assertThat(task.isSuccessful()).isFalse(); | ||
assertThat(task.getException()).isInstanceOf(FirebaseApiNotAvailableException.class); |
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 won't update the test because my code is correct".
Came up with an answer for you.
… InternalAuthProvider internalAuthProvider
Context: https://developer.android.com/guide/app-bundle/play-feature-delivery
Note to Googlers: This work is tracked by b/177833559.