This is an automated email from the ASF dual-hosted git repository.
deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new c25605eef89 HIVE-29703: LLAP: Parquet footer larger than alloc.max
throws RuntimeException during caching
c25605eef89 is described below
commit c25605eef8918cf0de89585ff455d19b687391f3
Author: Denys Kuzmenko <[email protected]>
AuthorDate: Wed Jul 15 17:18:15 2026 +0300
HIVE-29703: LLAP: Parquet footer larger than alloc.max throws
RuntimeException during caching
---
.../hive/llap/io/metadata/MetadataCache.java | 93 ++++++++---
...rcMetadataCache.java => TestMetadataCache.java} | 172 +++++++++++++++------
2 files changed, 190 insertions(+), 75 deletions(-)
diff --git
a/llap-server/src/java/org/apache/hadoop/hive/llap/io/metadata/MetadataCache.java
b/llap-server/src/java/org/apache/hadoop/hive/llap/io/metadata/MetadataCache.java
index 8be5a06631e..184c8e1d8c8 100644
---
a/llap-server/src/java/org/apache/hadoop/hive/llap/io/metadata/MetadataCache.java
+++
b/llap-server/src/java/org/apache/hadoop/hive/llap/io/metadata/MetadataCache.java
@@ -257,41 +257,86 @@ public LlapBufferOrBuffers putFileMetadata(Object
fileKey, int length, InputStre
}
- @SuppressWarnings({ "rawtypes", "unchecked" })
+ @SuppressWarnings("unchecked")
private LlapBufferOrBuffers wrapBbForFile(LlapBufferOrBuffers result,
Object fileKey, int length, InputStream stream, CacheTag tag,
AtomicBoolean isStopped) throws IOException {
- if (result != null) return result;
+ if (result != null) {
+ return result;
+ }
int maxAlloc = allocator.getMaxAllocation();
- LlapMetadataBuffer<Object>[] largeBuffers = null;
- if (maxAlloc < length) {
- largeBuffers = new LlapMetadataBuffer[length / maxAlloc];
- for (int i = 0; i < largeBuffers.length; ++i) {
- largeBuffers[i] = new LlapMetadataBuffer<>(fileKey, tag);
+ // Buffers are locked and handed to the cache only after this returns, so
release whatever we
+ // allocated if a later read or allocation throws - otherwise it leaks
(nothing else reclaims it).
+ if (length <= maxAlloc) {
+ // The whole footer fits in a single buffer - the overwhelmingly common
case.
+ LlapMetadataBuffer<Object> buffer = new LlapMetadataBuffer<>(fileKey,
tag);
+ allocator.allocateMultiple(new MemoryBuffer[] { buffer }, length, null,
isStopped);
+ boolean done = false;
+ try {
+ readIntoCacheBuffer(stream, length, buffer);
+ done = true;
+ return buffer;
+ } finally {
+ if (!done) {
+ deallocateQuietly(buffer);
+ }
}
- allocator.allocateMultiple(largeBuffers, maxAlloc, null, isStopped);
+ }
+ // Larger footers are split across maxAlloc-sized chunks, the last one
holding the remainder.
+ LlapMetadataBuffer<Object>[] largeBuffers = new LlapMetadataBuffer[length
/ maxAlloc];
+ for (int i = 0; i < largeBuffers.length; ++i) {
+ largeBuffers[i] = new LlapMetadataBuffer<>(fileKey, tag);
+ }
+ // allocateMultiple is all-or-nothing: on success every chunk is
allocated; on failure it
+ // releases whatever it reserved.
+ allocator.allocateMultiple(largeBuffers, maxAlloc, null, isStopped);
+
+ LlapMetadataBuffer<Object> smallBuffer = null;
+ boolean done = false;
+ try {
for (int i = 0; i < largeBuffers.length; ++i) {
readIntoCacheBuffer(stream, maxAlloc, largeBuffers[i]);
}
- }
- int smallSize = length % maxAlloc;
- if (smallSize == 0) {
- return new LlapMetadataBuffers(largeBuffers);
- } else {
- LlapMetadataBuffer<Object>[] smallBuffer = new LlapMetadataBuffer[1];
- smallBuffer[0] = new LlapMetadataBuffer(fileKey, tag);
- allocator.allocateMultiple(smallBuffer, length, null, isStopped);
- readIntoCacheBuffer(stream, smallSize, smallBuffer[0]);
- if (largeBuffers == null) {
- return smallBuffer[0]; // This is the overwhelmingly common case.
- } else {
- LlapMetadataBuffer<Object>[] cacheData = new
LlapMetadataBuffer[largeBuffers.length + 1];
- System.arraycopy(largeBuffers, 0, cacheData, 0, largeBuffers.length);
- cacheData[largeBuffers.length] = smallBuffer[0];
- return new LlapMetadataBuffers<>(cacheData);
+ int smallSize = length % maxAlloc;
+ if (smallSize == 0) {
+ done = true;
+ return new LlapMetadataBuffers<>(largeBuffers);
+ }
+ // Allocate the remainder only; the last chunk is smaller than maxAlloc.
+ LlapMetadataBuffer<Object> remainder = new LlapMetadataBuffer<>(fileKey,
tag);
+ allocator.allocateMultiple(new MemoryBuffer[] { remainder }, smallSize,
null, isStopped);
+ smallBuffer = remainder; // Registered for cleanup only now that
allocation succeeded.
+ readIntoCacheBuffer(stream, smallSize, remainder);
+
+ LlapMetadataBuffer<Object>[] cacheData = new
LlapMetadataBuffer[largeBuffers.length + 1];
+ System.arraycopy(largeBuffers, 0, cacheData, 0, largeBuffers.length);
+ cacheData[largeBuffers.length] = remainder;
+
+ done = true;
+ return new LlapMetadataBuffers<>(cacheData);
+ } finally {
+ if (!done) {
+ for (LlapMetadataBuffer<Object> buffer : largeBuffers) {
+ deallocateQuietly(buffer);
+ }
+ if (smallBuffer != null) {
+ deallocateQuietly(smallBuffer);
+ }
}
}
}
+ /**
+ * Releases a footer buffer that was allocated but not yet published to the
cache. The buffer must
+ * be unlocked (refCount 0, as deallocate asserts); callers here run before
it is ever locked.
+ */
+ private void deallocateQuietly(MemoryBuffer buffer) {
+ try {
+ allocator.deallocate(buffer);
+ } catch (Throwable t) {
+ LlapIoImpl.LOG.error("Ignoring the cleanup error after another error",
t);
+ }
+ }
+
private static void readIntoCacheBuffer(
InputStream stream, int length, MemoryBuffer dest) throws IOException {
ByteBuffer bb = dest.getByteBufferRaw();
diff --git
a/llap-server/src/test/org/apache/hadoop/hive/llap/cache/TestOrcMetadataCache.java
b/llap-server/src/test/org/apache/hadoop/hive/llap/cache/TestMetadataCache.java
similarity index 74%
rename from
llap-server/src/test/org/apache/hadoop/hive/llap/cache/TestOrcMetadataCache.java
rename to
llap-server/src/test/org/apache/hadoop/hive/llap/cache/TestMetadataCache.java
index 5ec15beccfb..369f0968bae 100644
---
a/llap-server/src/test/org/apache/hadoop/hive/llap/cache/TestOrcMetadataCache.java
+++
b/llap-server/src/test/org/apache/hadoop/hive/llap/cache/TestMetadataCache.java
@@ -20,6 +20,8 @@
import static
org.apache.hadoop.hive.llap.cache.LlapCacheableBuffer.INVALIDATE_OK;
import static org.junit.Assert.*;
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Random;
@@ -34,6 +36,7 @@
import org.apache.hadoop.hive.common.io.DataCache;
import org.apache.hadoop.hive.common.io.DiskRange;
import org.apache.hadoop.hive.common.io.DiskRangeList;
+import org.apache.hadoop.hive.common.io.encoded.MemoryBuffer;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.hive.llap.IllegalCacheConfigurationException;
@@ -51,7 +54,9 @@
import org.junit.Assert;
import org.junit.Test;
-public class TestOrcMetadataCache {
+public class TestMetadataCache {
+ private static final int MAX_ALLOC = 64;
+
private static class DummyCachePolicy implements LowLevelCachePolicy {
int lockCount = 0, unlockCount = 0;
@@ -94,10 +99,12 @@ public void debugDumpShort(StringBuilder sb) {
private static class DummyMemoryManager implements MemoryManager {
private int allocs;
+ private long reservedBytes;
@Override
public void reserveMemory(long memoryToReserve, AtomicBoolean isStopped) {
++allocs;
+ reservedBytes += memoryToReserve;
}
@Override public long evictMemory(long memoryToEvict) {
@@ -106,21 +113,21 @@ public void reserveMemory(long memoryToReserve,
AtomicBoolean isStopped) {
@Override
public void releaseMemory(long memUsage) {
+ reservedBytes -= memUsage;
}
@Override
public void updateMaxSize(long maxSize) {
}
+
+ long reservedBytes() {
+ return reservedBytes;
+ }
}
@Test
public void testCaseSomePartialBuffersAreEvicted() {
- final DummyMemoryManager mm = new DummyMemoryManager();
- final DummyCachePolicy cp = new DummyCachePolicy();
- final int MAX_ALLOC = 64;
- final LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("",
"");
- final BuddyAllocator alloc = new BuddyAllocator(false, false, 8,
MAX_ALLOC, 1, 4096, 0, null, mm, metrics, null, true);
- final MetadataCache cache = new MetadataCache(alloc, mm, cp, true,
metrics);
+ final MetadataCache cache = newMetadataCache();
final Object fileKey1 = new Object();
final Random rdm = new Random();
final ByteBuffer smallBuffer = ByteBuffer.allocate(2 * MAX_ALLOC);
@@ -138,14 +145,8 @@ public void testCaseSomePartialBuffersAreEvicted() {
}
@Test
- public void testBuffers() throws Exception {
- DummyMemoryManager mm = new DummyMemoryManager();
- DummyCachePolicy cp = new DummyCachePolicy();
- final int MAX_ALLOC = 64;
- LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
- BuddyAllocator alloc = new BuddyAllocator(
- false, false, 8, MAX_ALLOC, 1, 4096, 0, null, mm, metrics, null, true);
- MetadataCache cache = new MetadataCache(alloc, mm, cp, true, metrics);
+ public void testBuffers() {
+ MetadataCache cache = newMetadataCache();
Object fileKey1 = new Object();
Random rdm = new Random();
@@ -185,6 +186,104 @@ public void testBuffers() throws Exception {
assertFalse(b0.incRef() > 0); // Should have also been thrown out.
}
+ /**
+ * A footer of exactly maxAlloc: the split condition used a strict {@code
<}, so no buffer was
+ * allocated and the InputStream put path returned an empty wrapper (NPE on
lock).
+ */
+ @Test
+ public void testStreamFooterEqualToMaxAlloc() throws Exception {
+ verifyFooterFromStream(newMetadataCache(), MAX_ALLOC, true);
+ }
+
+ /**
+ * A footer larger than maxAlloc is split into a multi-buffer wrapper: an
exact multiple of maxAlloc
+ * (only full chunks) and a multiple plus a remainder (full chunks + a
smaller last chunk). The
+ * remainder buffer used to be allocated with the full footer length,
exceeding the max allocation.
+ */
+ @Test
+ public void testStreamFooterLargerThanMaxAlloc() throws Exception {
+ MetadataCache cache = newMetadataCache();
+ verifyFooterFromStream(cache, 2 * MAX_ALLOC, false);
+ verifyFooterFromStream(cache, 2 * MAX_ALLOC + 3, false);
+ }
+
+ /**
+ * If reading the footer stream fails after some buffers were already
allocated, every allocated
+ * buffer must be released back to the allocator. Otherwise the long-lived
LLAP daemon leaks arena
+ * memory on every failed footer read (a truncated file, an IO error, or an
oversized chunk).
+ */
+ @Test
+ public void testStreamFooterReadFailureReleasesBuffers() throws IOException {
+ assertFooterReadFailureReleasesBuffers(MAX_ALLOC); //
single-buffer path
+ assertFooterReadFailureReleasesBuffers(2 * MAX_ALLOC); //
multi-buffer, exact multiple
+ assertFooterReadFailureReleasesBuffers(2 * MAX_ALLOC + 3); //
multi-buffer, with remainder
+ }
+
+ private void assertFooterReadFailureReleasesBuffers(int length) throws
IOException {
+ final int arenaSize = 4096;
+ DummyMemoryManager mm = new DummyMemoryManager();
+ LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
+ BuddyAllocator alloc = new BuddyAllocator(
+ false, false, 8, MAX_ALLOC, 1, arenaSize, 0, null, mm, metrics, null,
true);
+ MetadataCache cache = new MetadataCache(alloc, mm, new DummyCachePolicy(),
true, metrics);
+
+ Object fileKey = new Object();
+ // One byte short of the footer, so reading the last chunk hits EOF
part-way through.
+ ByteArrayInputStream truncated = new ByteArrayInputStream(new byte[length
- 1]);
+ try {
+ cache.putFileMetadata(fileKey, length, truncated, null, null);
+ fail("Expected an EOFException for a truncated footer stream of length "
+ length);
+ } catch (EOFException expected) {
+ // expected
+ }
+ assertEquals("Allocated buffers must be released after a failed footer
read (length " + length + ")",
+ 0, mm.reservedBytes());
+ assertNull("A failed put must not leave a cache entry",
cache.getFileMetadata(fileKey));
+
+ // Independent check against the real allocator: leaked buffers keep
FLAG_NEW_ALLOC and can never
+ // be force-discarded, so re-filling the whole arena succeeds only if
nothing leaked.
+ MemoryBuffer[] wholeArena = new MemoryBuffer[arenaSize / MAX_ALLOC];
+ alloc.allocateMultiple(wholeArena, MAX_ALLOC);
+ }
+
+ private MetadataCache newMetadataCache() {
+ return newMetadataCache(4096);
+ }
+
+ private MetadataCache newMetadataCache(int arenaSize) {
+ return newMetadataCache(arenaSize, new DummyCachePolicy());
+ }
+
+ private MetadataCache newMetadataCache(int arenaSize, DummyCachePolicy cp) {
+ DummyMemoryManager mm = new DummyMemoryManager();
+ LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
+ BuddyAllocator alloc = new BuddyAllocator(
+ false, false, 8, MAX_ALLOC, 1, arenaSize, 0, null, mm, metrics, null,
true);
+ return new MetadataCache(alloc, mm, cp, true, metrics);
+ }
+
+ private void verifyFooterFromStream(MetadataCache cache, int length, boolean
expectSingleBuffer)
+ throws IOException {
+ Object fileKey = new Object();
+ ByteBuffer footer = ByteBuffer.allocate(length);
+ new Random().nextBytes(footer.array());
+
+ LlapBufferOrBuffers result = cache.putFileMetadata(
+ fileKey, length, new ByteArrayInputStream(footer.array()), null, null);
+ cache.decRefBuffer(result);
+ assertEquals(expectSingleBuffer, result.getSingleLlapBuffer() != null);
+ assertEquals(footer, reassemble(result));
+
+ result = cache.getFileMetadata(fileKey);
+ assertEquals(footer, reassemble(result));
+ cache.decRefBuffer(result);
+ }
+
+ private ByteBuffer reassemble(LlapBufferOrBuffers result) {
+ LlapAllocatorBuffer single = result.getSingleLlapBuffer();
+ return single != null ? single.getByteBufferDup() :
extractResultBbs(result);
+ }
+
public ByteBuffer extractResultBbs(LlapBufferOrBuffers result) {
int totalLen = 0;
for (LlapAllocatorBuffer buf : result.getMultipleLlapBuffers()) {
@@ -199,14 +298,9 @@ public ByteBuffer extractResultBbs(LlapBufferOrBuffers
result) {
}
@Test
- public void testIncompleteCbs() throws Exception {
- DummyMemoryManager mm = new DummyMemoryManager();
+ public void testIncompleteCbs() {
DummyCachePolicy cp = new DummyCachePolicy();
- final int MAX_ALLOC = 64;
- LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
- BuddyAllocator alloc = new BuddyAllocator(
- false, false, 8, MAX_ALLOC, 1, 4096, 0, null, mm, metrics, null, true);
- MetadataCache cache = new MetadataCache(alloc, mm, cp, true, metrics);
+ MetadataCache cache = newMetadataCache(4096, cp);
DataCache.BooleanRef gotAllData = new DataCache.BooleanRef();
Object fileKey1 = new Object();
@@ -239,13 +333,7 @@ public void testIncompleteCbs() throws Exception {
@Test
public void testGetOrcTailForPath() throws Exception {
- DummyMemoryManager mm = new DummyMemoryManager();
- DummyCachePolicy cp = new DummyCachePolicy();
- final int MAX_ALLOC = 64;
- LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
- BuddyAllocator alloc = new BuddyAllocator(
- false, false, 8, MAX_ALLOC, 1, 4 * 4096, 0, null, mm, metrics, null,
true);
- MetadataCache cache = new MetadataCache(alloc, mm, cp, true, metrics);
+ MetadataCache cache = newMetadataCache(4 * 4096);
Path path = new Path("../data/files/alltypesorc");
Configuration jobConf = new Configuration();
@@ -260,13 +348,7 @@ public void testGetOrcTailForPath() throws Exception {
@Test
public void testGetOrcTailForPathWithFileId() throws Exception {
- DummyMemoryManager mm = new DummyMemoryManager();
- DummyCachePolicy cp = new DummyCachePolicy();
- final int MAX_ALLOC = 64;
- LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
- BuddyAllocator alloc = new BuddyAllocator(
- false, false, 8, MAX_ALLOC, 1, 4 * 4096, 0, null, mm, metrics, null,
true);
- MetadataCache cache = new MetadataCache(alloc, mm, cp, true, metrics);
+ MetadataCache cache = newMetadataCache(4 * 4096);
Path path = new Path("../data/files/alltypesorc");
Configuration jobConf = new Configuration();
@@ -284,13 +366,7 @@ public void testGetOrcTailForPathWithFileId() throws
Exception {
@Test
public void testGetOrcTailForPathWithFileIdChange() throws Exception {
- DummyMemoryManager mm = new DummyMemoryManager();
- DummyCachePolicy cp = new DummyCachePolicy();
- final int MAX_ALLOC = 64;
- LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
- BuddyAllocator alloc = new BuddyAllocator(
- false, false, 8, MAX_ALLOC, 1, 4 * 4096, 0, null, mm, metrics, null,
true);
- MetadataCache cache = new MetadataCache(alloc, mm, cp, true, metrics);
+ MetadataCache cache = newMetadataCache(4 * 4096);
Path path = new Path("../data/files/alltypesorc");
Configuration jobConf = new Configuration();
@@ -317,14 +393,8 @@ public void testGetOrcTailForPathCacheNotReady() throws
Exception {
}
@Test
- public void testProactiveEvictionMark() throws Exception {
- DummyMemoryManager mm = new DummyMemoryManager();
- DummyCachePolicy cp = new DummyCachePolicy();
- final int MAX_ALLOC = 64;
- LlapDaemonCacheMetrics metrics = LlapDaemonCacheMetrics.create("", "");
- BuddyAllocator alloc = new BuddyAllocator(
- false, false, 8, MAX_ALLOC, 1, 4096, 0, null, mm, metrics, null, true);
- MetadataCache cache = new MetadataCache(alloc, mm, cp, true, metrics);
+ public void testProactiveEvictionMark() {
+ MetadataCache cache = newMetadataCache();
long fn1 = 1;
long fn2 = 2;