-
Notifications
You must be signed in to change notification settings - Fork 615
Prepare shutdown
to be a public api.
#589
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
Changes from 7 commits
fa5e624
9ba6ff3
835ddc8
e7e3ea1
317fea7
34171e8
74d507c
fe02b17
f8b33ed
d3141b2
701fcef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -117,7 +117,13 @@ static FirebaseFirestore newInstance( | |
// so there is no need to include it in the persistence key. | ||
String persistenceKey = app.getName(); | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. This is the wrong layer at which to do this. Instead, have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
(firebaseAppName, options) -> { | ||
firestore.shutdown(/* fromAppDeletion= */ true); | ||
}); | ||
return firestore; | ||
} | ||
|
||
@VisibleForTesting | ||
|
@@ -329,13 +335,42 @@ public Task<Void> runBatch(@NonNull WriteBatch.Function batchFunction) { | |
return batch.commit(); | ||
} | ||
|
||
@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 commentThe reason will be displayed to describe this comment to others. Learn more. It would be clearer if we split this apart:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
FirestoreMultiDbComponent component = this.getApp().get(FirestoreMultiDbComponent.class); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
That said, I'd recommend something along the following lines:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically what you suggested, slight tweak. |
||
component.remove(this.databaseId.getDatabaseId()); | ||
} | ||
// The client must be initialized to ensure that all subsequent API usage throws an exception. | ||
this.ensureClientConfigured(); | ||
return client.shutdown(); | ||
} | ||
|
||
/** | ||
* Shuts down this FirebaseFirestore instance. | ||
* | ||
* <p>After shutdown only the {@link #clearPersistence()} method may be used. Any other method | ||
* will throw an {@link IllegalStateException}. | ||
* | ||
* <p>To restart after shutdown, simply create a new instance of FirebaseFirestore with {@link | ||
* #getInstance()} or {@link #getInstance(FirebaseApp)}. | ||
* | ||
* <p>Shutdown does not cancel any pending writes and any tasks that are awaiting a response from | ||
* the server will not be resolved. The next time you start this instance, it will resume | ||
* attempting to send these writes to the server. | ||
* | ||
* <p>Note: Under normal circumstances, calling <code>shutdown()</code> is not required. This | ||
* method is useful only when you want to force this instance to release all of its resources or | ||
* in combination with {@link #clearPersistence} to ensure that all local state is destroyed | ||
* between test runs. | ||
* | ||
* @return A <code>Task</code> that is resolved when the instance has been successfully shut down. | ||
*/ | ||
@VisibleForTesting | ||
// TODO(b/135755126): Make this public and remove @VisibleForTesting | ||
Task<Void> shutdown() { | ||
return shutdown(/* fromAppDeletion= */ false); | ||
} | ||
|
||
@VisibleForTesting | ||
AsyncQueue getAsyncQueue() { | ||
return asyncQueue; | ||
|
@@ -396,7 +431,7 @@ public static void setLoggingEnabled(boolean loggingEnabled) { | |
@PublicApi | ||
public Task<Void> clearPersistence() { | ||
final TaskCompletionSource<Void> source = new TaskCompletionSource<>(); | ||
asyncQueue.enqueueAndForget( | ||
asyncQueue.enqueueAndForgetEvenAfterShutdown( | ||
() -> { | ||
try { | ||
if (client != null && !client.isShutdown()) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ | |
import io.grpc.ManagedChannelBuilder; | ||
import io.grpc.MethodDescriptor; | ||
import io.grpc.android.AndroidChannelBuilder; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
/** Manages the gRPC channel and encapsulates all SSL and gRPC initialization. */ | ||
|
@@ -137,48 +138,53 @@ <ReqT, RespT> Task<ClientCall<ReqT, RespT>> createClientCall( | |
|
||
/** Shuts down the gRPC channel and the internal worker queue. */ | ||
void shutdown() { | ||
channelTask.addOnCompleteListener( | ||
asyncQueue.getExecutor(), | ||
task -> { | ||
ManagedChannel channel = task.getResult(); | ||
channel.shutdown(); | ||
try { | ||
// TODO(rsgowman): Investigate occasional hangs in channel.shutdown(). | ||
// | ||
// While running the integration tests, channel.shutdown() will occasionally timeout. | ||
// (Typically on ~4-5 different tests, differing from one run to the next.) We should | ||
// figure this out. But in the meantime, just use an exceptionally short timeout here | ||
// and skip straight to shutdownNow() which works every time. (We don't support shutting | ||
// down Firestore, so this should only be triggered from the test suite.) | ||
if (!channel.awaitTermination(1, TimeUnit.SECONDS)) { | ||
Logger.debug( | ||
FirestoreChannel.class.getSimpleName(), | ||
"Unable to gracefully shutdown the gRPC ManagedChannel. Will attempt an immediate shutdown."); | ||
channel.shutdownNow(); | ||
|
||
// gRPC docs claim "Although forceful, the shutdown process is still not | ||
// instantaneous; isTerminated() will likely return false immediately after this | ||
// method returns." Therefore, we still need to awaitTermination() again. | ||
if (!channel.awaitTermination(60, TimeUnit.SECONDS)) { | ||
// Something bad has happened. We could assert, but this is just resource cleanup | ||
// for a resource that is likely only released at the end of the execution. So | ||
// instead, we'll just log the error. | ||
Logger.warn( | ||
FirestoreChannel.class.getSimpleName(), | ||
"Unable to forcefully shutdown the gRPC ManagedChannel."); | ||
} | ||
} | ||
} catch (InterruptedException e) { | ||
// (Re-)Cancel if current thread also interrupted | ||
channel.shutdownNow(); | ||
|
||
// Similar to above, something bad happened, but it's not worth asserting. Just log it. | ||
Logger.warn( | ||
FirestoreChannel.class.getSimpleName(), | ||
"Interrupted while shutting down the gRPC Managed Channel"); | ||
// Preserve interrupt status | ||
Thread.currentThread().interrupt(); | ||
} | ||
}); | ||
ManagedChannel channel = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
try { | ||
channel = Tasks.await(channelTask); | ||
} catch (ExecutionException | InterruptedException e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you catch Also, if there was an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
Logger.warn( | ||
FirestoreChannel.class.getSimpleName(), | ||
"Channel is not initialized, shutdown will just do nothing."); | ||
return; | ||
} | ||
|
||
channel.shutdown(); | ||
try { | ||
// TODO(rsgowman): Investigate occasional hangs in channel.shutdown(). | ||
// | ||
// While running the integration tests, channel.shutdown() will occasionally timeout. | ||
// (Typically on ~4-5 different tests, differing from one run to the next.) We should | ||
// figure this out. But in the meantime, just use an exceptionally short timeout here | ||
// and skip straight to shutdownNow() which works every time. (We don't support shutting | ||
// down Firestore, so this should only be triggered from the test suite.) | ||
if (!channel.awaitTermination(1, TimeUnit.SECONDS)) { | ||
Logger.debug( | ||
FirestoreChannel.class.getSimpleName(), | ||
"Unable to gracefully shutdown the gRPC ManagedChannel. Will attempt an immediate shutdown."); | ||
channel.shutdownNow(); | ||
|
||
// gRPC docs claim "Although forceful, the shutdown process is still not | ||
// instantaneous; isTerminated() will likely return false immediately after this | ||
// method returns." Therefore, we still need to awaitTermination() again. | ||
if (!channel.awaitTermination(60, TimeUnit.SECONDS)) { | ||
// Something bad has happened. We could assert, but this is just resource cleanup | ||
// for a resource that is likely only released at the end of the execution. So | ||
// instead, we'll just log the error. | ||
Logger.warn( | ||
FirestoreChannel.class.getSimpleName(), | ||
"Unable to forcefully shutdown the gRPC ManagedChannel."); | ||
} | ||
} | ||
} catch (InterruptedException e) { | ||
// (Re-)Cancel if current thread also interrupted | ||
channel.shutdownNow(); | ||
|
||
// Similar to above, something bad happened, but it's not worth asserting. Just log it. | ||
Logger.warn( | ||
FirestoreChannel.class.getSimpleName(), | ||
"Interrupted while shutting down the gRPC Managed Channel"); | ||
// Preserve interrupt status | ||
Thread.currentThread().interrupt(); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.