-
Notifications
You must be signed in to change notification settings - Fork 578
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
Prepare shutdown
to be a public api.
#589
Conversation
ghost
commented
Jul 8, 2019
- FirebaseApp delete calls firestore shutdown.
- Deregister firestore when shutdown is called, new instance can then be registered.
- Synchronize AsyncQueue.Executor scheduling. Also make it stop handling most of schedule requests once it is shutdown, only tasks originated from shutdown itself and clearPersistence can proceed.
firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreMultiDbComponent.java
Show resolved
Hide resolved
} | ||
}); | ||
} | ||
|
||
/** Has this client been shutdown. */ |
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.
Use a verb phrase that describes what the method does as if the sentence starts with "This method" (see http://go/java-practices/javadoc#summary_methods).
e.g.
/** Returns true if this client has been shut down. */
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.
* so that it's possible later to make assertions about executing on the correct thread. | ||
* A wrapper around a {@link ScheduledThreadPoolExecutor} class that provides: | ||
* | ||
* <ul> |
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.
Instead of <ul>
with manual numbers, consider just using an ordered list (<ol>
).
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.
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.
In javadoc this will now read:
1. 1. Synchronized ...
2. 2. Ability ...
3. 3. Single ...
<ol>
makes each <li>
insert a number instead of a bullet.
firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java
Show resolved
Hide resolved
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 looking a lot better.
return new FirebaseFirestore(context, databaseId, persistenceKey, provider, queue, app); | ||
FirebaseFirestore firestore = | ||
new FirebaseFirestore(context, databaseId, persistenceKey, provider, queue, app); | ||
app.addLifecycleEventListener( |
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 the wrong layer at which to do this. newInstance
shouldn't be registering a listener for this individual instance.
Instead, have FirestoreMultiDbComponent
implement FirebaseAppLifecycleListener
itself and have it register once, as the final action in its constructor. In the onDeleted
implementation, iterate through all instances and shut down the instances.
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.
@VisibleForTesting | ||
Task<Void> shutdown() { | ||
Task<Void> shutdown(boolean fromAppDeletion) { | ||
if (!fromAppDeletion && this.getApp() != 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.
It would be clearer if we split this apart:
- Public shutdown could just unconditionally navigate to to the component and remove this instance.
- If App is deleted, the lifecycle implementation in
FirestoreMultiDbComponent
will take care of dropping from that direction. - Have this method be package protected and not take a parameter. Call it something like
shutdownInternal
. (The precedent would be e.g. disableNetwork and disableNetworkInternal in RemoteStore. - Have this method only take the actions outside the
if
block.
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.
ManagedChannel channel = null; | ||
try { | ||
channel = Tasks.await(channelTask); | ||
} catch (ExecutionException | InterruptedException e) { |
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 you catch InterruptedException
you need to re-interrupt the current thread. That means that you can't handle these cases together.
Also, if there was an ExecutionException
this may be our only opportunity to log what happened, so we should log the the exception too.
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.
Thread.currentThread().interrupt(); | ||
} | ||
}); | ||
ManagedChannel channel = 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.
This needs a comment about why we're handling it synchronously because this is otherwise a suspicious idea.
Something like: "Handle shutdown synchronously to avoid re-enqueuing on the AsyncQueue after shutdown has started."
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.
queue.enqueueAndForget(runnableForStep(3)); | ||
queue.enqueueAndForgetEvenAfterShutdown(runnableForStep(4)); | ||
|
||
queue.getExecutor().execute(runnableForStep(5)); |
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 test makes no assertions. It seems like this should assert that 1, 2, and 4 completed.
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.
Added waitForExpectedSteps
, this plus the assertions in runnableForStep
should be sufficient now.
*/ | ||
private static final Matcher<ExpressionTree> WITHIN_SHUTDOWN = | ||
enclosingMethod(allOf(methodIsNamed("shutdown"))); | ||
// methodReturns(isVoidType()))); |
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.
Commented out code?
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.
Removed.
@@ -87,6 +87,19 @@ | |||
isSupertypeOf( | |||
state -> ASTHelpers.getType(state.findEnclosing(ClassTree.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.
This needs review by someone on the Android Core 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.
Asked Vlad to review.
Task<Void> shutdown() { | ||
Task<Void> shutdown(boolean fromAppDeletion) { | ||
if (!fromAppDeletion && this.getApp() != null) { | ||
FirestoreMultiDbComponent component = this.getApp().get(FirestoreMultiDbComponent.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.
You should not modify the errorprone check for "convenience" as this convenience comes at a price:
- You're essentially gaining access to any undeclared component dependency. This is something we(acore) want teams to do only if absolutely necessary and only as a temporary measure that they should consider tech debt.
- It makes the code harder to unit test as you're no longer getting everything you need via a constructor but effectively gaining access to services through another object you have access to via a "service locator" pattern(see https://github.com/google/guice/wiki/InjectOnlyDirectDependencies)
- Additionally, in this particular case, you are coupling this class to
FirestoreMultiDbComponent
imo unnecessarily
That said, I'd recommend something along the following lines:
- add a constructor parameter to
FirebaseFirestore
in the form ofConsumer<String> onShutdown
(or equivalent as it is java8 only) - have
FirestoreMultiDbComponent
inject theConsumer<String>
intoFirestore
as a simple lambda that removes the instance based on the providedString
dbId
- have firestore call
onShutdown
when appropriate.
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.
Basically what you suggested, slight tweak.
class FirestoreMultiDbComponent { | ||
class FirestoreMultiDbComponent implements FirebaseAppLifecycleListener { | ||
|
||
public interface DeregisterCommand { |
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.
java.lang.Runnable
instead?
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 a good idea.
Alternatively, you could just make an interface that contains remove(String databaseId)
such that FirestoreMultiDbComponent
could just implement it. This removes the extra capture.
In any case, the interface describing this behavior should not be nested in FirestoreMultiDbComponent
because that means FirebaseFirestore
still ends up having an awareness of that type. Instead nest the interface in FirebaseFirestore
.
Something like this is what I mean:
class FirebaseFiresore {
// ...
interface InstanceRegistry {
void remove(@NonNull String databaseId);
}
}
FirestoreMultiDbComponent can implement this interface directly.
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.
@@ -41,16 +48,39 @@ | |||
this.context = context; | |||
this.app = app; | |||
this.authProvider = authProvider; | |||
this.app.addLifecycleEventListener(this); |
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.
Just a consideration: app.delete()
is not a documented API and no other SDK integrates with it(hence it is effectively almost a noop), I'll defer to your judgement on whether you want to integrate with it
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.
During the API discussion about this, the consensus emerged that shutdown
must not leave a broken instance registered and that app deletion should mean shutdown of the Firestore instance.
While this hasn't been made public or documented on Android specifically, that's a localized problem. We want our ports to behave identically to the extent that we can arrange it. If this ever becomes a documented API we'll be ready. In other words: this will be the behavior on the other ports so might as well do the same here.
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.
sgtm
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.
LGTM, but make sure there is no data race in onDeleted
@@ -41,16 +48,39 @@ | |||
this.context = context; | |||
this.app = app; | |||
this.authProvider = authProvider; | |||
this.app.addLifecycleEventListener(this); |
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.
sgtm
} | ||
|
||
@Override | ||
public void onDeleted(String firebaseAppName, FirebaseOptions options) { |
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.
Shouldn't this method also synchronize its access to the map?
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. 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.
LGTM after addressing @vkryachko's synchronization comment.