Skip to content

Commit 7d2da18

Browse files
Your Namedaverigby
authored andcommitted
MB-41844: Avoid data race in FollyExecutorPool::doTaskQStat
As seen in TSan testing with FollyExecutorPool when running ep_perfsuite: Running [0013/0017]: Stat latency with 100 vbuckets. Also sets & DCP traffic on separate thread...================== WARNING: ThreadSanitizer: data race (pid=16435) Read of size 8 at 0x7b24000053f8 by main thread (mutexes: write M386882167168307840): #0 folly::HHWheelTimerBase<>::Callback::isScheduled() const follytsan/folly/io/async/HHWheelTimer.h:121:14 (libep.so+0x5c63b6) #1 FollyExecutorPool::doTaskQStat(...) kv_engine/engines/ep/src/folly_executorpool.cc:839:30 (libep.so+0x34ff85) #2 EventuallyPersistentEngine::doWorkloadStats(...) kv_engine/engines/ep/src/ep_engine.cc:4115:17 (libep.so+0x2e67d6) #3 EventuallyPersistentEngine::getStats(...) kv_engine/engines/ep/src/ep_engine.cc:4670:16 (libep.so+0x2d689a) ... Previous write of size 8 at 0x7b24000053f8 by thread T1: #0 memset <null> (ep_perfsuite+0x4b3a42) #1 folly::HHWheelTimerBase<>::Callback::cancelTimeoutImpl() follytsan/folly/io/async/HHWheelTimer.cpp:86:15 (libep.so+0x5c6507) #2 folly::HHWheelTimerBase<>::Callback::cancelTimeout() follytsan/folly/io/async/HHWheelTimer.h:114:7 (libep.so+0x5c6507) #3 FollyExecutorPool::TaskProxy::wake() kv_engine/engines/ep/src/folly_executorpool.cc:239:9 (libep.so+0x35e3e9) #4 FollyExecutorPool::State::wakeTask(unsigned long) kv_engine/engines/ep/src/folly_executorpool.cc:352:29 (libep.so+0x3625cf) #5 FollyExecutorPool::wake(unsigned long)::$_9::operator()() const kv_engine/engines/ep/src/folly_executorpool.cc:731:32 (libep.so+0x3515d1) ... Issue is how FollyExecutorPool::doTaskQStat() is implemented. It takes a copy of the taskOwners map on the eventBase thread to attempt to avoid races; however this is a shallow copy - the actual TaskProxy objects which the map references via shared_ptr are not copied. As such, when the TaskProxy objects are read by the non-eventBase thread a race is seen. Solution is to instead of taking a (shallow) copy and then manipulating the copy, just move the entire work onto the eventBase thread. Change-Id: I19de6911944370e836c9905e99c860ee3d5ccb0b Reviewed-on: http://review.couchbase.org/c/kv_engine/+/137406 Reviewed-by: James Harrison <james.harrison@couchbase.com> Tested-by: Build Bot <build@couchbase.com>
1 parent ce58be8 commit 7d2da18

File tree

1 file changed

+25
-18
lines changed

1 file changed

+25
-18
lines changed

engines/ep/src/folly_executorpool.cc

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,24 @@ struct FollyExecutorPool::State {
468468
return taskOwners;
469469
}
470470

471+
/**
472+
* @returns counts of how many tasks are waiting to run
473+
* (isScheduled() == true) for each task group.
474+
*/
475+
std::array<int, NUM_TASK_GROUPS> getWaitingTasksPerGroup() {
476+
std::array<int, NUM_TASK_GROUPS> waitingTasksPerGroup;
477+
for (const auto& owner : taskOwners) {
478+
for (const auto& task : owner.second) {
479+
if (task.second->isScheduled()) {
480+
const auto type = GlobalTask::getTaskType(
481+
task.second->task->getTaskId());
482+
waitingTasksPerGroup[type]++;
483+
}
484+
}
485+
}
486+
return waitingTasksPerGroup;
487+
}
488+
471489
private:
472490
/// Map of registered task owners (Taskables) to the Tasks they own.
473491
TaskOwnerMap taskOwners;
@@ -819,30 +837,19 @@ void FollyExecutorPool::doTaskQStat(Taskable& taskable,
819837
const AddStatFn& add_stat) {
820838
NonBucketAllocationGuard guard;
821839

822-
// Take a copy of the taskOwners map (so we don't have to acquire any
823-
// locks etc to calculate stats).
824-
auto* eventBase = futurePool->getEventBase();
825-
TaskOwnerMap taskOwnersCopy;
826-
eventBase->runInEventBaseThreadAndWait(
827-
[state = this->state.get(), &taskOwnersCopy] {
828-
taskOwnersCopy = state->copyTaskOwners();
829-
});
830-
831840
// Count how many tasks of each type are waiting to run - defined by
832841
// having an outstanding timeout.
833842
// Note: This mimics the behaviour of CB3ExecutorPool, which counts _all_
834843
// tasks across all taskables. This may or may not be the correct
835844
// behaviour...
845+
// The counting is done on the eventbase thread given it would be
846+
// racy to directly access the taskOwners from this thread.
847+
auto* eventBase = futurePool->getEventBase();
836848
std::array<int, NUM_TASK_GROUPS> waitingTasksPerGroup;
837-
for (const auto& owner : taskOwnersCopy) {
838-
for (const auto& task : owner.second) {
839-
if (task.second->isScheduled()) {
840-
const auto type =
841-
GlobalTask::getTaskType(task.second->task->getTaskId());
842-
waitingTasksPerGroup[type]++;
843-
}
844-
}
845-
}
849+
eventBase->runInEventBaseThreadAndWait(
850+
[state = this->state.get(), &waitingTasksPerGroup] {
851+
waitingTasksPerGroup = state->getWaitingTasksPerGroup();
852+
});
846853

847854
// Currently FollyExecutorPool implements a single task queue (per task
848855
// type) - report that as low priority.

0 commit comments

Comments
 (0)