Skip to content

Commit ea71423

Browse files
authored
Avoid deadlock during shutdown on Linux/Windows (#5920)
* Avoid deadlock during shutdown on Linux/Windows Under some circumstances, the Executor can be accessed by a user task that's running during shutdown. * Fix running with HAVE_LIBDISPATCH=0 on macOS
1 parent 7ef9914 commit ea71423

File tree

3 files changed

+70
-20
lines changed

3 files changed

+70
-20
lines changed

Firestore/Source/API/FIRFirestore.mm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,16 @@ - (void)setSettings:(FIRFirestoreSettings *)settings {
170170
_settings = settings;
171171
_firestore->set_settings([settings internalSettings]);
172172

173+
#if HAVE_LIBDISPATCH
173174
std::unique_ptr<util::Executor> user_executor =
174175
absl::make_unique<util::ExecutorLibdispatch>(settings.dispatchQueue);
176+
#else
177+
// It's possible to build without libdispatch on macOS for testing purposes.
178+
// In this case, avoid breaking the build.
179+
std::unique_ptr<util::Executor> user_executor =
180+
util::Executor::CreateSerial("com.google.firebase.firestore.user");
181+
#endif // HAVE_LIBDISPATCH
182+
175183
_firestore->set_user_executor(std::move(user_executor));
176184
}
177185
}

Firestore/core/src/util/executor_std.cc

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,30 +74,34 @@ ExecutorStd::~ExecutorStd() {
7474
}
7575

7676
void ExecutorStd::Dispose() {
77-
std::lock_guard<std::mutex> lock(mutex_);
77+
{
78+
std::lock_guard<std::mutex> lock(mutex_);
7879

79-
// Do nothing if already disposed.
80-
if (state_ == nullptr) {
81-
return;
82-
}
80+
// Do nothing if already disposed.
81+
if (state_ == nullptr) {
82+
return;
83+
}
8384

84-
state_->schedule_.Clear();
85-
86-
// Enqueue one Task with the kShutdownTag for each worker. Workers will finish
87-
// whatever task they're currently working on, execute this task, and then
88-
// quit.
89-
//
90-
// Note that this destructor may be running on a thread managed by this
91-
// Executor. This means that these tasks cannot be Awaited, though we do so
92-
// indirectly by joining threads if possible. On the thread currently running
93-
// this destructor, the kShutdownTag Task will execute after the destructor
94-
// completes.
95-
for (size_t i = 0; i < worker_thread_pool_.size(); ++i) {
96-
PushOnScheduleLocked(Immediate(), kShutdownTag, [] {});
97-
}
85+
state_->schedule_.Clear();
9886

99-
state_ = nullptr;
87+
// Enqueue one Task with the kShutdownTag for each worker. Workers will
88+
// finish whatever task they're currently working on, execute this task,
89+
// and then quit.
90+
//
91+
// Note that this destructor may be running on a thread managed by this
92+
// Executor. This means that these tasks cannot be Awaited, though we do so
93+
// indirectly by joining threads if possible. On the thread currently
94+
// running this destructor, the kShutdownTag Task will execute after the
95+
// destructor completes.
96+
for (size_t i = 0; i < worker_thread_pool_.size(); ++i) {
97+
PushOnScheduleLocked(Immediate(), kShutdownTag, [] {});
98+
}
99+
100+
state_ = nullptr;
101+
}
100102

103+
// Now that `state_` has been released, join any threads while not holding
104+
// the lock to avoid deadlocks where the thread tries to access the executor.
101105
for (std::thread& thread : worker_thread_pool_) {
102106
// If the current thread is running this destructor, we can't join the
103107
// thread. Instead detach it and rely on PollingThread to exit cleanly.

Firestore/core/test/unit/util/executor_test.cc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,44 @@ TEST_P(ExecutorTest, DestructorWaitsForExecutingTasks) {
363363
ASSERT_EQ(result, "1234");
364364
}
365365

366+
TEST_P(ExecutorTest, DisposeAvoidsDeadlockingWithCancellation) {
367+
Expectation running;
368+
Expectation shutdown_started;
369+
Expectation cancelled;
370+
371+
std::string result;
372+
373+
DelayedOperation operation;
374+
operation = executor->Schedule(Executor::Milliseconds(0), 42, [&] {
375+
result += "1";
376+
running.Fulfill();
377+
378+
Await(shutdown_started);
379+
380+
result += "3";
381+
operation.Cancel();
382+
383+
result += "4";
384+
cancelled.Fulfill();
385+
});
386+
387+
Expectation shutdown_complete;
388+
Async([&] {
389+
Await(running);
390+
result += "2";
391+
392+
shutdown_started.Fulfill();
393+
executor->Dispose();
394+
result += "5";
395+
396+
shutdown_complete.Fulfill();
397+
});
398+
399+
Await(cancelled);
400+
Await(shutdown_complete);
401+
ASSERT_EQ(result, "12345");
402+
}
403+
366404
TEST_P(ExecutorTest, DestructorAvoidsDeadlockWhenDeletingSelf) {
367405
Expectation complete;
368406
std::string result;

0 commit comments

Comments
 (0)