-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Fix a crash that happens when Firestore is being terminated when the last shared pointer to it is in a listener #7412
Changes from all commits
64883d9
338d0ba
3281b16
99f0708
b128d35
f752eee
1da99bc
2bc17dd
cfec3fd
e7b095a
bfe3b34
9e99c2a
8a3676b
d600f1d
a908828
9b7093a
80d35e3
d5e230d
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 |
---|---|---|
|
@@ -1441,6 +1441,30 @@ - (void)testAppDeleteLeadsToFirestoreTermination { | |
XCTAssertTrue(firestore.wrapped->client()->is_terminated()); | ||
} | ||
|
||
// Ensures b/172958106 doesn't regress. | ||
- (void)testDeleteAppWorksWhenLastReferenceToFirestoreIsInListener { | ||
FIRApp *app = testutil::AppForUnitTesting(util::MakeString([FSTIntegrationTestCase projectID])); | ||
FIRFirestore *firestore = [FIRFirestore firestoreForApp:app]; | ||
|
||
FIRDocumentReference *doc = [firestore documentWithPath:@"abc/123"]; | ||
// Make sure there is a listener. | ||
[doc addSnapshotListener:^(FIRDocumentSnapshot *, NSError *){ | ||
}]; | ||
NSDictionary<NSString *, id> *data = | ||
@{@"owner" : @{@"name" : @"Jonny", @"email" : @"[email protected]"}}; | ||
// Make sure the client is initialized. | ||
[self writeDocumentRef:doc data:data]; | ||
|
||
XCTestExpectation *expectation = [self expectationWithDescription:@"App is deleted"]; | ||
[app deleteApp:^(BOOL) { | ||
[expectation fulfill]; | ||
}]; | ||
// Let go of the last app reference. | ||
app = nil; | ||
|
||
[self awaitExpectations]; | ||
} | ||
|
||
- (void)testTerminateCanBeCalledMultipleTimes { | ||
FIRApp *app = testutil::AppForUnitTesting(util::MakeString([FSTIntegrationTestCase projectID])); | ||
FIRFirestore *firestore = [FIRFirestore firestoreForApp:app]; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,6 +64,7 @@ namespace core { | |
using api::DocumentReference; | ||
using api::DocumentSnapshot; | ||
using api::DocumentSnapshotListener; | ||
using api::Firestore; | ||
using api::ListenerRegistration; | ||
using api::QuerySnapshot; | ||
using api::QuerySnapshotListener; | ||
|
@@ -112,11 +113,13 @@ std::shared_ptr<FirestoreClient> FirestoreClient::Create( | |
std::shared_ptr<CredentialsProvider> credentials_provider, | ||
std::shared_ptr<Executor> user_executor, | ||
std::shared_ptr<AsyncQueue> worker_queue, | ||
std::unique_ptr<FirebaseMetadataProvider> firebase_metadata_provider) { | ||
std::unique_ptr<FirebaseMetadataProvider> firebase_metadata_provider, | ||
std::shared_ptr<Firestore> firestore) { | ||
// Have to use `new` because `make_shared` cannot access private constructor. | ||
std::shared_ptr<FirestoreClient> shared_client(new FirestoreClient( | ||
database_info, std::move(credentials_provider), std::move(user_executor), | ||
std::move(worker_queue), std::move(firebase_metadata_provider))); | ||
std::move(worker_queue), std::move(firebase_metadata_provider), | ||
std::move(firestore))); | ||
|
||
std::weak_ptr<FirestoreClient> weak_client(shared_client); | ||
auto credential_change_listener = [weak_client, settings](User user) mutable { | ||
|
@@ -158,12 +161,14 @@ FirestoreClient::FirestoreClient( | |
std::shared_ptr<CredentialsProvider> credentials_provider, | ||
std::shared_ptr<Executor> user_executor, | ||
std::shared_ptr<AsyncQueue> worker_queue, | ||
std::unique_ptr<FirebaseMetadataProvider> firebase_metadata_provider) | ||
std::unique_ptr<FirebaseMetadataProvider> firebase_metadata_provider, | ||
std::shared_ptr<Firestore> firestore) | ||
: database_info_(database_info), | ||
credentials_provider_(std::move(credentials_provider)), | ||
worker_queue_(std::move(worker_queue)), | ||
user_executor_(std::move(user_executor)), | ||
firebase_metadata_provider_(std::move(firebase_metadata_provider)) { | ||
firebase_metadata_provider_(std::move(firebase_metadata_provider)), | ||
weak_firestore_(std::move(firestore)) { | ||
} | ||
|
||
void FirestoreClient::Initialize(const User& user, const Settings& settings) { | ||
|
@@ -231,21 +236,27 @@ FirestoreClient::~FirestoreClient() { | |
} | ||
|
||
void FirestoreClient::Dispose() { | ||
// Prevent new API invocations from enqueueing further work. | ||
worker_queue_->EnterRestrictedMode(); | ||
|
||
// Clean up internal resources. It's possible that this can race with a call | ||
// to `Firestore::ClearPersistence` or `Firestore::Terminate`, but that's OK | ||
// because that operation does not rely on any state in this FirestoreClient. | ||
std::promise<void> signal_disposing; | ||
bool enqueued = worker_queue_->EnqueueEvenWhileRestricted([&, this] { | ||
// Once this task has started running, AsyncQueue::Dispose will block on its | ||
// completion. Signal as early as possible to lock out even restricted tasks | ||
// as early as possible. | ||
signal_disposing.set_value(); | ||
|
||
TerminateInternal(); | ||
}); | ||
bool enqueued = false; | ||
// If `remote_store_` is null, it means termination has finished already. In | ||
// that case, enqueing termination is not only unnecessary, but also | ||
// dangerous in the case when `Dispose` is invoked immediately after the | ||
// termination, still on the worker queue -- it would break the sequential | ||
// order invariant of the queue. | ||
if (!is_terminated()) { | ||
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 racy. Two threads could observe A better solution might be to have |
||
// Prevent new API invocations from enqueueing further work. | ||
worker_queue_->EnterRestrictedMode(); | ||
|
||
enqueued = worker_queue_->EnqueueEvenWhileRestricted([&, this] { | ||
// Once this task has started running, AsyncQueue::Dispose will block on | ||
// its completion. Signal as early as possible to lock out even restricted | ||
// tasks as early as possible. | ||
signal_disposing.set_value(); | ||
|
||
TerminateInternal(); | ||
}); | ||
} | ||
|
||
// If we successfully enqueued the TerminateInternal task then wait for it to | ||
// start. | ||
|
@@ -263,7 +274,13 @@ void FirestoreClient::Dispose() { | |
|
||
void FirestoreClient::TerminateAsync(StatusCallback callback) { | ||
worker_queue_->EnterRestrictedMode(); | ||
worker_queue_->EnqueueEvenWhileRestricted([this, callback] { | ||
worker_queue_->EnqueueEvenWhileRestricted([&, this, callback] { | ||
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. Default capture by reference in an asynchronously executing block is a recipe for disaster. It doesn't even seem like it's necessary. Remove? |
||
// Make sure this `FirestoreClient` is not destroyed during | ||
// `TerminateInternal`, so that `user_executor_` stays valid. | ||
auto self = shared_from_this(); | ||
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 we're depending on the It seems like both callbacks that call |
||
// Make sure that `api::Firestore` is not destroyed during the call to | ||
// `TerminateInternal`. | ||
auto firestore = weak_firestore_.lock(); | ||
TerminateInternal(); | ||
|
||
if (callback) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,7 +80,8 @@ class FirestoreClient : public std::enable_shared_from_this<FirestoreClient> { | |
std::shared_ptr<util::Executor> user_executor, | ||
std::shared_ptr<util::AsyncQueue> worker_queue, | ||
std::unique_ptr<remote::FirebaseMetadataProvider> | ||
firebase_metadata_provider); | ||
firebase_metadata_provider, | ||
std::shared_ptr<api::Firestore> firestore); | ||
|
||
~FirestoreClient(); | ||
|
||
|
@@ -185,7 +186,8 @@ class FirestoreClient : public std::enable_shared_from_this<FirestoreClient> { | |
std::shared_ptr<util::Executor> user_executor, | ||
std::shared_ptr<util::AsyncQueue> worker_queue, | ||
std::unique_ptr<remote::FirebaseMetadataProvider> | ||
firebase_metadata_provider); | ||
firebase_metadata_provider, | ||
std::shared_ptr<api::Firestore> firestore); | ||
|
||
void Initialize(const auth::User& user, const api::Settings& settings); | ||
|
||
|
@@ -223,6 +225,9 @@ class FirestoreClient : public std::enable_shared_from_this<FirestoreClient> { | |
bool credentials_initialized_ = false; | ||
local::LruDelegate* _Nullable lru_delegate_; | ||
util::DelayedOperation lru_callback_; | ||
|
||
// Used during shutdown to guarantee lifetimes. | ||
std::weak_ptr<api::Firestore> weak_firestore_; | ||
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. The type here doesn't actually matter since we don't need to manipulate this instance. Could this be |
||
}; | ||
|
||
} // namespace core | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,6 +86,7 @@ void AsyncQueue::ExecuteBlocking(const Operation& operation) { | |
"ExecuteBlocking may not be called " | ||
"before the previous operation finishes executing"); | ||
|
||
auto self = shared_from_this(); | ||
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. Is there a reason we haven't done this before? 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 seems like an oversight. I think the idea at the time was that FirestoreClient would keep the AsyncQueue alive as long as it needed to, but this doesn't account for the case where the Firestore destructor is running on the AsyncQueue. |
||
is_operation_in_progress_ = true; | ||
operation(); | ||
is_operation_in_progress_ = false; | ||
|
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.
There's no reference to
remote_store_
in the code below, andremote_store_
isn't actually a good signal anyway, since it is reset asynchronously. I think you're referring tois_terminated()
, right?Second, phrasing this in terms of "the sequential order invariant" obscures what's going on. I think the issue is that
EnqueueEvenWhileRestricted
can't be called while already on the async queue. Restated that way it seems like we should just fix that rather than trying to introduce yet another guard on top of that.