Skip to content

HIVE-29780: Fix LLAP plugin rendezvous race under concurrent DAGs (NullPointerException in LlapTaskCommunicator$3.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$3.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.

…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.
@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants