Repository navigation
Conversation
…BoundedQueueExecutor
There was a problem hiding this comment.
📋 Review Summary
This PR resolves a ThreadSanitizer (TSAN) data race in BoundedQueueExecutor.onWorkStolen() by replacing LinkedBlockingQueue.removeIf(...) with explicit iteration and Iterator.remove(). The change properly ensures all queue and node accesses acquire fullyLock(), while preserving amortized O(1) per-element unlinking and safely notifying ThreadPoolExecutor termination via executor.purge() if shutdown races with removal.
🔍 General Feedback
- The root cause analysis of
LinkedBlockingQueue.bulkRemovereading non-volatileheadandNode.itemfields outsidefullyLock()is spot on. - The replacement iterator loop and post-removal
isShutdown()->executor.purge()check cleanly mirrorThreadPoolExecutor.purge()semantics and maintain thread-safety without performance regressions.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
|
Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #40812 +/- ##
============================================
- Coverage 80.04% 75.97% -4.08%
- Complexity 0 2275 +2275
============================================
Files 490 259 -231
Lines 82916 14384 -68532
Branches 0 1482 +1482
============================================
- Hits 66373 10928 -55445
+ Misses 16543 2764 -13779
- Partials 0 692 +692
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Summary
Follow-up to #40593.
BoundedQueueExecutor.onWorkStolen()callsworkQueue.removeIf(BoundedQueueExecutor::isStaleShell)onLinkedBlockingQueuewhile worker threads concurrently dequeue tasks viaThreadPoolExecutor.getTask() -> LinkedBlockingQueue.poll().In OpenJDK's
LinkedBlockingQueue.bulkRemove(Predicate)(used byremoveIf), the non-volatileheadfield (Node<E> p = null, ancestor = head;) andNode.itemfield (nodes[i].item) are read outsidefullyLock(), racing with concurrent consumer threads writinghead = firstandfirst.item = nullinLinkedBlockingQueue.dequeue()undertakeLock. This triggers a ThreadSanitizer (TSAN) data race inBoundedQueueExecutorTest.testConcurrentExecuteAndPollWork_processesEachWorkExactlyOnce.Replace
workQueue.removeIf(...)withworkQueue.iterator()+Iterator.remove()(matching the idiom used byThreadPoolExecutor.purge()):LinkedBlockingQueue.ItracquiresfullyLock()for all reads/writes ofhead,Node.next, andNode.item, while still cachingancestoracrossremove()calls for amortized O(1) unlinking per element (O(n) total scan).executor.shutdown()races concurrently with queue removal, invokeexecutor.purge()soThreadPoolExecutor.tryTerminate()is notified in case the queue became empty aftershutdown()checked it.Testing
./gradlew :runners:google-cloud-dataflow-java:worker:test --tests org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutorTest --tests org.apache.beam.runners.dataflow.worker.util.KeyGroupWorkIndexTest