Skip to content

[CELEBORN-2459] Fix race causing DataPusher to accept data during termination and drop it - #3844

Open
rjvkr2021 wants to merge 3 commits into
apache:mainfrom
rjvkr2021:rajeevkumar/CELEBORN-2459
Open

[CELEBORN-2459] Fix race causing DataPusher to accept data during termination and drop it#3844
rjvkr2021 wants to merge 3 commits into
apache:mainfrom
rjvkr2021:rajeevkumar/CELEBORN-2459

Conversation

@rjvkr2021

@rjvkr2021 rjvkr2021 commented Sep 9, 2026

Copy link
Copy Markdown

What changes were proposed in this pull request?

This PR fixes a race causing DataPusher to accept data during termination and drop it.

Why are the changes needed?

Background

When a task wants to push shuffle data to a Celeborn worker, it does not push the
data directly. Instead, the data is copied into a reusable buffer and enqueued
for asynchronous processing.

The producer returns successfully after the buffer is accepted by the queue. A
background thread later dequeues the buffer, pushes its data to the worker, and
returns the buffer to the reusable buffer pool.

Race

The race occurs when the background thread is being terminated. In the original
implementation, termination waits for the reusable buffer pool (idleQueue)
to become full and then sets terminated=true.

The check and the state change are not an atomic admission barrier. After the
buffer-pool check succeeds but before terminated is set, a producer can
borrow a reusable buffer, copy data into it, enqueue it successfully, and
return from addTask().

The background thread can then observe terminated, stop without processing
the newly enqueued buffer, and termination can clear the queue.

Impact

The producer has reported success even though the data was never pushed to the
worker. This causes silent data loss.

Fix

Task admission and background thread termination must be ordered through an explicit lifecycle:

RUNNING -> CLOSING -> TERMINATED

When termination begins, the pusher transitions to CLOSING before waiting for
existing work to finish. New calls to addTask() are rejected in CLOSING,
while producers admitted in RUNNING are allowed to complete. After all
admitted producers and queued tasks have been drained, the pusher transitions
to TERMINATED.

This guarantees that every successful addTask() corresponds to work admitted
before termination and therefore handled before termination completes.

Does this PR resolve a correctness bug?

  • Yes

Does this PR introduce any user-facing change?

No

How was this patch tested?

Added following unit tests:

  1. testDataPusherRejectsTasksWhileClosing
  2. testDataPusherDrainsTaskAdmittedBeforeTermination

@cxzl25
cxzl25 requested a lite review from Copilot September 9, 2026 09:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Fixes a race in DataPusher’s shutdown path where tasks could be admitted during termination and then dropped, by introducing an explicit lifecycle state machine and admission barrier.

Changes:

  • Add RUNNING -> CLOSING -> TERMINATED lifecycle gating for addTask() vs waitOnTermination().
  • Track and drain in-flight producers before allowing termination to proceed.
  • Add unit tests covering admission rejection while closing and draining of pre-admitted work.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
client/src/main/java/org/apache/celeborn/client/write/DataPusher.java Introduces lifecycle state + producer-drain coordination to prevent task admission during termination.
client/src/test/java/org/apache/celeborn/client/write/DataPushQueueSuiteJ.java Adds unit tests that exercise the new closing/termination behavior under concurrency.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

lifecycleLock.lockInterruptibly();
try {
if (lifecycleState != LifecycleState.RUNNING) {
throw new IOException("DataPusher is closing or terminated");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

updated the exception message accordingly

@rjvkr2021

Copy link
Copy Markdown
Author

@cxzl25 can you please review the pr? i have addressed all the comments left by copilot.

@github-actions github-actions Bot added the correctness Correctness bugfix label Sep 9, 2026
@rjvkr2021

Copy link
Copy Markdown
Author

@cxzl25 just a gentle reminder.


dataPusher.addTask(0, new byte[1], 1);
AtomicReference<Throwable> terminationFailure = new AtomicReference<>();
Thread terminationThread =

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.

Is this a real production environment case?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don’t know. Actually, I was investigating something else when I discovered this race.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants