This is an automated email from the ASF dual-hosted git repository.

abstractdog pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tez.git


The following commit(s) were added to refs/heads/master by this push:
     new 8fde3534b TEZ-4732: MergeManager: deadlock when usedMemory exceeds 
limit but commitMemory is below merge threshold (#515) (Ryu Kobayashi reviewed 
by Laszlo Bodor)
8fde3534b is described below

commit 8fde3534b0e5a127e96c24aa0ae804060844adcd
Author: Ryu Kobayashi <[email protected]>
AuthorDate: Wed Jul 1 18:05:20 2026 +0900

    TEZ-4732: MergeManager: deadlock when usedMemory exceeds limit but 
commitMemory is below merge threshold (#515) (Ryu Kobayashi reviewed by Laszlo 
Bodor)
---
 .../shuffle/orderedgrouped/MergeManager.java       |  9 ++-
 .../shuffle/orderedgrouped/TestMergeManager.java   | 75 ++++++++++++++++++++++
 2 files changed, 83 insertions(+), 1 deletion(-)

diff --git 
a/tez-runtime-library/src/main/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/MergeManager.java
 
b/tez-runtime-library/src/main/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/MergeManager.java
index 5e1354d8f..fa5d5df9e 100644
--- 
a/tez-runtime-library/src/main/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/MergeManager.java
+++ 
b/tez-runtime-library/src/main/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/MergeManager.java
@@ -402,7 +402,14 @@ public class MergeManager implements 
FetchedInputAllocatorOrderedGrouped {
   public synchronized void waitForShuffleToMergeMemory() throws 
InterruptedException {
     long startTime = System.currentTimeMillis();
     while(usedMemory > memoryLimit) {
-      wait();
+      wait(1000);
+      if (usedMemory > memoryLimit && !inMemoryMapOutputs.isEmpty()) {
+        // Avoid deadlock: if memory is over limit but commitMemory is below 
mergeThreshold,
+        // no merge will be triggered automatically. Force a merge to free 
memory.
+        LOG.info("Memory limit exceeded with no merge triggered 
(usedMemory={}, commitMemory={}, mergeThreshold={})."
+            + " Forcing in-memory merge to avoid deadlock.", usedMemory, 
commitMemory, mergeThreshold);
+        startMemToDiskMerge();
+      }
     }
     if (LOG.isDebugEnabled()) {
       LOG.debug("Waited for " + (System.currentTimeMillis() - startTime) + " 
for memory to become"
diff --git 
a/tez-runtime-library/src/test/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/TestMergeManager.java
 
b/tez-runtime-library/src/test/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/TestMergeManager.java
index 048699de2..caac28ca6 100644
--- 
a/tez-runtime-library/src/test/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/TestMergeManager.java
+++ 
b/tez-runtime-library/src/test/java/org/apache/tez/runtime/library/common/shuffle/orderedgrouped/TestMergeManager.java
@@ -19,6 +19,7 @@
 package org.apache.tez.runtime.library.common.shuffle.orderedgrouped;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.atLeastOnce;
@@ -255,6 +256,80 @@ public class TestMergeManager {
     assertEquals(data1.length + data2.length, mergeManager.getUsedMemory());
   }
 
+  @Test(timeout = 30000)
+  public void testWaitForShuffleToMergeMemoryNoDeadlock() throws Throwable {
+    Configuration conf = new TezConfiguration(defaultConf);
+    conf.setBoolean(TezRuntimeConfiguration.TEZ_RUNTIME_COMPRESS, false);
+    conf.set(TezRuntimeConfiguration.TEZ_RUNTIME_KEY_CLASS, 
IntWritable.class.getName());
+    conf.set(TezRuntimeConfiguration.TEZ_RUNTIME_VALUE_CLASS, 
IntWritable.class.getName());
+
+    Path localDir = new Path(workDir, "local");
+    localFs.mkdirs(localDir);
+    conf.setStrings(TezRuntimeFrameworkConfigs.LOCAL_DIRS, 
localDir.toString());
+
+    FileSystem fs = FileSystem.getLocal(conf);
+    LocalDirAllocator localDirAllocator = new 
LocalDirAllocator(TezRuntimeFrameworkConfigs.LOCAL_DIRS);
+    InputContext inputContext = 
createMockInputContext(UUID.randomUUID().toString());
+    ExceptionReporter exceptionReporter = mock(ExceptionReporter.class);
+
+    // initialMemoryAvailable=2000000: memoryLimit=2000000, 
mergeThreshold=1800000, maxSingleShuffleLimit=500000
+    MergeManager mergeManager = new MergeManager(conf, fs, localDirAllocator, 
inputContext,
+        null, null, null, null, exceptionReporter, 2000000, null, false, -1);
+    mergeManager.configureAndStart();
+
+    // Generate valid IFile data (~490000 bytes, below 
maxSingleShuffleLimit=500000)
+    InputAttemptIdentifier id1 = new InputAttemptIdentifier(0, 0);
+    InputAttemptIdentifier id2 = new InputAttemptIdentifier(1, 0);
+    InputAttemptIdentifier id3 = new InputAttemptIdentifier(2, 0);
+    InputAttemptIdentifier id4 = new InputAttemptIdentifier(3, 0);
+    InputAttemptIdentifier id5 = new InputAttemptIdentifier(4, 0);
+    byte[] data1 = generateDataBySize(conf, 490000, id1);
+    byte[] data2 = generateDataBySize(conf, 490000, id2);
+
+    // Reserve and commit 2 outputs: commitMemory ~= 980000 < 
mergeThreshold=1800000 (no auto-merge)
+    MapOutput mo1 = mergeManager.reserve(id1, data1.length, data1.length, 0);
+    MapOutput mo2 = mergeManager.reserve(id2, data2.length, data2.length, 0);
+    assertEquals(MapOutput.Type.MEMORY, mo1.getType());
+    assertEquals(MapOutput.Type.MEMORY, mo2.getType());
+    System.arraycopy(data1, 0, mo1.getMemory(), 0, data1.length);
+    System.arraycopy(data2, 0, mo2.getMemory(), 0, data2.length);
+    mo1.commit();
+    mo2.commit();
+
+    // Reserve 3 more outputs without committing to push usedMemory > 
memoryLimit=2000000
+    MapOutput mo3 = mergeManager.reserve(id3, data1.length, data1.length, 0);
+    MapOutput mo4 = mergeManager.reserve(id4, data1.length, data1.length, 0);
+    MapOutput mo5 = mergeManager.reserve(id5, data1.length, data1.length, 0);
+    assertEquals(MapOutput.Type.MEMORY, mo3.getType());
+    assertEquals(MapOutput.Type.MEMORY, mo4.getType());
+    assertEquals(MapOutput.Type.MEMORY, mo5.getType());
+
+    // Verify deadlock precondition: usedMemory > memoryLimit, commitMemory < 
mergeThreshold
+    assertTrue("usedMemory should exceed memoryLimit", 
mergeManager.getUsedMemory() > 2000000L);
+    assertTrue("commitMemory should be below mergeThreshold", 
mergeManager.getCommitMemory() < 1800000L);
+
+    // waitForShuffleToMergeMemory() should complete without deadlock:
+    // the fix forces a merge when memory is exhausted but commitMemory < 
mergeThreshold
+    Thread waitThread = new Thread(() -> {
+      try {
+        mergeManager.waitForShuffleToMergeMemory();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    });
+    waitThread.start();
+    waitThread.join(15000);
+    assertFalse("waitForShuffleToMergeMemory deadlocked", 
waitThread.isAlive());
+
+    if (waitThread.isAlive()) {
+      waitThread.interrupt();
+    }
+    mo3.abort();
+    mo4.abort();
+    mo5.abort();
+    mergeManager.close(true);
+  }
+
   @Test
   public void testDiskMergeWithCodec() throws Throwable {
     Configuration conf = new TezConfiguration(defaultConf);

Reply via email to