This is an automated email from the ASF dual-hosted git repository.
sunchao pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 80554625ed68 [SPARK-57242][CORE] Avoid unbounded page allocation
retries after allocator OOM
80554625ed68 is described below
commit 80554625ed689b790fdee3c1576b38ca8fb585d3
Author: Chao Sun <[email protected]>
AuthorDate: Mon Jun 22 11:37:08 2026 -0700
[SPARK-57242][CORE] Avoid unbounded page allocation retries after allocator
OOM
### Why are the changes needed?
Spark page allocation has two distinct steps:
1. `MemoryManager` grants the task permission to use execution memory.
2. The Tungsten `MemoryAllocator` physically allocates the on-heap or
off-heap page.
An execution-memory grant does not guarantee that the physical allocator
can create the page. The allocator may still throw `OutOfMemoryError`, for
example because of memory pressure outside Spark's execution-memory accounting
or because a sufficiently large allocation cannot be satisfied.
When that happens, `TaskMemoryManager.allocatePage()` currently retains the
grant as acquired-but-unused memory and recursively calls `allocatePage()`:
```text
acquire grant G1 -> allocator OOM -> retain G1 -> recurse
acquire grant G2 -> allocator OOM -> retain G2 -> recurse
...
```
Each retry asks for another execution-memory grant even though the task has
not physically allocated the previous one. There is no check that retrying made
progress. Under a persistent allocator/accounting mismatch, the task can pin an
increasing amount of execution memory, recurse repeatedly, and eventually block
waiting for more execution memory or fail far away from the original allocator
OOM.
This is the generic `TaskMemoryManager` failure path underlying the
long-running allocation retry described by
[SPARK-54354](https://issues.apache.org/jira/browse/SPARK-54354). That issue
bounded temporary memory managers used by hashed relations, while
[SPARK-54818](https://issues.apache.org/jira/browse/SPARK-54818) improved
allocator OOM diagnostics. The recursive retry behavior remains for other
page-allocation consumers.
Recovery should remain bounded while preserving the useful behavior of the
old implementation:
- retry the original grant while spilling releases tracked task memory;
- allow a smaller partial-page retry after spilling, or from one additional
free-tail grant;
- return allocation failure after those bounded attempts are exhausted.
### What changes were proposed in this PR?
For `TaskMemoryManager`:
- Replace recursive page allocation with bounded iterative recovery.
- After allocator OOM, spill registered task-memory consumers directly
without acquiring another fair-share execution-memory grant.
- Measure progress using the consumer's tracked memory before and after
spilling.
- Retry the original grant while spilling makes progress, then attempt a
smaller page based on the memory released.
- Preserve one bounded additional execution-memory acquisition so callers
can still receive a usable partial page from a free-tail grant.
- Prevent page allocations made from inside either recovery spilling path
from recursively entering allocator recovery.
- Track acquired-but-unused portions of successful partial pages and make
cleanup idempotent.
- Mark pages returned by the final minimum-size retry after allocator
failure so callers can distinguish them from ordinary exact-fit grants.
The direct spill path can reset `ShuffleExternalSorter` while record
insertion is in progress, so this PR also makes its pointer-array lifecycle
safe for that recovery path:
- `ShuffleInMemorySorter.reset()` frees its pointer array and allocates the
replacement lazily, outside the spill callback.
- `ShuffleExternalSorter` reuses successful pointer-array growth after a
spill instead of collapsing back to a one-record array.
- If that retained growth leaves no room for the first data page, the empty
sorter falls back to its minimum usable pointer array and retries the data-page
allocation once.
- Record insertion rechecks pointer-array capacity after data-page
allocation because that allocation may have triggered a spill and reset the
array.
- Empty iteration and cleanup remain valid while the replacement pointer
array has not yet been allocated.
- Spill accounting includes both data pages and the pointer array actually
released.
For consumers that cannot safely retain one exact-fit page per record:
- `BytesToBytesMap` rejects marked minimum-retry pages and falls back
instead of consuming one page-table slot per record.
- Python `HybridQueue` frees exact-fit partial pages and falls back to its
disk queue.
Normal page allocation does not enter the allocator-OOM recovery loop.
Shuffle record insertion adds one inexpensive pointer-capacity check after
data-page allocation so it can recover if that allocation spilled and reset the
sorter. This introduces no new configuration or public API.
### How was this PR tested?
Added deterministic `TaskMemoryManagerSuite` coverage for:
- allocator OOM with no spillable memory
- successful spill and same-grant retry
- partial-page retry after spilling
- partial-page retry from a free-tail grant without spilling
- nested page allocation during spill and free-tail recovery
- off-heap allocator failure
- idempotent failed-grant cleanup
- preserving ordinary exact-fit grants while rejecting pathological
minimum-retry pages, including after another consumer spills
Added Python row-queue coverage for exact-fit partial-page fallback to disk.
Added shuffle sorter coverage for:
- lazy pointer-array allocation after reset
- cleanup while the pointer array is lazily unallocated
- complete spill accounting
- minimum pointer-array growth
- retaining successful pointer-array growth after a growth allocation
triggers spill
- avoiding per-record spills with a one-record initial buffer under both
TimSort and radix sort
- data-page allocation triggering a spill during record insertion
Validation performed:
- `TaskMemoryManagerSuite`
- `BytesToBytesMapOnHeapSuite`
- `BytesToBytesMapOffHeapSuite`
- `RowQueueSuite`
- `ShuffleInMemorySorterSuite`
- `ShuffleInMemoryRadixSorterSuite`
- `ShuffleExternalSorterSuite`
- `UnsafeShuffleWriterSuite`
- `UnsafeExternalSorterSuite`
- Core compilation and Checkstyle
- Scalafmt validation
- `git diff --check`
Closes #56293 from
sunchao/dev/chao/codex/spark-57242-allocator-oom-recovery.
Authored-by: Chao Sun <[email protected]>
Signed-off-by: Chao Sun <[email protected]>
(cherry picked from commit bbe84d5fe4875a18e1ea3aa10825f42012c399b0)
Signed-off-by: Chao Sun <[email protected]>
---
.../org/apache/spark/memory/MemoryConsumer.java | 12 +-
.../org/apache/spark/memory/TaskMemoryManager.java | 339 +++++++++++--
.../spark/shuffle/sort/ShuffleExternalSorter.java | 73 ++-
.../spark/shuffle/sort/ShuffleInMemorySorter.java | 60 ++-
.../apache/spark/unsafe/map/BytesToBytesMap.java | 10 +-
.../spark/memory/TaskMemoryManagerSuite.java | 563 +++++++++++++++++++++
.../shuffle/sort/ShuffleInMemorySorterSuite.java | 28 +
.../shuffle/sort/UnsafeShuffleWriterSuite.java | 16 +
.../shuffle/sort/ShuffleExternalSorterSuite.scala | 386 +++++++++++---
.../spark/sql/execution/python/HybridQueue.scala | 11 +-
.../spark/sql/execution/python/RowQueueSuite.scala | 30 +-
11 files changed, 1382 insertions(+), 146 deletions(-)
diff --git a/core/src/main/java/org/apache/spark/memory/MemoryConsumer.java
b/core/src/main/java/org/apache/spark/memory/MemoryConsumer.java
index 65d62f68ad5b..d2ee6269c134 100644
--- a/core/src/main/java/org/apache/spark/memory/MemoryConsumer.java
+++ b/core/src/main/java/org/apache/spark/memory/MemoryConsumer.java
@@ -73,7 +73,9 @@ public abstract class MemoryConsumer {
*
* This should be implemented by subclass.
*
- * Note: In order to avoid possible deadlock, should not call
acquireMemory() from spill().
+ * Note: In order to avoid possible deadlock, implementations must release
memory synchronously
+ * on the calling thread and must not acquire task memory from spill(),
either directly or from
+ * another thread.
*
* Note: today, this only frees Tungsten-managed pages.
*
@@ -115,7 +117,8 @@ public abstract class MemoryConsumer {
* @throws SparkOutOfMemoryError
*/
protected MemoryBlock allocatePage(long required) {
- MemoryBlock page = taskMemoryManager.allocatePage(Math.max(pageSize,
required), this);
+ MemoryBlock page =
+ taskMemoryManager.allocatePageWithMinimum(Math.max(pageSize, required),
required, this);
if (page == null || page.size() < required) {
throwOom(page, required);
}
@@ -131,6 +134,11 @@ public abstract class MemoryConsumer {
taskMemoryManager.freePage(page, this);
}
+ /** Returns whether this page came from a minimum retry after a partial
allocation failed. */
+ protected boolean isPageAllocationFromMinimumRetry(MemoryBlock page) {
+ return taskMemoryManager.isPageAllocationFromMinimumRetry(page);
+ }
+
/**
* Allocates memory of `size`.
*/
diff --git a/core/src/main/java/org/apache/spark/memory/TaskMemoryManager.java
b/core/src/main/java/org/apache/spark/memory/TaskMemoryManager.java
index 7d2a3fb63f9e..099d95c6f9de 100644
--- a/core/src/main/java/org/apache/spark/memory/TaskMemoryManager.java
+++ b/core/src/main/java/org/apache/spark/memory/TaskMemoryManager.java
@@ -29,6 +29,7 @@ import org.apache.spark.internal.SparkLogger;
import org.apache.spark.internal.SparkLoggerFactory;
import org.apache.spark.internal.LogKeys;
import org.apache.spark.internal.MDC;
+import org.apache.spark.unsafe.memory.MemoryAllocator;
import org.apache.spark.unsafe.memory.MemoryBlock;
import org.apache.spark.util.Utils;
@@ -94,8 +95,14 @@ public class TaskMemoryManager {
*/
private final BitSet allocatedPages = new BitSet(PAGE_TABLE_SIZE);
+ /** Pages allocated at the caller's minimum after a partial allocation also
failed. */
+ @GuardedBy("this")
+ private final BitSet pagesAllocatedFromMinimumRetry = new
BitSet(PAGE_TABLE_SIZE);
+
private final MemoryManager memoryManager;
+ private final MemoryAllocator tungstenMemoryAllocator;
+
private final long taskAttemptId;
/**
@@ -114,7 +121,25 @@ public class TaskMemoryManager {
/**
* The amount of memory that is acquired but not used.
*/
- private volatile long acquiredButNotUsed = 0L;
+ @GuardedBy("this")
+ private long acquiredButNotUsed = 0L;
+
+ /**
+ * Prevent nested page allocations while spilling from recursively entering
allocator recovery.
+ */
+ private final ThreadLocal<Boolean> inPageAllocationRecovery =
+ ThreadLocal.withInitial(() -> false);
+
+ private static final class PageAllocationRequest {
+ private MemoryConsumer consumer;
+ private long minimumSize;
+ }
+
+ /**
+ * Carries a padded page request's minimum usable size through the existing
virtual allocatePage
+ * entry point, so subclasses overriding that method continue to intercept
page allocations.
+ */
+ private final ThreadLocal<PageAllocationRequest> pageAllocationRequest = new
ThreadLocal<>();
/**
* Current off heap memory usage by this task.
@@ -144,8 +169,17 @@ public class TaskMemoryManager {
* Construct a new TaskMemoryManager.
*/
public TaskMemoryManager(MemoryManager memoryManager, long taskAttemptId) {
+ this(memoryManager, taskAttemptId,
memoryManager.tungstenMemoryAllocator());
+ }
+
+ @VisibleForTesting
+ TaskMemoryManager(
+ MemoryManager memoryManager,
+ long taskAttemptId,
+ MemoryAllocator tungstenMemoryAllocator) {
this.tungstenMemoryMode = memoryManager.tungstenMemoryMode();
this.memoryManager = memoryManager;
+ this.tungstenMemoryAllocator = tungstenMemoryAllocator;
this.taskAttemptId = taskAttemptId;
this.consumers = new HashSet<>();
}
@@ -253,6 +287,24 @@ public class TaskMemoryManager {
int idx) {
MemoryMode mode = requestingConsumer.getMode();
MemoryConsumer consumerToSpill = cList.get(idx);
+ long released = spillConsumer(requestingConsumer, requested,
consumerToSpill);
+ if (released > 0) {
+ // When our spill handler releases memory,
`ExecutionMemoryPool#releaseMemory()` will
+ // immediately notify other tasks that memory has been freed, and they
may acquire the
+ // newly-freed memory before we have a chance to do so (SPARK-35486).
Therefore we may
+ // not be able to acquire all the memory that was just spilled. In that
case, we will
+ // try again in the next loop iteration.
+ return memoryManager.acquireExecutionMemory(requested, taskAttemptId,
mode);
+ } else {
+ cList.remove(idx);
+ return 0;
+ }
+ }
+
+ private long spillConsumer(
+ MemoryConsumer requestingConsumer,
+ long requested,
+ MemoryConsumer consumerToSpill) {
if (logger.isDebugEnabled()) {
logger.debug("Task {} try to spill {} from {} for {}", taskAttemptId,
Utils.bytesToString(requested), consumerToSpill, requestingConsumer);
@@ -265,17 +317,8 @@ public class TaskMemoryManager {
Utils.bytesToString(released), Utils.bytesToString(requested),
consumerToSpill,
requestingConsumer);
}
-
- // When our spill handler releases memory,
`ExecutionMemoryPool#releaseMemory()` will
- // immediately notify other tasks that memory has been freed, and they
may acquire the
- // newly-freed memory before we have a chance to do so (SPARK-35486).
Therefore we may
- // not be able to acquire all the memory that was just spilled. In
that case, we will
- // try again in the next loop iteration.
- return memoryManager.acquireExecutionMemory(requested, taskAttemptId,
mode);
- } else {
- cList.remove(idx);
- return 0;
}
+ return released;
} catch (ClosedByInterruptException | InterruptedIOException e) {
// This called by user to kill a task (e.g: speculative task).
logger.error("Error while calling spill() on {}", e,
@@ -295,6 +338,96 @@ public class TaskMemoryManager {
}
}
+ /**
+ * Spill task-managed memory after the allocator rejects a grant which the
memory manager thought
+ * was available. Unlike acquireExecutionMemory(), this does not request
another grant and cannot
+ * block waiting for fair-share memory.
+ */
+ private synchronized long spillConsumersForPageAllocation(
+ long required,
+ MemoryConsumer requestingConsumer) {
+ TreeMap<Long, List<MemoryConsumer>> sortedConsumers = new TreeMap<>();
+ for (MemoryConsumer c : consumers) {
+ if (c.getUsed() > 0 && c.getMode() == requestingConsumer.getMode()) {
+ long key = c == requestingConsumer ? 0 : c.getUsed();
+ List<MemoryConsumer> list =
+ sortedConsumers.computeIfAbsent(key, k -> new ArrayList<>(1));
+ list.add(c);
+ }
+ }
+
+ long released = 0L;
+ while (released < required && !sortedConsumers.isEmpty()) {
+ Map.Entry<Long, List<MemoryConsumer>> currentEntry =
+ sortedConsumers.ceilingEntry(required - released);
+ if (currentEntry == null) {
+ currentEntry = sortedConsumers.lastEntry();
+ }
+ List<MemoryConsumer> cList = currentEntry.getValue();
+ int idx = cList.size() - 1;
+ MemoryConsumer consumerToSpill = cList.get(idx);
+ long usedBeforeSpill = consumerToSpill.getUsed();
+ spillConsumer(requestingConsumer, required - released, consumerToSpill);
+ // Measure net tracked memory released. Spill callbacks must not
reacquire execution memory;
+ // if a custom consumer violates that contract, conservatively treat it
as making no progress.
+ long actuallyReleased = Math.max(0L, usedBeforeSpill -
consumerToSpill.getUsed());
+ if (actuallyReleased > 0) {
+ released = Math.addExact(released, actuallyReleased);
+ } else {
+ cList.remove(idx);
+ }
+ if (cList.isEmpty()) {
+ sortedConsumers.remove(currentEntry.getKey());
+ }
+ }
+ return released;
+ }
+
+ private long recoverFromPageAllocationFailure(
+ long required,
+ MemoryConsumer requestingConsumer) {
+ if (inPageAllocationRecovery.get()) {
+ return 0;
+ }
+
+ inPageAllocationRecovery.set(true);
+ try {
+ return spillConsumersForPageAllocation(required, requestingConsumer);
+ } finally {
+ inPageAllocationRecovery.remove();
+ }
+ }
+
+ private long acquireAdditionalExecutionMemoryForPageAllocation(
+ long required,
+ MemoryConsumer requestingConsumer) {
+ if (inPageAllocationRecovery.get()) {
+ return 0;
+ }
+
+ inPageAllocationRecovery.set(true);
+ try {
+ return acquireExecutionMemory(required, requestingConsumer);
+ } finally {
+ inPageAllocationRecovery.remove();
+ }
+ }
+
+ private void logPageAllocationFailure(long allocationSize, int retryCount,
OutOfMemoryError e) {
+ try {
+ if (retryCount == 0) {
+ logger.warn("Failed to allocate a page ({} bytes), try spilling task
memory.", e,
+ MDC.of(LogKeys.PAGE_SIZE, allocationSize));
+ } else {
+ logger.warn("Failed to allocate a page ({} bytes) after {} spill
retries.",
+ MDC.of(LogKeys.PAGE_SIZE, allocationSize),
+ MDC.of(LogKeys.NUM_RETRY, retryCount));
+ }
+ } catch (OutOfMemoryError ignored) {
+ // Preserve allocator recovery even if diagnostics cannot allocate
memory.
+ }
+ }
+
/**
* Release N bytes of execution memory for a MemoryConsumer.
*/
@@ -355,10 +488,6 @@ public class TaskMemoryManager {
return memoryManager.pageSizeBytes();
}
- public MemoryBlock allocatePage(long size, MemoryConsumer consumer) {
- return allocatePage(size, consumer, 0);
- }
-
/**
* Allocate a block of memory that will be tracked in the MemoryManager's
page table; this is
* intended for allocating large blocks of Tungsten memory that will be
shared between operators.
@@ -368,12 +497,45 @@ public class TaskMemoryManager {
*
* @throws TooLargePageException
*/
- private MemoryBlock allocatePage(
+ public MemoryBlock allocatePage(long size, MemoryConsumer consumer) {
+ PageAllocationRequest request = pageAllocationRequest.get();
+ long minimumSize = request != null && request.consumer == consumer
+ ? Math.min(request.minimumSize, size)
+ : size;
+ return allocatePageInternal(size, minimumSize, consumer);
+ }
+
+ MemoryBlock allocatePageWithMinimum(
+ long size,
+ long minimumSize,
+ MemoryConsumer consumer) {
+ assert(minimumSize >= 0 && minimumSize <= size);
+ PageAllocationRequest request = pageAllocationRequest.get();
+ if (request == null) {
+ request = new PageAllocationRequest();
+ pageAllocationRequest.set(request);
+ }
+ MemoryConsumer previousConsumer = request.consumer;
+ long previousMinimumSize = request.minimumSize;
+ request.consumer = consumer;
+ request.minimumSize = minimumSize;
+ try {
+ return allocatePage(size, consumer);
+ } finally {
+ request.consumer = previousConsumer;
+ request.minimumSize = previousMinimumSize;
+ }
+ }
+
+ private MemoryBlock allocatePageInternal(
long size,
- MemoryConsumer consumer,
- int retryCount) {
+ long minimumSize,
+ MemoryConsumer consumer) {
assert(consumer != null);
assert(consumer.getMode() == tungstenMemoryMode);
+ if (inPageAllocationRecovery.get()) {
+ return null;
+ }
if (size > MAXIMUM_PAGE_SIZE_BYTES) {
throw new TooLargePageException(size);
}
@@ -394,33 +556,108 @@ public class TaskMemoryManager {
allocatedPages.set(pageNumber);
}
MemoryBlock page = null;
+ boolean pageAllocated = false;
+ int retryCount = 0;
+ long allocationSize = acquired;
+ long partialAllocationSize = 0;
+ boolean tryingPartialAllocation = false;
+ boolean minimumRetryAfterPartialAllocationFailure = false;
try {
- page = memoryManager.tungstenMemoryAllocator().allocate(acquired);
- } catch (OutOfMemoryError e) {
- if (retryCount == 0) {
- logger.warn("Failed to allocate a page ({} bytes) for {} times, try
again.", e,
- MDC.of(LogKeys.PAGE_SIZE, acquired),
- MDC.of(LogKeys.NUM_RETRY, retryCount));
- } else {
- logger.warn("Failed to allocate a page ({} bytes) for {} times, try
again.",
- MDC.of(LogKeys.PAGE_SIZE, acquired),
- MDC.of(LogKeys.NUM_RETRY, retryCount));
+ while (true) {
+ try {
+ page = tungstenMemoryAllocator.allocate(allocationSize);
+ break;
+ } catch (OutOfMemoryError e) {
+ logPageAllocationFailure(allocationSize, retryCount, e);
+ if (tryingPartialAllocation) {
+ if (minimumSize > 0 && minimumSize < allocationSize) {
+ // Reuse the retained grant for one final attempt at the
caller's usable minimum.
+ long surplus = Math.subtractExact(acquired, minimumSize);
+ releaseExecutionMemory(surplus, consumer);
+ acquired = minimumSize;
+ allocationSize = minimumSize;
+ minimumRetryAfterPartialAllocationFailure = true;
+ retryCount++;
+ continue;
+ }
+ return null;
+ }
+ long released = recoverFromPageAllocationFailure(allocationSize,
consumer);
+ if (released > 0) {
+ long remaining = allocationSize - partialAllocationSize;
+ partialAllocationSize += Math.min(released, remaining);
+ } else if (partialAllocationSize > 0 && partialAllocationSize <
allocationSize) {
+ // Preserve one bounded attempt to combine the memory made
available by spilling with
+ // any remaining free-tail grant, then fall back to a partial page
for callers that can
+ // use one. The additional grant may already include the spilled
memory, so do not add
+ // it to partialAllocationSize.
+ long additionalAcquired =
+ acquireAdditionalExecutionMemoryForPageAllocation(size,
consumer);
+ if (additionalAcquired > 0) {
+ long overlap =
+ additionalAcquired >= partialAllocationSize ?
partialAllocationSize : 0L;
+ if (overlap > 0) {
+ releaseExecutionMemory(overlap, consumer);
+ }
+ acquired = Math.addExact(acquired, additionalAcquired - overlap);
+ }
+ allocationSize =
+ additionalAcquired > 0 ? additionalAcquired :
partialAllocationSize;
+ tryingPartialAllocation = true;
+ } else if (partialAllocationSize == 0) {
+ // Preserve one bounded attempt to acquire a smaller free-tail
grant. The previous
+ // recursive implementation could return a partial page this way
after retaining the
+ // rejected grant, but could also retry without bound.
+ long additionalAcquired =
+ acquireAdditionalExecutionMemoryForPageAllocation(size,
consumer);
+ if (additionalAcquired <= 0) {
+ return null;
+ }
+ acquired = Math.addExact(acquired, additionalAcquired);
+ allocationSize = additionalAcquired;
+ tryingPartialAllocation = true;
+ } else if (minimumSize > 0 && minimumSize < allocationSize) {
+ // The original grant is still reserved. If the caller padded a
smaller allocation to
+ // the configured page size, make one bounded attempt at the
minimum usable size without
+ // acquiring more execution memory.
+ long surplus = Math.subtractExact(acquired, minimumSize);
+ releaseExecutionMemory(surplus, consumer);
+ acquired = minimumSize;
+ allocationSize = minimumSize;
+ tryingPartialAllocation = true;
+ minimumRetryAfterPartialAllocationFailure = true;
+ } else {
+ return null;
+ }
+ retryCount++;
+ }
}
- // there is no enough memory actually, it means the actual free memory
is smaller than
- // MemoryManager thought, we should keep the acquired memory.
+ page.pageNumber = pageNumber;
+ pageTable[pageNumber] = page;
synchronized (this) {
- acquiredButNotUsed += acquired;
- allocatedPages.clear(pageNumber);
+ acquiredButNotUsed = Math.addExact(acquiredButNotUsed, acquired -
page.size());
+ if (minimumRetryAfterPartialAllocationFailure) {
+ pagesAllocatedFromMinimumRetry.set(pageNumber);
+ }
+ }
+ pageAllocated = true;
+ if (logger.isTraceEnabled()) {
+ logger.trace("Allocate page number {} ({} bytes)", pageNumber,
allocationSize);
+ }
+ return page;
+ } finally {
+ if (!pageAllocated) {
+ if (page != null) {
+ page.pageNumber = MemoryBlock.FREED_IN_TMM_PAGE_NUMBER;
+ tungstenMemoryAllocator.free(page);
+ }
+ synchronized (this) {
+ pageTable[pageNumber] = null;
+ allocatedPages.clear(pageNumber);
+ acquiredButNotUsed = Math.addExact(acquiredButNotUsed, acquired);
+ }
}
- // this could trigger spilling to free some pages.
- return allocatePage(size, consumer, retryCount + 1);
- }
- page.pageNumber = pageNumber;
- pageTable[pageNumber] = page;
- if (logger.isTraceEnabled()) {
- logger.trace("Allocate page number {} ({} bytes)", pageNumber, acquired);
}
- return page;
}
/**
@@ -437,6 +674,7 @@ public class TaskMemoryManager {
pageTable[page.pageNumber] = null;
synchronized (this) {
allocatedPages.clear(page.pageNumber);
+ pagesAllocatedFromMinimumRetry.clear(page.pageNumber);
}
if (logger.isTraceEnabled()) {
logger.trace("Freed page number {} ({} bytes)", page.pageNumber,
page.size());
@@ -446,10 +684,19 @@ public class TaskMemoryManager {
// Doing this allows the MemoryAllocator to detect when a
TaskMemoryManager-managed
// page has been inappropriately directly freed without calling
TMM.freePage().
page.pageNumber = MemoryBlock.FREED_IN_TMM_PAGE_NUMBER;
- memoryManager.tungstenMemoryAllocator().free(page);
+ tungstenMemoryAllocator.free(page);
releaseExecutionMemory(pageSize, consumer);
}
+ boolean isPageAllocationFromMinimumRetry(MemoryBlock page) {
+ synchronized (this) {
+ int pageNumber = page.pageNumber;
+ return pageNumber >= 0 && pageNumber < PAGE_TABLE_SIZE &&
+ allocatedPages.get(pageNumber) &&
+ pagesAllocatedFromMinimumRetry.get(pageNumber);
+ }
+ }
+
/**
* Given a memory page and offset within that page, encode this address into
a 64-bit long.
* This address will remain valid as long as the corresponding page has not
been freed.
@@ -526,6 +773,7 @@ public class TaskMemoryManager {
* value can be used to detect memory leaks.
*/
public long cleanUpAllAllocatedMemory() {
+ final long acquiredButNotUsedToRelease;
synchronized (this) {
for (MemoryConsumer c: consumers) {
if (c != null && c.getUsed() > 0) {
@@ -543,14 +791,19 @@ public class TaskMemoryManager {
logger.debug("unreleased page: {} in task {}", page,
taskAttemptId);
}
page.pageNumber = MemoryBlock.FREED_IN_TMM_PAGE_NUMBER;
- memoryManager.tungstenMemoryAllocator().free(page);
+ tungstenMemoryAllocator.free(page);
}
}
Arrays.fill(pageTable, null);
+ allocatedPages.clear();
+ pagesAllocatedFromMinimumRetry.clear();
+ acquiredButNotUsedToRelease = acquiredButNotUsed;
+ acquiredButNotUsed = 0L;
}
// release the memory that is not used by any consumer (acquired for pages
in tungsten mode).
- memoryManager.releaseExecutionMemory(acquiredButNotUsed, taskAttemptId,
tungstenMemoryMode);
+ memoryManager.releaseExecutionMemory(
+ acquiredButNotUsedToRelease, taskAttemptId, tungstenMemoryMode);
return memoryManager.releaseAllExecutionMemoryForTask(taskAttemptId);
}
diff --git
a/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleExternalSorter.java
b/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleExternalSorter.java
index f053135c4dbd..f899698cecb8 100644
---
a/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleExternalSorter.java
+++
b/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleExternalSorter.java
@@ -304,7 +304,8 @@ final class ShuffleExternalSorter extends MemoryConsumer
implements ShuffleCheck
}
writeSortedFile(false);
- final long spillSize = freeMemory();
+ final long spillSize = getMemoryUsage();
+ freeMemory();
inMemSorter.reset();
// Reset the in-memory sorter's pointer array only after freeing up the
memory pages holding the
// records. Otherwise, if the task is over allocated memory, then without
freeing the memory
@@ -363,6 +364,11 @@ final class ShuffleExternalSorter extends MemoryConsumer
implements ShuffleCheck
}
}
+ private void allocateInitialPointerArray() {
+ LongArray array =
allocateArray(inMemSorter.getInitialSizeWithUsableCapacity());
+ inMemSorter.expandPointerArray(array);
+ }
+
/**
* Checks whether there is enough space to insert an additional record in to
the sort pointer
* array and grows the array if additional space is required. If the
required space cannot be
@@ -371,29 +377,59 @@ final class ShuffleExternalSorter extends MemoryConsumer
implements ShuffleCheck
private void growPointerArrayIfNecessary() throws IOException {
assert(inMemSorter != null);
if (!inMemSorter.hasSpaceForAnotherRecord()) {
+ if (!inMemSorter.hasPointerArray()) {
+ allocateInitialPointerArray();
+ return;
+ }
+
long used = inMemSorter.getMemoryUsage();
- LongArray array;
+ LongArray array = null;
try {
// could trigger spilling
array = allocateArray(used / 8 * 2);
} catch (TooLargePageException e) {
// The pointer array is too big to fix in a single page, spill.
spill();
- return;
} catch (SparkOutOfMemoryError e) {
// should have trigger spilling
- if (!inMemSorter.hasSpaceForAnotherRecord()) {
+ if (!"UNABLE_TO_ACQUIRE_MEMORY".equals(e.getCondition()) ||
+ inMemSorter.hasPointerArray()) {
logger.error("Unable to grow the pointer array");
throw e;
}
- return;
}
// check if spilling is triggered or not
- if (inMemSorter.hasSpaceForAnotherRecord()) {
- freeArray(array);
- } else {
- inMemSorter.expandPointerArray(array);
+ if (!inMemSorter.hasPointerArray()) {
+ // A spill reset the pointer array while allocateArray() was in
progress. Reuse a successful
+ // growth allocation, or restore the minimum usable initial array if
allocation failed.
+ if (array != null) {
+ inMemSorter.expandPointerArray(array);
+ } else {
+ allocateInitialPointerArray();
+ }
+ return;
+ }
+ inMemSorter.expandPointerArray(array);
+ }
+ }
+
+ private void acquireNewPageWithPointerArrayFallback(int required) {
+ try {
+ acquireNewPageIfNecessary(required);
+ } catch (SparkOutOfMemoryError e) {
+ long minimumPointerArrayBytes =
+ Math.multiplyExact(inMemSorter.getInitialSizeWithUsableCapacity(), 8L);
+ if (!"UNABLE_TO_ACQUIRE_MEMORY".equals(e.getCondition()) ||
+ inMemSorter.numRecords() != 0 ||
+ inMemSorter.getMemoryUsage() <= minimumPointerArrayBytes) {
+ throw e;
}
+ // A growth allocation retained after spilling can consume all memory
made available by the
+ // spill. Since the sorter is still empty, shrink the pointer array and
retry the data page
+ // once so that pointer growth cannot starve record storage.
+ inMemSorter.reset();
+ allocateInitialPointerArray();
+ acquireNewPageIfNecessary(required);
}
}
@@ -447,7 +483,24 @@ final class ShuffleExternalSorter extends MemoryConsumer
implements ShuffleCheck
final int uaoSize = UnsafeAlignedOffset.getUaoSize();
// Need 4 or 8 bytes to store the record length.
final int required = length + uaoSize;
- acquireNewPageIfNecessary(required);
+ acquireNewPageWithPointerArrayFallback(required);
+ // Data page allocation may spill and reset the pointer array, so check
its capacity again.
+ try {
+ growPointerArrayIfNecessary();
+ } catch (SparkOutOfMemoryError e) {
+ if (!"UNABLE_TO_ACQUIRE_MEMORY".equals(e.getCondition()) ||
+ inMemSorter.hasPointerArray() ||
+ inMemSorter.numRecords() != 0 ||
+ currentPage == null ||
+ allocatedPages.size() != 1) {
+ throw e;
+ }
+ // The newly acquired empty data page consumed the remaining fair-share
memory. Release it,
+ // restore the minimum pointer array first, and retry the data page once.
+ freeMemory();
+ allocateInitialPointerArray();
+ acquireNewPageIfNecessary(required);
+ }
assert(currentPage != null);
final Object base = currentPage.getBaseObject();
diff --git
a/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorter.java
b/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorter.java
index 7ab522d26c7d..49aa0434a48e 100644
---
a/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorter.java
+++
b/core/src/main/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorter.java
@@ -76,9 +76,13 @@ final class ShuffleInMemorySorter {
}
private int getUsableCapacity() {
+ return getUsableCapacity(array.size());
+ }
+
+ private int getUsableCapacity(long size) {
// Radix sort requires same amount of used memory as buffer, Tim sort
requires
// half of the used memory as buffer.
- return (int) (array.size() / (useRadixSort ? 2 : 1.5));
+ return (int) (size / (useRadixSort ? 2 : 1.5));
}
public void free() {
@@ -92,33 +96,47 @@ final class ShuffleInMemorySorter {
return pos;
}
+ public int getInitialSize() {
+ return initialSize;
+ }
+
+ public long getInitialSizeWithUsableCapacity() {
+ long size = initialSize;
+ while (getUsableCapacity(size) == 0) {
+ size = Math.multiplyExact(size, 2L);
+ }
+ return size;
+ }
+
+ public boolean hasPointerArray() {
+ return array != null;
+ }
+
public void reset() {
- // Reset `pos` here so that `spill` triggered by the below `allocateArray`
will be no-op.
pos = 0;
if (consumer != null) {
- consumer.freeArray(array);
- // As `array` has been released, we should set it to `null` to avoid
accessing it before
- // `allocateArray` returns. `usableCapacity` is also set to `0` to avoid
any codes writing
- // data to `ShuffleInMemorySorter` when `array` is `null` (e.g., in
- // ShuffleExternalSorter.growPointerArrayIfNecessary, we may try to
access
- // `ShuffleInMemorySorter` when `allocateArray` throws
SparkOutOfMemoryError).
+ if (array != null) {
+ consumer.freeArray(array);
+ }
+ // Allocate the replacement lazily. reset() is called while spilling,
and allocating here can
+ // recursively trigger another spill while a partially complete
allocation is still retained.
array = null;
usableCapacity = 0;
- array = consumer.allocateArray(initialSize);
- usableCapacity = getUsableCapacity();
}
}
public void expandPointerArray(LongArray newArray) {
- assert(newArray.size() > array.size());
- Platform.copyMemory(
- array.getBaseObject(),
- array.getBaseOffset(),
- newArray.getBaseObject(),
- newArray.getBaseOffset(),
- pos * 8L
- );
- consumer.freeArray(array);
+ if (array != null) {
+ assert(newArray.size() > array.size());
+ Platform.copyMemory(
+ array.getBaseObject(),
+ array.getBaseOffset(),
+ newArray.getBaseObject(),
+ newArray.getBaseOffset(),
+ pos * 8L
+ );
+ consumer.freeArray(array);
+ }
array = newArray;
usableCapacity = getUsableCapacity();
}
@@ -182,6 +200,10 @@ final class ShuffleInMemorySorter {
* Return an iterator over record pointers in sorted order.
*/
public ShuffleSorterIterator getSortedIterator() {
+ if (pos == 0) {
+ return new ShuffleSorterIterator(0, array, 0);
+ }
+
int offset = 0;
if (useRadixSort) {
offset = RadixSort.sort(
diff --git
a/core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java
b/core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java
index 486bfd62bc97..fc1bcd8bb336 100644
--- a/core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java
+++ b/core/src/main/java/org/apache/spark/unsafe/map/BytesToBytesMap.java
@@ -836,11 +836,19 @@ public final class BytesToBytesMap extends MemoryConsumer
{
* @return whether there is enough space to allocate the new page.
*/
private boolean acquireNewPage(long required) {
+ final MemoryBlock page;
try {
- currentPage = allocatePage(required);
+ page = allocatePage(required);
} catch (SparkOutOfMemoryError e) {
return false;
}
+ // Retaining exact-fit minimum-retry pages would consume one page-table
slot per map entry.
+ if (required < pageSizeBytes && page.size() == required &&
+ isPageAllocationFromMinimumRetry(page)) {
+ freePage(page);
+ return false;
+ }
+ currentPage = page;
dataPages.add(currentPage);
UnsafeAlignedOffset.putSize(currentPage.getBaseObject(),
currentPage.getBaseOffset(), 0);
pageCursor = UnsafeAlignedOffset.getUaoSize();
diff --git
a/core/src/test/java/org/apache/spark/memory/TaskMemoryManagerSuite.java
b/core/src/test/java/org/apache/spark/memory/TaskMemoryManagerSuite.java
index 25543690b832..6c408c0c928b 100644
--- a/core/src/test/java/org/apache/spark/memory/TaskMemoryManagerSuite.java
+++ b/core/src/test/java/org/apache/spark/memory/TaskMemoryManagerSuite.java
@@ -17,16 +17,156 @@
package org.apache.spark.memory;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.apache.spark.SparkConf;
+import org.apache.spark.unsafe.Platform;
+import org.apache.spark.unsafe.UnsafeAlignedOffset;
+import org.apache.spark.unsafe.map.BytesToBytesMap;
import org.apache.spark.unsafe.memory.MemoryAllocator;
import org.apache.spark.unsafe.memory.MemoryBlock;
import org.apache.spark.internal.config.package$;
public class TaskMemoryManagerSuite {
+ private static final class TestAllocator implements MemoryAllocator {
+ private int failuresRemaining;
+ private int allocationAttempts;
+
+ TestAllocator(int failuresRemaining) {
+ this.failuresRemaining = failuresRemaining;
+ }
+
+ @Override
+ public MemoryBlock allocate(long size) throws OutOfMemoryError {
+ allocationAttempts++;
+ if (failuresRemaining > 0) {
+ failuresRemaining--;
+ // checkstyle.off: RegexpSinglelineJava
+ throw new OutOfMemoryError("test allocator failure");
+ // checkstyle.on: RegexpSinglelineJava
+ }
+ return MemoryAllocator.HEAP.allocate(size);
+ }
+
+ @Override
+ public void free(MemoryBlock memory) {
+ MemoryAllocator.HEAP.free(memory);
+ }
+ }
+
+ private static final class SizeLimitedAllocator implements MemoryAllocator {
+ private final long maximumAllocationSize;
+ private int allocationAttempts;
+ private long lastAllocationSize;
+
+ SizeLimitedAllocator(long maximumAllocationSize) {
+ this.maximumAllocationSize = maximumAllocationSize;
+ }
+
+ @Override
+ public MemoryBlock allocate(long size) throws OutOfMemoryError {
+ allocationAttempts++;
+ lastAllocationSize = size;
+ if (size > maximumAllocationSize) {
+ // checkstyle.off: RegexpSinglelineJava
+ throw new OutOfMemoryError("test allocator failure");
+ // checkstyle.on: RegexpSinglelineJava
+ }
+ return MemoryAllocator.HEAP.allocate(size);
+ }
+
+ @Override
+ public void free(MemoryBlock memory) {
+ MemoryAllocator.HEAP.free(memory);
+ }
+ }
+
+ private static final class CompetingTaskAllocator implements MemoryAllocator
{
+ private final MemoryManager memoryManager;
+ private int allocationAttempts;
+
+ CompetingTaskAllocator(MemoryManager memoryManager) {
+ this.memoryManager = memoryManager;
+ }
+
+ @Override
+ public MemoryBlock allocate(long size) throws OutOfMemoryError {
+ allocationAttempts++;
+ if (allocationAttempts == 2) {
+ long acquired =
+ memoryManager.acquireExecutionMemory(2048L, 1L, MemoryMode.ON_HEAP);
+ Assertions.assertEquals(2048L, acquired);
+ }
+ if (size > 1024L) {
+ // checkstyle.off: RegexpSinglelineJava
+ throw new OutOfMemoryError("test allocator failure");
+ // checkstyle.on: RegexpSinglelineJava
+ }
+ return MemoryAllocator.HEAP.allocate(size);
+ }
+
+ @Override
+ public void free(MemoryBlock memory) {
+ MemoryAllocator.HEAP.free(memory);
+ }
+ }
+
+ private static final class PageAllocatingConsumer extends MemoryConsumer {
+ PageAllocatingConsumer(TaskMemoryManager manager, long pageSize) {
+ super(manager, pageSize, MemoryMode.ON_HEAP);
+ }
+
+ MemoryBlock allocate(long required) {
+ return allocatePage(required);
+ }
+
+ void freeAllocatedPage(MemoryBlock page) {
+ freePage(page);
+ }
+
+ @Override
+ public long spill(long size, MemoryConsumer trigger) {
+ return 0;
+ }
+ }
+
+ private static final class AllocatingSpillConsumer extends
TestMemoryConsumer {
+ private MemoryBlock nestedPage;
+
+ AllocatingSpillConsumer(TaskMemoryManager manager) {
+ super(manager);
+ }
+
+ @Override
+ public long spill(long size, MemoryConsumer trigger) throws IOException {
+ nestedPage = taskMemoryManager.allocatePage(256, this);
+ long used = getUsed();
+ free(used);
+ return used;
+ }
+ }
+
+ private static final class NonSpillingAllocatingConsumer extends
TestMemoryConsumer {
+ private int spillAttempts;
+ private MemoryBlock nestedPage;
+
+ NonSpillingAllocatingConsumer(TaskMemoryManager manager) {
+ super(manager);
+ }
+
+ @Override
+ public long spill(long size, MemoryConsumer trigger) {
+ spillAttempts++;
+ nestedPage = taskMemoryManager.allocatePage(256, this);
+ return 0;
+ }
+ }
+
@Test
public void leakedPageMemoryIsDetected() {
final TaskMemoryManager manager = new TaskMemoryManager(
@@ -102,6 +242,429 @@ public class TaskMemoryManagerSuite {
Assertions.assertThrows(AssertionError.class, () ->
manager.freePage(dataPage, c));
}
+ @Test
+ public void pageAllocationFailureWithoutSpillableMemoryReturnsNull() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(4096);
+ final TestAllocator allocator = new TestAllocator(Integer.MAX_VALUE);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final MemoryConsumer c = new TestMemoryConsumer(manager);
+
+ Assertions.assertNull(manager.allocatePage(4096, c));
+ Assertions.assertEquals(1, allocator.allocationAttempts);
+ Assertions.assertEquals(4096, manager.getMemoryConsumptionForThisTask());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ }
+
+ @Test
+ public void pageAllocationFailureSpillsAndRetriesTheSameGrant() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(5120);
+ final TestAllocator allocator = new TestAllocator(1);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final TestMemoryConsumer existingConsumer = new
TestMemoryConsumer(manager);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+ existingConsumer.use(1024);
+
+ final MemoryBlock page = requestingConsumer.allocate(4096);
+ Assertions.assertNotNull(page);
+ Assertions.assertEquals(2, allocator.allocationAttempts);
+ Assertions.assertEquals(0, existingConsumer.getUsed());
+ Assertions.assertEquals(4096, requestingConsumer.getUsed());
+ Assertions.assertEquals(4096, manager.getMemoryConsumptionForThisTask());
+
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(0, requestingConsumer.getUsed());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ }
+
+ @Test
+ public void pageAllocationFailureCanRetryWithPartialPage() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(5120);
+ final SizeLimitedAllocator allocator = new SizeLimitedAllocator(1024);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final TestMemoryConsumer existingConsumer = new
TestMemoryConsumer(manager);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+ existingConsumer.use(1024);
+
+ final MemoryBlock page = requestingConsumer.allocate(1024);
+ Assertions.assertEquals(1024, page.size());
+ Assertions.assertEquals(3, allocator.allocationAttempts);
+ Assertions.assertEquals(1024, allocator.lastAllocationSize);
+ Assertions.assertEquals(0, existingConsumer.getUsed());
+ Assertions.assertEquals(1024, requestingConsumer.getUsed());
+ Assertions.assertEquals(4096, manager.getMemoryConsumptionForThisTask());
+
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(0, requestingConsumer.getUsed());
+ Assertions.assertEquals(3072, manager.getMemoryConsumptionForThisTask());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void pageAllocationFailureCombinesSpillWithFreeTail() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(6144);
+ final SizeLimitedAllocator allocator = new SizeLimitedAllocator(2048);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final TestMemoryConsumer existingConsumer = new
TestMemoryConsumer(manager);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+ existingConsumer.use(1024);
+
+ final MemoryBlock page = requestingConsumer.allocate(2048);
+ Assertions.assertEquals(2048, page.size());
+ Assertions.assertEquals(3, allocator.allocationAttempts);
+ Assertions.assertEquals(2048, allocator.lastAllocationSize);
+ Assertions.assertEquals(0, existingConsumer.getUsed());
+ Assertions.assertEquals(2048, requestingConsumer.getUsed());
+ Assertions.assertEquals(5120, manager.getMemoryConsumptionForThisTask());
+
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(0, requestingConsumer.getUsed());
+ Assertions.assertEquals(3072, manager.getMemoryConsumptionForThisTask());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void
pageAllocationFailurePreservesSmallerGrantAfterCompetingAcquisition() {
+ final SparkConf conf = new SparkConf(false)
+ .set("spark.testing", "true")
+ .set("spark.testing.memory", "10240")
+ .set("spark.memory.fraction", "1.0")
+ .set("spark.memory.storageFraction", "0.5");
+ final MemoryManager memoryManager =
UnifiedMemoryManager$.MODULE$.apply(conf, 1);
+ final CompetingTaskAllocator allocator = new
CompetingTaskAllocator(memoryManager);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final TestMemoryConsumer existingConsumer = new
TestMemoryConsumer(manager);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+ existingConsumer.use(2048);
+
+ final MemoryBlock page = requestingConsumer.allocate(1024);
+ Assertions.assertEquals(1024, page.size());
+ Assertions.assertEquals(3, allocator.allocationAttempts);
+ Assertions.assertEquals(0, existingConsumer.getUsed());
+ Assertions.assertEquals(1024, requestingConsumer.getUsed());
+ Assertions.assertEquals(5120, manager.getMemoryConsumptionForThisTask());
+
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(4096, manager.getMemoryConsumptionForThisTask());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ memoryManager.releaseExecutionMemory(2048L, 1L, MemoryMode.ON_HEAP);
+ }
+
+ @Test
+ public void pageAllocationFailureReleasesSurplusGrantAfterFullSpill() {
+ final SparkConf conf = new SparkConf(false)
+ .set("spark.testing", "true")
+ .set("spark.testing.memory", "10240")
+ .set("spark.memory.fraction", "1.0")
+ .set("spark.memory.storageFraction", "0.5");
+ final MemoryManager memoryManager =
UnifiedMemoryManager$.MODULE$.apply(conf, 1);
+ final CompetingTaskAllocator allocator = new
CompetingTaskAllocator(memoryManager);
+ final AtomicInteger acquireCalls = new AtomicInteger();
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator) {
+ @Override
+ public long acquireExecutionMemory(long size, MemoryConsumer consumer) {
+ Assertions.assertEquals(
+ 1, acquireCalls.incrementAndGet(), "page recovery must not acquire
another grant");
+ return super.acquireExecutionMemory(size, consumer);
+ }
+ };
+ final TestMemoryConsumer existingConsumer = new
TestMemoryConsumer(manager);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+ existingConsumer.use(4096);
+ acquireCalls.set(0);
+
+ final MemoryBlock page = requestingConsumer.allocate(1024);
+ Assertions.assertEquals(1, acquireCalls.get());
+ Assertions.assertEquals(1024, page.size());
+ Assertions.assertEquals(3, allocator.allocationAttempts);
+ Assertions.assertEquals(0, existingConsumer.getUsed());
+ Assertions.assertEquals(1024, requestingConsumer.getUsed());
+ Assertions.assertEquals(1024, manager.getMemoryConsumptionForThisTask());
+
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ memoryManager.releaseExecutionMemory(2048L, 1L, MemoryMode.ON_HEAP);
+ }
+
+ @Test
+ public void pageAllocationFailureCanRetryWithFreeTail() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(5120);
+ final SizeLimitedAllocator allocator = new SizeLimitedAllocator(1024);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+
+ final MemoryBlock page = requestingConsumer.allocate(1024);
+ Assertions.assertNotNull(page);
+ Assertions.assertEquals(1024, page.size());
+ Assertions.assertEquals(2, allocator.allocationAttempts);
+ Assertions.assertEquals(1024, allocator.lastAllocationSize);
+ Assertions.assertEquals(1024, requestingConsumer.getUsed());
+ Assertions.assertEquals(5120, manager.getMemoryConsumptionForThisTask());
+
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(0, requestingConsumer.getUsed());
+ Assertions.assertEquals(4096, manager.getMemoryConsumptionForThisTask());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void pageAllocationFailureCanRetryMinimumAfterFullFreeTailGrant() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(9216);
+ final SizeLimitedAllocator allocator = new SizeLimitedAllocator(1024);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+
+ final MemoryBlock page = requestingConsumer.allocate(1024);
+ Assertions.assertEquals(1024, page.size());
+ Assertions.assertEquals(3, allocator.allocationAttempts);
+ Assertions.assertEquals(1024, allocator.lastAllocationSize);
+ Assertions.assertEquals(1024, requestingConsumer.getUsed());
+ Assertions.assertEquals(1024, manager.getMemoryConsumptionForThisTask());
+
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(0, requestingConsumer.getUsed());
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void pageAllocationFailureMinimumRetryIsBounded() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(9216);
+ final SizeLimitedAllocator allocator = new SizeLimitedAllocator(0);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+
+ Assertions.assertThrows(
+ SparkOutOfMemoryError.class, () -> requestingConsumer.allocate(1024));
+ Assertions.assertEquals(3, allocator.allocationAttempts);
+ Assertions.assertEquals(1024, allocator.lastAllocationSize);
+ Assertions.assertEquals(0, requestingConsumer.getUsed());
+ Assertions.assertEquals(1024, manager.getMemoryConsumptionForThisTask());
+
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void bytesToBytesMapAcceptsExactFitInitialGrant() {
+ final long pageSize = 4096L;
+ final long recordPageSize =
+ (3L * UnsafeAlignedOffset.getUaoSize()) + (3L * Long.BYTES);
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(1024L + recordPageSize);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0);
+ final BytesToBytesMap map = new BytesToBytesMap(manager, 64, pageSize);
+ final long[] row = new long[]{1L};
+
+ try {
+ final BytesToBytesMap.Location location =
+ map.lookup(row, Platform.LONG_ARRAY_OFFSET, Long.BYTES);
+ Assertions.assertTrue(location.append(
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES,
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES));
+ Assertions.assertEquals(
+ 1024L + recordPageSize, manager.getMemoryConsumptionForThisTask());
+ } finally {
+ map.free();
+ }
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void bytesToBytesMapAcceptsExactFitFreeTailRetry() {
+ final long pageSize = 4096L;
+ final long recordPageSize =
+ (3L * UnsafeAlignedOffset.getUaoSize()) + (3L * Long.BYTES);
+ final AtomicInteger exactFitAllocations = new AtomicInteger();
+ final MemoryAllocator allocator = new MemoryAllocator() {
+ @Override
+ public MemoryBlock allocate(long size) throws OutOfMemoryError {
+ if (size == pageSize) {
+ // checkstyle.off: RegexpSinglelineJava
+ throw new OutOfMemoryError("test allocator failure");
+ // checkstyle.on: RegexpSinglelineJava
+ }
+ if (size == recordPageSize) {
+ exactFitAllocations.incrementAndGet();
+ }
+ return MemoryAllocator.HEAP.allocate(size);
+ }
+
+ @Override
+ public void free(MemoryBlock memory) {
+ MemoryAllocator.HEAP.free(memory);
+ }
+ };
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(1024L + pageSize + recordPageSize);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final BytesToBytesMap map = new BytesToBytesMap(manager, 64, pageSize);
+ final long[] row = new long[]{1L};
+
+ try {
+ final BytesToBytesMap.Location location =
+ map.lookup(row, Platform.LONG_ARRAY_OFFSET, Long.BYTES);
+ Assertions.assertTrue(location.append(
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES,
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES));
+ Assertions.assertEquals(1, exactFitAllocations.get());
+ Assertions.assertEquals(
+ 1024L + pageSize + recordPageSize,
manager.getMemoryConsumptionForThisTask());
+ } finally {
+ map.free();
+ }
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void bytesToBytesMapFallsBackFromExactFitPartialPage() {
+ final long pageSize = 4096L;
+ final long recordPageSize =
+ (3L * UnsafeAlignedOffset.getUaoSize()) + (3L * Long.BYTES);
+ final AtomicInteger exactFitAllocations = new AtomicInteger();
+ final MemoryAllocator allocator = new MemoryAllocator() {
+ @Override
+ public MemoryBlock allocate(long size) throws OutOfMemoryError {
+ if (size == pageSize) {
+ // checkstyle.off: RegexpSinglelineJava
+ throw new OutOfMemoryError("test allocator failure");
+ // checkstyle.on: RegexpSinglelineJava
+ }
+ if (size == recordPageSize) {
+ exactFitAllocations.incrementAndGet();
+ }
+ return MemoryAllocator.HEAP.allocate(size);
+ }
+
+ @Override
+ public void free(MemoryBlock memory) {
+ MemoryAllocator.HEAP.free(memory);
+ }
+ };
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(9216L);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final BytesToBytesMap map = new BytesToBytesMap(manager, 64, pageSize);
+ final long[] row = new long[]{1L};
+
+ try {
+ final BytesToBytesMap.Location location =
+ map.lookup(row, Platform.LONG_ARRAY_OFFSET, Long.BYTES);
+ Assertions.assertFalse(location.append(
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES,
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES));
+ Assertions.assertEquals(1, exactFitAllocations.get());
+ Assertions.assertEquals(1024L,
manager.getMemoryConsumptionForThisTask());
+ } finally {
+ map.free();
+ }
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void bytesToBytesMapFallsBackFromExactFitPageAfterSpill() {
+ final long pageSize = 4096L;
+ final long recordPageSize =
+ (3L * UnsafeAlignedOffset.getUaoSize()) + (3L * Long.BYTES);
+ final SizeLimitedAllocator allocator = new SizeLimitedAllocator(1024L);
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(1024L + (2L * pageSize));
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final BytesToBytesMap map = new BytesToBytesMap(manager, 64, pageSize);
+ final TestMemoryConsumer spillableConsumer = new
TestMemoryConsumer(manager);
+ final long[] row = new long[]{1L};
+ spillableConsumer.use(pageSize);
+
+ try {
+ final BytesToBytesMap.Location location =
+ map.lookup(row, Platform.LONG_ARRAY_OFFSET, Long.BYTES);
+ Assertions.assertFalse(location.append(
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES,
+ row, Platform.LONG_ARRAY_OFFSET, Long.BYTES));
+ Assertions.assertEquals(4, allocator.allocationAttempts);
+ Assertions.assertEquals(recordPageSize, allocator.lastAllocationSize);
+ Assertions.assertEquals(0, spillableConsumer.getUsed());
+ Assertions.assertEquals(1024L,
manager.getMemoryConsumptionForThisTask());
+ } finally {
+ map.free();
+ }
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void freeTailAcquisitionDoesNotReenterPageAllocation() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(6144);
+ final SizeLimitedAllocator allocator = new SizeLimitedAllocator(1024);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final NonSpillingAllocatingConsumer existingConsumer =
+ new NonSpillingAllocatingConsumer(manager);
+ final PageAllocatingConsumer requestingConsumer = new
PageAllocatingConsumer(manager, 4096);
+ existingConsumer.use(1024);
+
+ final MemoryBlock page = requestingConsumer.allocate(1024);
+ Assertions.assertNotNull(page);
+ Assertions.assertEquals(2, existingConsumer.spillAttempts);
+ Assertions.assertNull(existingConsumer.nestedPage);
+ Assertions.assertEquals(1024, existingConsumer.getUsed());
+ Assertions.assertEquals(1024, requestingConsumer.getUsed());
+ Assertions.assertEquals(6144, manager.getMemoryConsumptionForThisTask());
+
+ existingConsumer.free(1024);
+ requestingConsumer.freeAllocatedPage(page);
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void nestedPageAllocationFailureDoesNotReenterRecovery() {
+ final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
+ memoryManager.limit(4096);
+ final TestAllocator allocator = new TestAllocator(Integer.MAX_VALUE);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final AllocatingSpillConsumer existingConsumer = new
AllocatingSpillConsumer(manager);
+ final TestMemoryConsumer requestingConsumer = new
TestMemoryConsumer(manager);
+ existingConsumer.use(1024);
+
+ Assertions.assertNull(manager.allocatePage(1024, requestingConsumer));
+ Assertions.assertNull(existingConsumer.nestedPage);
+ Assertions.assertEquals(2, allocator.allocationAttempts);
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ Assertions.assertEquals(0, manager.getMemoryConsumptionForThisTask());
+ }
+
+ @Test
+ public void offHeapPageAllocationFailureReturnsNull() {
+ final SparkConf conf = new SparkConf()
+ .set(package$.MODULE$.MEMORY_OFFHEAP_ENABLED(), true)
+ .set(package$.MODULE$.MEMORY_OFFHEAP_SIZE(), 4096L);
+ final TestMemoryManager memoryManager = new TestMemoryManager(conf);
+ memoryManager.limit(4096);
+ final TestAllocator allocator = new TestAllocator(Integer.MAX_VALUE);
+ final TaskMemoryManager manager = new TaskMemoryManager(memoryManager, 0,
allocator);
+ final MemoryConsumer c = new TestMemoryConsumer(manager,
MemoryMode.OFF_HEAP);
+
+ Assertions.assertNull(manager.allocatePage(4096, c));
+ Assertions.assertEquals(1, allocator.allocationAttempts);
+ Assertions.assertEquals(0, manager.cleanUpAllAllocatedMemory());
+ }
+
@Test
public void cooperativeSpilling() {
final TestMemoryManager memoryManager = new TestMemoryManager(new
SparkConf());
diff --git
a/core/src/test/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorterSuite.java
b/core/src/test/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorterSuite.java
index aed8ccea9352..4f277d52d838 100644
---
a/core/src/test/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorterSuite.java
+++
b/core/src/test/java/org/apache/spark/shuffle/sort/ShuffleInMemorySorterSuite.java
@@ -32,6 +32,7 @@ import org.apache.spark.memory.TaskMemoryManager;
import org.apache.spark.memory.TestMemoryConsumer;
import org.apache.spark.memory.TestMemoryManager;
import org.apache.spark.unsafe.Platform;
+import org.apache.spark.unsafe.array.LongArray;
import org.apache.spark.unsafe.memory.MemoryBlock;
public class ShuffleInMemorySorterSuite {
@@ -57,6 +58,33 @@ public class ShuffleInMemorySorterSuite {
Assertions.assertFalse(iter.hasNext());
}
+ @Test
+ public void testResetAllocatesPointerArrayLazily() {
+ final ShuffleInMemorySorter sorter = new ShuffleInMemorySorter(
+ consumer, 4, shouldUseRadixSort());
+ Assertions.assertTrue(sorter.hasSpaceForAnotherRecord());
+
+ sorter.reset();
+ Assertions.assertEquals(0, sorter.getMemoryUsage());
+ Assertions.assertFalse(sorter.hasSpaceForAnotherRecord());
+ Assertions.assertFalse(sorter.getSortedIterator().hasNext());
+
+ final LongArray array = consumer.allocateArray(sorter.getInitialSize());
+ sorter.expandPointerArray(array);
+ Assertions.assertTrue(sorter.hasSpaceForAnotherRecord());
+ sorter.free();
+ }
+
+ @Test
+ public void testInitialSizeWithUsableCapacity() {
+ final ShuffleInMemorySorter sorter = new ShuffleInMemorySorter(
+ consumer, 1, shouldUseRadixSort());
+
+ Assertions.assertEquals(1, sorter.getInitialSize());
+ Assertions.assertEquals(2, sorter.getInitialSizeWithUsableCapacity());
+ sorter.free();
+ }
+
@Test
public void testBasicSorting() {
final String[] dataToSort = new String[] {
diff --git
a/core/src/test/java/org/apache/spark/shuffle/sort/UnsafeShuffleWriterSuite.java
b/core/src/test/java/org/apache/spark/shuffle/sort/UnsafeShuffleWriterSuite.java
index 26f0a8635447..3bddc8afcd5f 100644
---
a/core/src/test/java/org/apache/spark/shuffle/sort/UnsafeShuffleWriterSuite.java
+++
b/core/src/test/java/org/apache/spark/shuffle/sort/UnsafeShuffleWriterSuite.java
@@ -557,6 +557,22 @@ public class UnsafeShuffleWriterSuite implements
ShuffleChecksumTestHelper {
assertEquals(3, spillFilesCreated.size());
}
+ @Test
+ public void smallInitialSortBufferDoesNotSpillEveryRecordRadixOff() throws
Exception {
+ conf.set(package$.MODULE$.SHUFFLE_SORT_INIT_BUFFER_SIZE(), 1L);
+ conf.set(package$.MODULE$.SHUFFLE_SORT_USE_RADIXSORT(), false);
+ writeEnoughRecordsToTriggerSortBufferExpansionAndSpill();
+ assertEquals(12, spillFilesCreated.size());
+ }
+
+ @Test
+ public void smallInitialSortBufferDoesNotSpillEveryRecordRadixOn() throws
Exception {
+ conf.set(package$.MODULE$.SHUFFLE_SORT_INIT_BUFFER_SIZE(), 1L);
+ conf.set(package$.MODULE$.SHUFFLE_SORT_USE_RADIXSORT(), true);
+ writeEnoughRecordsToTriggerSortBufferExpansionAndSpill();
+ assertEquals(14, spillFilesCreated.size());
+ }
+
private void writeEnoughRecordsToTriggerSortBufferExpansionAndSpill() throws
Exception {
memoryManager.limit(DEFAULT_INITIAL_SORT_BUFFER_SIZE * 16);
final UnsafeShuffleWriter<Object, Object> writer = createWriter(false);
diff --git
a/core/src/test/scala/org/apache/spark/shuffle/sort/ShuffleExternalSorterSuite.scala
b/core/src/test/scala/org/apache/spark/shuffle/sort/ShuffleExternalSorterSuite.scala
index 46b4e8b5202d..f363afecf331 100644
---
a/core/src/test/scala/org/apache/spark/shuffle/sort/ShuffleExternalSorterSuite.scala
+++
b/core/src/test/scala/org/apache/spark/shuffle/sort/ShuffleExternalSorterSuite.scala
@@ -24,11 +24,11 @@ import org.scalatestplus.mockito.MockitoSugar
import org.apache.spark._
import org.apache.spark.executor.{ShuffleWriteMetrics, TaskMetrics}
-import org.apache.spark.internal.config.MEMORY_FRACTION
-import org.apache.spark.internal.config.MEMORY_OFFHEAP_ENABLED
+import org.apache.spark.internal.config.{MEMORY_FRACTION,
MEMORY_OFFHEAP_ENABLED, SHUFFLE_SORT_USE_RADIXSORT}
import org.apache.spark.internal.config.Tests._
import org.apache.spark.memory._
-import org.apache.spark.unsafe.Platform
+import org.apache.spark.unsafe.{Platform, UnsafeAlignedOffset}
+import org.apache.spark.unsafe.memory.MemoryBlock
class ShuffleExternalSorterSuite extends SparkFunSuite with LocalSparkContext
with MockitoSugar {
@@ -56,13 +56,14 @@ class ShuffleExternalSorterSuite extends SparkFunSuite with
LocalSparkContext wi
memoryManager.maxHeapMemory - memoryManager.executionMemoryUsed >
400) {
val acquireExecutionMemoryMethod =
memoryManager.getClass.getMethods.filter(_.getName ==
"acquireExecutionMemory").head
- acquireExecutionMemoryMethod.invoke(
- memoryManager,
- JLong.valueOf(
- memoryManager.maxHeapMemory - memoryManager.executionMemoryUsed
- 400),
- JLong.valueOf(1L), // taskAttemptId
- MemoryMode.ON_HEAP
- ).asInstanceOf[java.lang.Long]
+ acquireExecutionMemoryMethod
+ .invoke(
+ memoryManager,
+ JLong.valueOf(
+ memoryManager.maxHeapMemory -
memoryManager.executionMemoryUsed - 400),
+ JLong.valueOf(1L), // taskAttemptId
+ MemoryMode.ON_HEAP)
+ .asInstanceOf[java.lang.Long]
}
super.acquireExecutionMemory(required, consumer)
}
@@ -116,93 +117,346 @@ class ShuffleExternalSorterSuite extends SparkFunSuite
with LocalSparkContext wi
parameters = Map("requestedBytes" -> "800", "receivedBytes" -> "400"))
}
- test("cleanupResources should not NPE when reset fails to reallocate array")
{
- // Reproduces a bug where:
- // 1. insertRecord() triggers spill -> reset() -> array = null ->
allocateArray() throws OOM
- // 2. OOM propagates out of insertRecord()
- // 3. UnsafeShuffleWriter's finally block calls cleanupResources()
- // 4. cleanupResources() -> freeMemory() -> updatePeakMemoryUsed() ->
getMemoryUsage()
- // -> inMemSorter.getMemoryUsage() -> NPE because inMemSorter.array
is still null
- //
- // The root cause: reset() sets array = null, then allocateArray() fails.
The sorter is left
- // with inMemSorter != null but inMemSorter.array == null.
cleanupResources() calls
- // freeMemory() which calls getMemoryUsage() before reaching
inMemSorter.free().
+ test("cleanupResources should handle lazily reset pointer array") {
val conf = new SparkConf()
.setMaster("local[1]")
.setAppName("ShuffleExternalSorterSuite")
.set(IS_TESTING, true)
- .set(TEST_MEMORY, 1600L)
+ .set(TEST_MEMORY, 10L * 1024 * 1024)
.set(MEMORY_FRACTION, 0.9999)
.set(MEMORY_OFFHEAP_ENABLED, false)
sc = new SparkContext(conf)
val memoryManager = UnifiedMemoryManager(conf, 1)
+ val taskMemoryManager = new TaskMemoryManager(memoryManager, 0)
+ val taskContext = mock[TaskContext]
+ val taskMetrics = new TaskMetrics
+ when(taskContext.taskMetrics()).thenReturn(taskMetrics)
+ val sorter = new ShuffleExternalSorter(
+ taskMemoryManager,
+ sc.env.blockManager,
+ taskContext,
+ 100,
+ 1,
+ conf,
+ new ShuffleWriteMetrics)
+ val bytes = new Array[Byte](1)
+ sorter.insertRecord(bytes, Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ sorter.spill()
+
+ val inMemSorterField = sorter.getClass.getDeclaredField("inMemSorter")
+ inMemSorterField.setAccessible(true)
+ val arrayField = classOf[ShuffleInMemorySorter].getDeclaredField("array")
+ arrayField.setAccessible(true)
+ assert(
+ arrayField.get(inMemSorterField.get(sorter)) == null,
+ "spill should leave the pointer array unallocated until the next insert")
+
+ sorter.cleanupResources()
+ }
+
+ test("spill should report all released memory") {
+ val conf = new SparkConf()
+ .setMaster("local[1]")
+ .setAppName("ShuffleExternalSorterSuite")
+ .set(IS_TESTING, true)
+ .set(TEST_MEMORY, 10L * 1024 * 1024)
+ .set(MEMORY_FRACTION, 0.9999)
+
+ sc = new SparkContext(conf)
+ val memoryManager = UnifiedMemoryManager(conf, 1)
+ val taskMemoryManager = new TaskMemoryManager(memoryManager, 0)
+ val taskContext = mock[TaskContext]
+ val taskMetrics = new TaskMetrics
+ when(taskContext.taskMetrics()).thenReturn(taskMetrics)
+ val sorter = new ShuffleExternalSorter(
+ taskMemoryManager,
+ sc.env.blockManager,
+ taskContext,
+ 100,
+ 1,
+ conf,
+ new ShuffleWriteMetrics)
- var shouldStealMemory = false
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ val usedBeforeSpill = sorter.getUsed
+ val spillSize = sorter.spill(Long.MaxValue, sorter)
- // Override acquireExecutionMemory to steal freed memory during reset()'s
allocateArray(),
- // forcing the allocation to fail with OOM.
+ assert(spillSize === usedBeforeSpill - sorter.getUsed)
+ assert(taskMetrics.memoryBytesSpilled === spillSize)
+ sorter.cleanupResources()
+ }
+
+ test("minimum pointer array should grow before the first insert") {
+ val conf = new SparkConf()
+ .setMaster("local[1]")
+ .setAppName("ShuffleExternalSorterSuite")
+ .set(IS_TESTING, true)
+ .set(TEST_MEMORY, 10L * 1024 * 1024)
+ .set(MEMORY_FRACTION, 0.9999)
+
+ sc = new SparkContext(conf)
+ val memoryManager = UnifiedMemoryManager(conf, 1)
+
+ Seq(false, true).foreach { useRadixSort =>
+ conf.set(SHUFFLE_SORT_USE_RADIXSORT, useRadixSort)
+ val taskMemoryManager = new TaskMemoryManager(memoryManager, 0)
+ val taskContext = mock[TaskContext]
+ when(taskContext.taskMetrics()).thenReturn(new TaskMetrics)
+ val sorter = new ShuffleExternalSorter(
+ taskMemoryManager,
+ sc.env.blockManager,
+ taskContext,
+ 1,
+ 1,
+ conf,
+ new ShuffleWriteMetrics)
+
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ assert(sorter.closeAndGetSpills().length === 1)
+ sorter.cleanupResources()
+ }
+ }
+
+ test("successful growth allocation after spill should be reused") {
+ val conf = new SparkConf()
+ .setMaster("local[1]")
+ .setAppName("ShuffleExternalSorterSuite")
+ .set(IS_TESTING, true)
+ .set(TEST_MEMORY, 10L * 1024 * 1024)
+ .set(MEMORY_FRACTION, 0.9999)
+
+ sc = new SparkContext(conf)
+ val memoryManager = UnifiedMemoryManager(conf, 1)
+
+ Seq(false, true).foreach { useRadixSort =>
+ conf.set(SHUFFLE_SORT_USE_RADIXSORT, useRadixSort)
+ val initialSize = 1
+ var spillOnGrowthAllocation = false
+ val taskMemoryManager = new TaskMemoryManager(memoryManager, 0) {
+ override def allocatePage(size: Long, consumer: MemoryConsumer):
MemoryBlock = {
+ if (spillOnGrowthAllocation) {
+ spillOnGrowthAllocation = false
+ consumer.spill(size, consumer)
+ }
+ super.allocatePage(size, consumer)
+ }
+ }
+ val taskContext = mock[TaskContext]
+ when(taskContext.taskMetrics()).thenReturn(new TaskMetrics)
+ val sorter = new ShuffleExternalSorter(
+ taskMemoryManager,
+ sc.env.blockManager,
+ taskContext,
+ initialSize,
+ 1,
+ conf,
+ new ShuffleWriteMetrics)
+
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ spillOnGrowthAllocation = true
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+
+ val inMemSorterField = sorter.getClass.getDeclaredField("inMemSorter")
+ inMemSorterField.setAccessible(true)
+ val inMemSorter =
inMemSorterField.get(sorter).asInstanceOf[ShuffleInMemorySorter]
+ assert(
+ inMemSorter.getMemoryUsage ===
inMemSorter.getInitialSizeWithUsableCapacity * 2L * 8L)
+ assert(sorter.closeAndGetSpills().length === 2)
+ sorter.cleanupResources()
+ }
+ }
+
+ test("pointer fallback should preserve spill failures") {
+ val conf = new SparkConf()
+ .setMaster("local[1]")
+ .setAppName("ShuffleExternalSorterSuite")
+ .set(IS_TESTING, true)
+ .set(TEST_MEMORY, 10L * 1024 * 1024)
+ .set(MEMORY_FRACTION, 0.9999)
+
+ sc = new SparkContext(conf)
+ val memoryManager = UnifiedMemoryManager(conf, 1)
+ var spillOnGrowthAllocation = false
+ var failNextPageAllocation = false
val taskMemoryManager = new TaskMemoryManager(memoryManager, 0) {
- override def acquireExecutionMemory(required: Long, consumer:
MemoryConsumer): Long = {
- if (shouldStealMemory &&
- memoryManager.maxHeapMemory - memoryManager.executionMemoryUsed >
400) {
- val acquireExecutionMemoryMethod =
- memoryManager.getClass.getMethods.filter(_.getName ==
"acquireExecutionMemory").head
- acquireExecutionMemoryMethod.invoke(
- memoryManager,
- JLong.valueOf(
- memoryManager.maxHeapMemory - memoryManager.executionMemoryUsed
- 400),
- JLong.valueOf(1L),
- MemoryMode.ON_HEAP
- ).asInstanceOf[java.lang.Long]
+ override def allocatePage(size: Long, consumer: MemoryConsumer):
MemoryBlock = {
+ if (spillOnGrowthAllocation) {
+ spillOnGrowthAllocation = false
+ consumer.spill(size, consumer)
+ failNextPageAllocation = true
+ } else if (failNextPageAllocation) {
+ failNextPageAllocation = false
+ val parameters = new java.util.HashMap[String, String]()
+ parameters.put("consumerToSpill", "test")
+ parameters.put("message", "test failure")
+ // scalastyle:off throwerror
+ throw new SparkOutOfMemoryError("SPILL_OUT_OF_MEMORY", parameters)
+ // scalastyle:on throwerror
}
- super.acquireExecutionMemory(required, consumer)
+ super.allocatePage(size, consumer)
}
}
val taskContext = mock[TaskContext]
- val taskMetrics = new TaskMetrics
- when(taskContext.taskMetrics()).thenReturn(taskMetrics)
+ when(taskContext.taskMetrics()).thenReturn(new TaskMetrics)
val sorter = new ShuffleExternalSorter(
taskMemoryManager,
sc.env.blockManager,
taskContext,
- 100, // initialSize: ShuffleInMemorySorter needs 800 bytes (100 * 8)
+ 1,
1,
conf,
new ShuffleWriteMetrics)
- val inMemSorter = {
- val field = sorter.getClass.getDeclaredField("inMemSorter")
- field.setAccessible(true)
- field.get(sorter).asInstanceOf[ShuffleInMemorySorter]
+
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ spillOnGrowthAllocation = true
+ val error = intercept[SparkOutOfMemoryError] {
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
}
+ assert(error.getCondition === "SPILL_OUT_OF_MEMORY")
+ sorter.cleanupResources()
+ }
- // Fill the pointer array until there's no space for another record.
- val bytes = new Array[Byte](1)
- while (inMemSorter.hasSpaceForAnotherRecord) {
- sorter.insertRecord(bytes, Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ test("pointer growth should preserve spill failures") {
+ val conf = new SparkConf()
+ .setMaster("local[1]")
+ .setAppName("ShuffleExternalSorterSuite")
+ .set(IS_TESTING, true)
+ .set(TEST_MEMORY, 10L * 1024 * 1024)
+ .set(MEMORY_FRACTION, 0.9999)
+
+ sc = new SparkContext(conf)
+ val memoryManager = UnifiedMemoryManager(conf, 1)
+ var failGrowthAfterSpill = false
+ val taskMemoryManager = new TaskMemoryManager(memoryManager, 0) {
+ override def allocatePage(size: Long, consumer: MemoryConsumer):
MemoryBlock = {
+ if (failGrowthAfterSpill) {
+ failGrowthAfterSpill = false
+ consumer.spill(size, consumer)
+ val parameters = new java.util.HashMap[String, String]()
+ parameters.put("consumerToSpill", "test")
+ parameters.put("message", "test failure")
+ // scalastyle:off throwerror
+ throw new SparkOutOfMemoryError("SPILL_OUT_OF_MEMORY", parameters)
+ // scalastyle:on throwerror
+ }
+ super.allocatePage(size, consumer)
+ }
}
+ val taskContext = mock[TaskContext]
+ when(taskContext.taskMetrics()).thenReturn(new TaskMetrics)
+ val sorter = new ShuffleExternalSorter(
+ taskMemoryManager,
+ sc.env.blockManager,
+ taskContext,
+ 1,
+ 1,
+ conf,
+ new ShuffleWriteMetrics)
- // Enable memory stealing so that when spill -> reset() -> allocateArray()
runs, the freed
- // memory is consumed before allocateArray can use it, causing OOM.
- shouldStealMemory = true
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ failGrowthAfterSpill = true
+ val error = intercept[SparkOutOfMemoryError] {
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ }
+ assert(error.getCondition === "SPILL_OUT_OF_MEMORY")
+ sorter.cleanupResources()
+ }
- // insertRecord triggers spill -> reset() -> array = null -> allocateArray
fails -> OOM
- intercept[SparkOutOfMemoryError] {
- sorter.insertRecord(bytes, Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ test("data page allocation spill should restore the pointer array") {
+ val conf = new SparkConf()
+ .setMaster("local[1]")
+ .setAppName("ShuffleExternalSorterSuite")
+ .set(IS_TESTING, true)
+ .set(TEST_MEMORY, 10L * 1024 * 1024)
+ .set(MEMORY_FRACTION, 0.9999)
+
+ sc = new SparkContext(conf)
+
+ val memoryManager = UnifiedMemoryManager(conf, 1)
+ var spillOnNextPageAllocation = false
+ val taskMemoryManager = new TaskMemoryManager(memoryManager, 0) {
+ override def allocatePage(size: Long, consumer: MemoryConsumer):
MemoryBlock = {
+ if (spillOnNextPageAllocation) {
+ spillOnNextPageAllocation = false
+ consumer.spill(size, consumer)
+ }
+ super.allocatePage(size, consumer)
+ }
}
+ val taskContext = mock[TaskContext]
+ when(taskContext.taskMetrics()).thenReturn(new TaskMetrics)
+ val sorter = new ShuffleExternalSorter(
+ taskMemoryManager,
+ sc.env.blockManager,
+ taskContext,
+ 100,
+ 1,
+ conf,
+ new ShuffleWriteMetrics)
- // Verify the broken state: inMemSorter != null but inMemSorter.array ==
null
- val inMemSorterField = sorter.getClass.getDeclaredField("inMemSorter")
- inMemSorterField.setAccessible(true)
- assert(inMemSorterField.get(sorter) != null, "inMemSorter should still be
non-null")
- val arrayField = classOf[ShuffleInMemorySorter].getDeclaredField("array")
- arrayField.setAccessible(true)
- assert(arrayField.get(inMemSorterField.get(sorter)) == null,
- "inMemSorter.array should be null (reset freed it, allocateArray
failed)")
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ spillOnNextPageAllocation = true
+ val bytes =
+ new Array[Byte]((taskMemoryManager.pageSizeBytes() -
UnsafeAlignedOffset.getUaoSize).toInt)
+ sorter.insertRecord(bytes, Platform.BYTE_ARRAY_OFFSET, bytes.length, 0)
- // Without the fix, this NPEs in:
- // cleanupResources -> freeMemory -> updatePeakMemoryUsed ->
getMemoryUsage
- // -> inMemSorter.getMemoryUsage -> array.size() -> NPE
+ assert(sorter.closeAndGetSpills().length === 2)
sorter.cleanupResources()
}
+
+ test("data page allocation should not starve pointer restoration") {
+ val numCores = 4
+ val conf = new SparkConf(false)
+ .setMaster(s"local[$numCores]")
+ .setAppName("ShuffleExternalSorterSuite")
+ .set(TEST_MEMORY, 512L * 1024 * 1024)
+ .set(MEMORY_FRACTION, 0.01)
+
+ sc = new SparkContext(conf)
+ val memoryManager = UnifiedMemoryManager(conf, numCores)
+ val taskMemoryManager = new TaskMemoryManager(memoryManager, 0)
+ val taskContext = mock[TaskContext]
+ when(taskContext.taskMetrics()).thenReturn(new TaskMetrics)
+ val sorter = new ShuffleExternalSorter(
+ taskMemoryManager,
+ sc.env.blockManager,
+ taskContext,
+ 4096,
+ 1,
+ conf,
+ new ShuffleWriteMetrics)
+
+ val pageSize = taskMemoryManager.pageSizeBytes()
+ assert(memoryManager.maxHeapMemory / numCores < pageSize)
+ val firstRecord = new Array[Byte]((pageSize -
UnsafeAlignedOffset.getUaoSize).toInt)
+ sorter.insertRecord(firstRecord, Platform.BYTE_ARRAY_OFFSET,
firstRecord.length, 0)
+
+ val acquireExecutionMemoryMethod =
+ memoryManager.getClass.getMethods.filter(_.getName ==
"acquireExecutionMemory").head
+ val releaseExecutionMemoryMethod =
+ memoryManager.getClass.getMethods.filter(_.getName ==
"releaseExecutionMemory").head
+ (1L until numCores.toLong).foreach { taskAttemptId =>
+ val granted = acquireExecutionMemoryMethod.invoke(
+ memoryManager,
+ JLong.valueOf(1L),
+ JLong.valueOf(taskAttemptId),
+ MemoryMode.ON_HEAP).asInstanceOf[JLong]
+ assert(granted === 1L)
+ }
+
+ try {
+ sorter.insertRecord(new Array[Byte](1), Platform.BYTE_ARRAY_OFFSET, 1, 0)
+ } finally {
+ sorter.cleanupResources()
+ (1L until numCores.toLong).foreach { taskAttemptId =>
+ releaseExecutionMemoryMethod.invoke(
+ memoryManager,
+ JLong.valueOf(1L),
+ JLong.valueOf(taskAttemptId),
+ MemoryMode.ON_HEAP)
+ }
+ assert(taskMemoryManager.cleanUpAllAllocatedMemory() === 0L)
+ }
+ }
}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/python/HybridQueue.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/python/HybridQueue.scala
index 90996c552645..4eb0a1425b91 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/python/HybridQueue.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/python/HybridQueue.scala
@@ -110,10 +110,15 @@ abstract class HybridQueue[T, Q <: Queue[T]](
case _: SparkOutOfMemoryError =>
null
}
- val buffer = if (page != null) {
- createInMemoryQueue(page)
- } else {
+ val exactFitPartialPage = page != null &&
+ required < memManager.pageSizeBytes() && page.size() == required
+ val buffer = if (page == null || exactFitPartialPage) {
+ if (page != null) {
+ freePage(page)
+ }
createDiskQueue()
+ } else {
+ createInMemoryQueue(page)
}
synchronized {
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/python/RowQueueSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/python/RowQueueSuite.scala
index 10d3b1429600..3610b3581640 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/python/RowQueueSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/python/RowQueueSuite.scala
@@ -21,11 +21,11 @@ import java.io.File
import org.apache.spark.{SparkConf, SparkFunSuite}
import org.apache.spark.internal.config._
-import org.apache.spark.memory.{MemoryMode, TaskMemoryManager,
TestMemoryManager}
+import org.apache.spark.memory.{MemoryConsumer, MemoryMode, TaskMemoryManager,
TestMemoryManager}
import org.apache.spark.security.{CryptoStreamUtils, EncryptionFunSuite}
import org.apache.spark.serializer.{JavaSerializer, SerializerManager}
import org.apache.spark.sql.catalyst.expressions.UnsafeRow
-import org.apache.spark.unsafe.memory.MemoryBlock
+import org.apache.spark.unsafe.memory.{MemoryAllocator, MemoryBlock}
import org.apache.spark.util.Utils
class RowQueueSuite extends SparkFunSuite with EncryptionFunSuite {
@@ -94,6 +94,32 @@ class RowQueueSuite extends SparkFunSuite with
EncryptionFunSuite {
queue.close()
}
+ test("hybrid queue uses disk for an exact-fit partial page") {
+ val conf = new SparkConf(false)
+ val serManager = createSerializerManager(conf)
+ val mem = new TestMemoryManager(conf)
+ var pageFreed = false
+ val taskM = new TaskMemoryManager(mem, 0) {
+ override def allocatePage(size: Long, consumer: MemoryConsumer):
MemoryBlock = {
+ MemoryAllocator.HEAP.allocate(20)
+ }
+
+ override def freePage(page: MemoryBlock, consumer: MemoryConsumer): Unit
= {
+ pageFreed = true
+ MemoryAllocator.HEAP.free(page)
+ }
+ }
+ val queue = HybridRowQueue(taskM, Utils.createTempDir().getCanonicalFile,
1, serManager)
+ val row = new UnsafeRow(1)
+ row.pointTo(new Array[Byte](16), 16)
+
+ assert(queue.add(row) === QueueMode.DISK)
+ assert(pageFreed)
+ assert(queue.getUsed === 0)
+ assert(queue.remove().getSizeInBytes === 16)
+ queue.close()
+ }
+
Seq(true, false).foreach { isOffHeap =>
encryptionTest(s"hybrid queue (offHeap=$isOffHeap)") { conf =>
conf.set(MEMORY_OFFHEAP_ENABLED, isOffHeap)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]