Skip to content

HIVE-29780: Fix LLAP plugin rendezvous race under concurrent DAGs (NullPointerException in LlapTaskCommunicator.setResponse) - #6659

Open
abstractdog wants to merge 1 commit into
apache:masterfrom
abstractdog:HIVE-29780
Open

HIVE-29780: Fix LLAP plugin rendezvous race under concurrent DAGs (NullPointerException in LlapTaskCommunicator.setResponse)#6659
abstractdog wants to merge 1 commit into
apache:masterfrom
abstractdog:HIVE-29780

Conversation

@abstractdog

@abstractdog abstractdog commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Replace the class-static single-slot rendezvous between LlapTaskCommunicator and LlapTaskSchedulerService with a per-ApplicationAttemptId handshake. Each side now parks itself in a ConcurrentMap<ApplicationAttemptId, ThisSide> keyed by the appAttemptId both plugins for the same DAG share (via TaskCommunicatorContext.getApplicationAttemptId() / TaskSchedulerContext.getApplicationAttemptId()). The existing pluginInitLock stays so the pair-or-park section is atomic per DAG. shutdown() on each side reaps its own leftover entry with ConcurrentMap.remove(key, this) so a failed DAG init doesn't leak.

Why are the changes needed?

The single-slot handshake pairs plugins across DAGs when multiple DAGs come up concurrently in the same JVM: the communicator from DAG-A ends up bound to the scheduler from DAG-B, DAG-A's real communicator is left with scheduler == null, and the first task submission hits

java.lang.NullPointerException: Cannot invoke
  "LlapTaskSchedulerService.notifyStarted(TezTaskAttemptID)"
  because "this.this$0.scheduler" is null
    at LlapTaskCommunicator$3.setResponse(LlapTaskCommunicator.java:540)

which surfaces as Vertex Map 1 killed/failed due to:OWN_TASK_FAILUREMoveTask return code 2. In a 30-way concurrent-INSERT reproduction on MiniHS2 (LLAP) 5 of 30 sessions failed with this NPE.

Does this PR introduce any user-facing change?

No. A production Tez DAGAppMaster runs one DAG at a time so the race is not reachable there, and the map holds at most one entry at any moment; the hot paths (task submission, scheduling) are unchanged.

How was this patch tested?

30-way concurrent INSERT burst on MiniHS2 (clusterType=llap, -DminiHS2.isMetastoreRemote=true) against an insert-only managed partitioned table. Before: 25/30 sessions succeeded, 5 hit the NPE and hive.log had 120 stack traces from Tez task-attempt retries. After: 30/30 sessions succeeded, zero notifyStarted NPEs in hive.log, all 30 writers landed in their own subdirectory as expected for insert-only ACID.

@abstractdog
abstractdog force-pushed the HIVE-29780 branch 2 times, most recently from 8b20674 to df2cc85 Compare July 29, 2026 14:53
@abstractdog abstractdog changed the title HIVE-29780: Fix LLAP plugin rendezvous race under concurrent DAGs (NullPointerException in LlapTaskCommunicator$3.setResponse) HIVE-29780: Fix LLAP plugin rendezvous race under concurrent DAGs (NullPointerException in LlapTaskCommunicator.setResponse) Jul 30, 2026
@abstractdog

Copy link
Copy Markdown
Contributor Author

I'll fix related failures in TestLlapTaskSchedulerService

Copilot AI 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.

Pull request overview

This PR addresses a Tez LLAP plugin initialization race where concurrent DAGs in the same JVM could cross-wire LlapTaskCommunicator and LlapTaskSchedulerService, leading to scheduler == null and a NullPointerException in LlapTaskCommunicator.setResponse. It replaces the previous single static-slot rendezvous with a per-ApplicationAttemptId handshake so each DAG’s plugin pair is matched only with its counterpart.

Changes:

  • Replace the class-static single “instance” rendezvous with ConcurrentMap<ApplicationAttemptId, ...> parking/removal on both plugins.
  • Key the handshake on TaskCommunicatorContext.getApplicationAttemptId() / TaskSchedulerContext.getApplicationAttemptId() while keeping the existing init lock for atomic pairing.

Reviewed changes

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

File Description
llap-tez/src/java/org/apache/hadoop/hive/llap/tezplugins/LlapTaskSchedulerService.java Parks/removes schedulers by ApplicationAttemptId to avoid cross-DAG plugin pairing.
llap-tez/src/java/org/apache/hadoop/hive/llap/tezplugins/LlapTaskCommunicator.java Parks/removes communicators by ApplicationAttemptId to avoid cross-DAG plugin pairing and documents the race.
Comments suppressed due to low confidence (1)

llap-tez/src/java/org/apache/hadoop/hive/llap/tezplugins/LlapTaskCommunicator.java:176

  • pendingCommunicators entries are never removed if the communicator parks itself (put) and then gets shut down before the matching scheduler is constructed. That can leak the communicator instance and also lets a later scheduler init for the same ApplicationAttemptId pair against a stopped communicator (reintroducing null/incorrect wiring). Consider storing the appAttemptId on the instance and removing the entry in shutdown (e.g., synchronized on pluginInitLock, pendingCommunicators.remove(appAttemptId, this)).
    ApplicationAttemptId appAttemptId = getContext().getApplicationAttemptId();
    synchronized (pluginInitLock) {
      LlapTaskSchedulerService peer = LlapTaskSchedulerService.pendingSchedulers.remove(appAttemptId);
      if (peer != null) {
        // We are the last to initialize for this DAG.
        peer.setTaskCommunicator(this);
        this.setScheduler(peer);
      } else {
        pendingCommunicators.put(appAttemptId, this);
      }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sonarqubecloud

Copy link
Copy Markdown

@illiabarbashov-sketch illiabarbashov-sketch 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.

LGTM. A couple of questions/clarifications asked.

this.appAttemptId = getContext().getApplicationAttemptId();
synchronized (pluginInitLock) {
LlapTaskSchedulerService peer = LlapTaskSchedulerService.instance;
LlapTaskSchedulerService peer = LlapTaskSchedulerService.pendingSchedulers.remove(appAttemptId);

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.

Would like to know what do you think about extracting this handshake logic into a separate singleton class LlapPluginRendezvous since test already bears that name? The benefit is that this would allow us to avoid static fields cross read between those two classes. While the separate singleton class will work as an intermediary for both.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

absolutely makes sense!
in the long term, tez might have its own solution for the same (I can also find a ticket where this came up in tez), but now, it would be a good to move all this hack to a separate class

@Override
public void shutdown() {
super.shutdown();
// Reap our rendezvous entry if we parked ourselves at construction and are being torn down

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.

I don't know if we want to provide that much of a context in the comment for 3-liner? On the other hand, that might be another +1 to a separate class to handle rendezvous handshake, we can comment the purpose in the javadoc of that class. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

right, it's just not necessary to explain it too much here

…llPointerException in LlapTaskCommunicator.setResponse)

LlapTaskCommunicator and LlapTaskSchedulerService are constructed
independently by the Tez app-master and have to find each other to
wire the callback that reports task starts back to the scheduler
(scheduler.notifyStarted(taskAttemptID) in the SubmitWork response
callback). They used a class-static handshake:

    static final Object pluginInitLock = new Object();
    static LlapTaskCommunicator instance = null;   // one slot
    static LlapTaskSchedulerService instance = null;

    // in each constructor, under pluginInitLock:
    if (peer.instance != null) { pair(); peer.instance = null; }
    else { this.instance = this; }

This single-slot handshake is race-prone when multiple DAGs come up
concurrently in the same JVM. The communicator from DAG-A could end up
paired with the scheduler from DAG-B (whichever happened to park itself
in "instance" first), and DAG-A's real communicator was then left with
scheduler == null. The first task submission for that DAG then hit
LlapTaskCommunicator$3.setResponse:540 → scheduler.notifyStarted(...)
→ NPE, and the vertex died with OWN_TASK_FAILURE / return code 2.

A production Tez DAGAppMaster only runs one DAG at a time, so this race
is not reachable there. It shows up in JVMs that host many concurrent
DAGs — MiniHS2, MiniLlapCluster, tests that fan out concurrent inserts.
In a 30-way concurrent insert-only INSERT reproduction on MiniHS2 this
took out ~5 of 30 sessions.

Fix: key the rendezvous on ApplicationAttemptId (both TaskCommunicator
Context and TaskSchedulerContext expose it, and the two plugins for the
same DAG share it). Replace the class-static "instance" slot with a
ConcurrentMap<ApplicationAttemptId, ThisSide>. The pluginInitLock stays
so the pair-or-park section remains atomic per DAG. shutdown() on each
side reaps its own leftover entry via ConcurrentMap.remove(key, this),
so a failed DAG init that only constructed one plugin doesn't leak.

Production impact: none. The map holds at most one entry per DAG init;
in a production AM (one DAG per AM lifetime) that's at most one entry
total, put and removed at plugin construction / shutdown. Nothing on
the task submission or scheduling hot path changes.

Verification (30-way concurrent INSERT into an insert-only managed
partitioned Parquet table on s3a://):
  Before: 25 of 30 sessions succeeded, 5 hit the NPE (hive.log had
          120 stack traces from Tez task-attempt retries).
  After:  30 of 30 sessions succeeded, hive.log has 0 notifyStarted
          NPEs. All 30 writers landed in their own
          delta_<writeId>_<writeId>_0000 subdirectory as expected for
          insert-only ACID.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants