Skip to content

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

Merged
merged 27 commits into from
Feb 23, 2021

Conversation

dconeybe
Copy link
Contributor

Context: https://developer.android.com/guide/app-bundle/play-feature-delivery

Note to Googlers: This work is tracked by b/177833559.

@googlebot googlebot added the cla: yes Override cla label Feb 10, 2021
@google-oss-bot
Copy link
Contributor

google-oss-bot commented Feb 10, 2021

Coverage Report

Affected SDKs

  • firebase-firestore

    SDK overall coverage changed from 48.64% (f56a977) to 48.72% (3b36cfc4) by +0.08%.

    Filename Base (f56a977) Head (3b36cfc4) Diff
    AbstractStream.java 36.90% 34.52% -2.38%
    AsyncQueue.java 77.89% 76.88% -1.01%
    FirebaseAuthCredentialsProvider.java 41.67% 100.00% +58.33%
    FirebaseFirestore.java 44.08% 42.95% -1.13%

Test Logs

Notes

HTML coverage reports can be produced locally with ./gradlew <product>:checkCoverage.
Report files are located at <product-build-dir>/reports/jacoco/.

Head commit (3b36cfc4) is created by Prow via merging commits: f56a977 90403bc.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Feb 10, 2021

Binary Size Report

Affected SDKs

  • firebase-firestore

    Type Base (f56a977) Head (3b36cfc4) Diff
    aar 1.10 MB 1.10 MB +92 B (+0.0%)
    apk (aggressive) 438 kB 438 kB +216 B (+0.0%)
    apk (release) 3.19 MB 3.19 MB -56 B (-0.0%)

Test Logs

Notes

Head 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);
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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)

Copy link
Member

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?

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'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.

@dconeybe
Copy link
Contributor Author

/test device-check-changed

2 similar comments
@dconeybe
Copy link
Contributor Author

/test device-check-changed

@dconeybe
Copy link
Contributor Author

/test device-check-changed

internalAuthProviderProviderRef.set(provider);
currentUser = getUser(internalAuthProviderProviderRef.get());
if (changeListener != null) {
changeListener.onValue(currentUser);
Copy link
Contributor

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"));
Copy link
Contributor

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);

Copy link
Contributor

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?

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 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.

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 inline doForceRefresh?

Copy link
Contributor Author

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"?

Copy link
Contributor

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).

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 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();
Copy link
Contributor

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?

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 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();
Copy link
Contributor

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.

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, this.internalAuthProviderProvider.get() initially returns null; therefore, this null guard is required.

Copy link
Contributor

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;
Copy link
Contributor

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);
Copy link
Contributor

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.

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'll update this test based on the result of the discussion about FirebaseApiNotAvailableException in another comment.

Copy link
Contributor

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> {
Copy link
Contributor

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.

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. 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?

Comment on lines 322 to 328
private final AtomicReference<Provider<InternalAuthProvider>> internalAuthProviderProviderRef =
new AtomicReference<>();

@Override
public void whenAvailable(@NonNull DeferredHandler<InternalAuthProvider> handler) {
Provider<InternalAuthProvider> internalAuthProviderProvider;
synchronized (lock) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@dconeybe
Copy link
Contributor Author

/test smoke-tests

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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);

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 inline doForceRefresh?


InternalAuthProvider internalAuthProvider = internalAuthProviderProviderRef.get().get();
if (internalAuthProvider == null) {
return Tasks.forException(new FirebaseApiNotAvailableException("auth is not available"));
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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;
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 you could simplify your code if this was just a InternalAuthProvider. You can "unbox" the Provider in whenAvailable.

Copy link
Contributor Author

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" (

context.report(
INVALID_PROVIDER_ASSIGNMENT,
context.getCallLocation(node, includeReceiver = false, includeArguments = true),
"Provider.get() assignment to a field detected.")
)

Copy link
Member

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.

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. 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();
Copy link
Contributor

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
@dconeybe
Copy link
Contributor Author

/test device-check-changed

@vkryachko vkryachko dismissed their stale review February 23, 2021 01:14

Deferred use lgtm

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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);

Copy link
Contributor

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"));
Copy link
Contributor

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);
Copy link
Contributor

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.

@dconeybe dconeybe merged commit f54d96a into master Feb 23, 2021
@dconeybe dconeybe deleted the dconeybe/DynamicModuleSupportBug177833559 branch February 23, 2021 16:04
@firebase firebase locked and limited conversation to collaborators Mar 26, 2021
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