Skip to content
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

Merged
11 commits merged into from
Jul 17, 2019
Merged

Prepare shutdown to be a public api. #589

11 commits merged into from
Jul 17, 2019

Conversation

ghost
Copy link

@ghost ghost commented Jul 8, 2019

  1. FirebaseApp delete calls firestore shutdown.
  2. Deregister firestore when shutdown is called, new instance can then be registered.
  3. 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.

@googlebot googlebot added the cla: yes Override cla label Jul 8, 2019
@wilhuff wilhuff self-assigned this Jul 9, 2019
}
});
}

/** Has this client been shutdown. */
Copy link
Contributor

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

Copy link
Author

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

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

Copy link
Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

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.

@wilhuff wilhuff assigned ghost and unassigned wilhuff Jul 10, 2019
@ghost ghost assigned wilhuff and unassigned ghost Jul 11, 2019
@wilhuff wilhuff assigned ghost and unassigned wilhuff Jul 12, 2019
@ghost ghost assigned wilhuff and unassigned ghost Jul 16, 2019
Copy link
Contributor

@wilhuff wilhuff left a 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(
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 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.

Copy link
Author

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

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.

Copy link
Author

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

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.

Copy link
Author

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

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

Copy link
Author

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

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.

Copy link
Author

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

Choose a reason for hiding this comment

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

Commented out code?

Copy link
Author

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

/**
Copy link
Contributor

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.

Copy link
Author

Choose a reason for hiding this comment

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

Asked Vlad to review.

@wilhuff wilhuff assigned ghost and unassigned wilhuff Jul 16, 2019
@ghost ghost requested a review from vkryachko July 17, 2019 13:20
Task<Void> shutdown() {
Task<Void> shutdown(boolean fromAppDeletion) {
if (!fromAppDeletion && this.getApp() != null) {
FirestoreMultiDbComponent component = this.getApp().get(FirestoreMultiDbComponent.class);
Copy link
Member

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 of Consumer<String> onShutdown(or equivalent as it is java8 only)
  • have FirestoreMultiDbComponent inject the Consumer<String> into Firestore as a simple lambda that removes the instance based on the provided String dbId
  • have firestore call onShutdown when appropriate.

Copy link
Author

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

Choose a reason for hiding this comment

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

java.lang.Runnable instead?

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

Copy link
Author

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

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

Copy link
Contributor

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.

Copy link
Member

Choose a reason for hiding this comment

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

sgtm

@ghost ghost assigned wilhuff and unassigned ghost Jul 17, 2019
@wilhuff wilhuff assigned ghost and unassigned wilhuff Jul 17, 2019
@ghost ghost assigned vkryachko and wilhuff and unassigned ghost Jul 17, 2019
Copy link
Member

@vkryachko vkryachko left a 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);
Copy link
Member

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

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?

Copy link
Author

Choose a reason for hiding this comment

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

Fixed. Thanks.

Copy link
Contributor

@wilhuff wilhuff left a 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.

@wilhuff wilhuff assigned ghost and unassigned vkryachko and wilhuff Jul 17, 2019
@ghost ghost assigned vkryachko and unassigned ghost Jul 17, 2019
@ghost ghost merged commit 8152ab8 into master Jul 17, 2019
@rlazo rlazo deleted the MakeShutdownPublic branch September 27, 2019 14:55
@firebase firebase locked and limited conversation to collaborators Oct 9, 2019
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants