Skip to content

[Dataflow Streaming] Avoid LinkedBlockingQueue.removeIf data race in BoundedQueueExecutor - #40812

Merged
damccorm merged 1 commit into
apache:masterfrom
damccorm:fix-bounded-queue-executor-tsan
Oct 28, 2026
Merged

damccorm merged 1 commit into
apache:masterfrom
damccorm:fix-bounded-queue-executor-tsan

Conversation

@damccorm

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #40593.

BoundedQueueExecutor.onWorkStolen() calls workQueue.removeIf(BoundedQueueExecutor::isStaleShell) on LinkedBlockingQueue while worker threads concurrently dequeue tasks via ThreadPoolExecutor.getTask() -> LinkedBlockingQueue.poll().

In OpenJDK's LinkedBlockingQueue.bulkRemove(Predicate) (used by removeIf), the non-volatile head field (Node<E> p = null, ancestor = head;) and Node.item field (nodes[i].item) are read outside fullyLock(), racing with concurrent consumer threads writing head = first and first.item = null in LinkedBlockingQueue.dequeue() under takeLock. This triggers a ThreadSanitizer (TSAN) data race in BoundedQueueExecutorTest.testConcurrentExecuteAndPollWork_processesEachWorkExactlyOnce.

Replace workQueue.removeIf(...) with workQueue.iterator() + Iterator.remove() (matching the idiom used by ThreadPoolExecutor.purge()):

  • LinkedBlockingQueue.Itr acquires fullyLock() for all reads/writes of head, Node.next, and Node.item, while still caching ancestor across remove() calls for amortized O(1) unlinking per element (O(n) total scan).
  • If executor.shutdown() races concurrently with queue removal, invoke executor.purge() so ThreadPoolExecutor.tryTerminate() is notified in case the queue became empty after shutdown() 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

@github-actions github-actions Bot added runners dataflow size/s S: 10-49 lines changed labels Oct 26, 2026
@damccorm
damccorm marked this pull request as ready for review October 26, 2026 13:59
@damccorm

Copy link
Copy Markdown
Contributor Author

R: @arunpandianp

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.bulkRemove reading non-volatile head and Node.item fields outside fullyLock() is spot on.
  • The replacement iterator loop and post-removal isShutdown() -> executor.purge() check cleanly mirror ThreadPoolExecutor.purge() semantics and maintain thread-safety without performance regressions.

💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.

@github-actions

Copy link
Copy Markdown
Contributor

Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment assign set of reviewers

@codecov

codecov Bot commented Oct 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.97%. Comparing base (a8d6afd) to head (2c5bdcc).
⚠️ Report is 86 commits behind head on master.

Files with missing lines Patch % Lines
...ers/dataflow/worker/util/BoundedQueueExecutor.java 75.00% 1 Missing and 1 partial ⚠️
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     
Flag Coverage Δ
java 75.97% <75.00%> (?)
python ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@damccorm
damccorm merged commit bd682df into apache:master Oct 28, 2026
19 of 21 checks passed
@damccorm
damccorm deleted the fix-bounded-queue-executor-tsan branch October 28, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dataflow runners size/s S: 10-49 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants