diff --git a/core/src/main/java/org/apache/accumulo/core/conf/Property.java b/core/src/main/java/org/apache/accumulo/core/conf/Property.java index b148c1a60e5..b5d4da2fe60 100644 --- a/core/src/main/java/org/apache/accumulo/core/conf/Property.java +++ b/core/src/main/java/org/apache/accumulo/core/conf/Property.java @@ -594,6 +594,11 @@ Each key is the name of the pool (can be assigned any string). Each value is a J "Properties in this category affect the behavior of the scan servers.", "2.1.0"), SSERV_DATACACHE_SIZE("sserver.cache.data.size", "10%", PropertyType.MEMORY, "Specifies the size of the cache for RFile data blocks on each scan server.", "2.1.0"), + SSERV_COMPRESSED_DATACACHE_SIZE("sserver.cache.compressed.data.size", "0", PropertyType.MEMORY, + "Specifies the size of the secondary compressed cache for compressed RFile data blocks." + + "When set to a non-zero value a secondary block cache is created that stores block in compressed form." + + "A value of 0 disables the secondary compressed cache (default).", + "4.1.0"), SSERV_INDEXCACHE_SIZE("sserver.cache.index.size", "25%", PropertyType.MEMORY, "Specifies the size of the cache for RFile index blocks on each scan server.", "2.1.0"), SSERV_SUMMARYCACHE_SIZE("sserver.cache.summary.size", "10%", PropertyType.MEMORY, @@ -679,6 +684,11 @@ Each key is the name of the pool (can be assigned any string). Each value is a J "Specifies a default blocksize for the tserver caches.", "1.3.5"), TSERV_DATACACHE_SIZE("tserver.cache.data.size", "10%", PropertyType.MEMORY, "Specifies the size of the cache for RFile data blocks.", "1.3.5"), + TSERV_COMPRESSED_DATACACHE_SIZE("tserver.cache.data.compressed.size", "0", PropertyType.MEMORY, + "Specifies the size of the secondary compressed cache for compressed RFile data blocks." + + "When set to a non-zero value a secondary block cache is created that stores block in compressed form." + + "A value of 0 disables the secondary compressed cache (default).", + "4.1.0"), TSERV_INDEXCACHE_SIZE("tserver.cache.index.size", "25%", PropertyType.MEMORY, "Specifies the size of the cache for RFile index blocks.", "1.3.5"), TSERV_SUMMARYCACHE_SIZE("tserver.cache.summary.size", "10%", PropertyType.MEMORY, @@ -1720,9 +1730,10 @@ public static boolean isValidTablePropertyKey(String key) { // SSERV options SSERV_CACHED_TABLET_METADATA_REFRESH_PERCENT, SSERV_THREADCHECK, SSERV_CLIENTPORT, - SSERV_DATACACHE_SIZE, SSERV_INDEXCACHE_SIZE, SSERV_SUMMARYCACHE_SIZE, SSERV_DEFAULT_BLOCKSIZE, - SSERV_SCAN_REFERENCE_EXPIRATION_TIME, SSERV_CACHED_TABLET_METADATA_EXPIRATION, - SSERV_MINTHREADS, SSERV_MINTHREADS_TIMEOUT, SSERV_WAL_SORT_MAX_CONCURRENT, SSERV_GROUP_NAME, + SSERV_DATACACHE_SIZE, SSERV_COMPRESSED_DATACACHE_SIZE, SSERV_INDEXCACHE_SIZE, + SSERV_SUMMARYCACHE_SIZE, SSERV_DEFAULT_BLOCKSIZE, SSERV_SCAN_REFERENCE_EXPIRATION_TIME, + SSERV_CACHED_TABLET_METADATA_EXPIRATION, SSERV_MINTHREADS, SSERV_MINTHREADS_TIMEOUT, + SSERV_WAL_SORT_MAX_CONCURRENT, SSERV_GROUP_NAME, // TSERV options TSERV_TOTAL_MUTATION_QUEUE_MAX, TSERV_WAL_MAX_SIZE, TSERV_WAL_MAX_AGE, @@ -1731,9 +1742,9 @@ public static boolean isValidTablePropertyKey(String key) { TSERV_SCAN_RESULTS_MAX_TIMEOUT, TSERV_MINC_MAXCONCURRENT, TSERV_THREADCHECK, TSERV_LOG_BUSY_TABLETS_COUNT, TSERV_LOG_BUSY_TABLETS_INTERVAL, TSERV_WAL_SORT_MAX_CONCURRENT, TSERV_SLOW_FILEPERMIT_MILLIS, TSERV_WAL_BLOCKSIZE, TSERV_CLIENTPORT, TSERV_DATACACHE_SIZE, - TSERV_INDEXCACHE_SIZE, TSERV_SUMMARYCACHE_SIZE, TSERV_DEFAULT_BLOCKSIZE, TSERV_MINTHREADS, - TSERV_MINTHREADS_TIMEOUT, TSERV_NATIVEMAP_ENABLED, TSERV_MAXMEM, TSERV_SCAN_MAX_OPENFILES, - TSERV_ONDEMAND_UNLOADER_INTERVAL, TSERV_GROUP_NAME, + TSERV_COMPRESSED_DATACACHE_SIZE, TSERV_INDEXCACHE_SIZE, TSERV_SUMMARYCACHE_SIZE, + TSERV_DEFAULT_BLOCKSIZE, TSERV_MINTHREADS, TSERV_MINTHREADS_TIMEOUT, TSERV_NATIVEMAP_ENABLED, + TSERV_MAXMEM, TSERV_SCAN_MAX_OPENFILES, TSERV_ONDEMAND_UNLOADER_INTERVAL, TSERV_GROUP_NAME, // GC options GC_CANDIDATE_BATCH_SIZE, GC_CYCLE_START, GC_PORT, diff --git a/core/src/main/java/org/apache/accumulo/core/file/blockfile/cache/impl/BlockCacheConfiguration.java b/core/src/main/java/org/apache/accumulo/core/file/blockfile/cache/impl/BlockCacheConfiguration.java index efcb2997e16..a518a7e24c0 100644 --- a/core/src/main/java/org/apache/accumulo/core/file/blockfile/cache/impl/BlockCacheConfiguration.java +++ b/core/src/main/java/org/apache/accumulo/core/file/blockfile/cache/impl/BlockCacheConfiguration.java @@ -23,6 +23,7 @@ import java.util.Map; import org.apache.accumulo.core.conf.AccumuloConfiguration; +import org.apache.accumulo.core.conf.ConfigurationTypeHelper; import org.apache.accumulo.core.conf.Property; import org.apache.accumulo.core.spi.cache.BlockCacheManager.Configuration; import org.apache.accumulo.core.spi.cache.CacheType; @@ -41,22 +42,24 @@ public class BlockCacheConfiguration implements Configuration { private final long dataMaxSize; private final long summaryMaxSize; + private final long compressedDataMaxSize; public static BlockCacheConfiguration forTabletServer(AccumuloConfiguration conf) { return new BlockCacheConfiguration(conf, Property.TSERV_PREFIX, Property.TSERV_INDEXCACHE_SIZE, Property.TSERV_DATACACHE_SIZE, Property.TSERV_SUMMARYCACHE_SIZE, - Property.TSERV_DEFAULT_BLOCKSIZE); + Property.TSERV_DEFAULT_BLOCKSIZE, Property.TSERV_COMPRESSED_DATACACHE_SIZE); } public static BlockCacheConfiguration forScanServer(AccumuloConfiguration conf) { return new BlockCacheConfiguration(conf, Property.SSERV_PREFIX, Property.SSERV_INDEXCACHE_SIZE, Property.SSERV_DATACACHE_SIZE, Property.SSERV_SUMMARYCACHE_SIZE, - Property.SSERV_DEFAULT_BLOCKSIZE); + Property.SSERV_DEFAULT_BLOCKSIZE, Property.SSERV_COMPRESSED_DATACACHE_SIZE); } private BlockCacheConfiguration(AccumuloConfiguration conf, Property serverPrefix, Property indexCacheSizeProperty, Property dataCacheSizeProperty, - Property summaryCacheSizeProperty, Property defaultBlockSizeProperty) { + Property summaryCacheSizeProperty, Property defaultBlockSizeProperty, + Property compressedDataCacheSizeProperty) { this.serverPrefix = serverPrefix; this.genProps = conf.getAllPropertiesWithPrefix(serverPrefix); @@ -64,6 +67,13 @@ private BlockCacheConfiguration(AccumuloConfiguration conf, Property serverPrefi this.dataMaxSize = conf.getAsBytes(dataCacheSizeProperty); this.summaryMaxSize = conf.getAsBytes(summaryCacheSizeProperty); this.blockSize = conf.getAsBytes(defaultBlockSizeProperty); + + String compressedSizeStr = conf.get(compressedDataCacheSizeProperty); + if (compressedSizeStr == null) { + compressedSizeStr = compressedDataCacheSizeProperty.getDefaultValue(); + } + + this.compressedDataMaxSize = ConfigurationTypeHelper.getMemoryAsBytes(compressedSizeStr); } @Override @@ -83,7 +93,8 @@ public long getBlockSize() { @Override public String toString() { return "indexMaxSize: " + indexMaxSize + "dataMaxSize: " + dataMaxSize + "summaryMaxSize: " - + summaryMaxSize + ", blockSize: " + getBlockSize(); + + summaryMaxSize + ", blockSize: " + getBlockSize() + ", compressedDataMaxSize: " + + compressedDataMaxSize; } @Override @@ -121,4 +132,8 @@ public static String getCachePropertyBase(Property serverPrefix) { return serverPrefix.getKey() + "cache.config."; } + public long getCompressedDataMaxSize() { + return compressedDataMaxSize; + } + } diff --git a/core/src/main/java/org/apache/accumulo/core/file/blockfile/cache/impl/CompressedBlockCache.java b/core/src/main/java/org/apache/accumulo/core/file/blockfile/cache/impl/CompressedBlockCache.java new file mode 100644 index 00000000000..d3680e7ff06 --- /dev/null +++ b/core/src/main/java/org/apache/accumulo/core/file/blockfile/cache/impl/CompressedBlockCache.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.accumulo.core.file.blockfile.cache.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.function.Supplier; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.apache.accumulo.core.spi.cache.BlockCache; +import org.apache.accumulo.core.spi.cache.CacheEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CompressedBlockCache implements BlockCache { + + private static final Logger log = LoggerFactory.getLogger(CompressedBlockCache.class); + private final BlockCache delegate; + + public CompressedBlockCache(BlockCache delegate) { + this.delegate = delegate; + } + + private static byte[] compress(byte[] uncompressed) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(uncompressed.length / 2 + 16); + GZIPOutputStream gzip = new GZIPOutputStream(baos)) { + gzip.write(uncompressed); + gzip.finish(); + return baos.toByteArray(); + } catch (IOException e) { + log.warn("Failed to compress block for cache storage", e); + return null; + } + } + + private final byte[] uncompress(byte[] compressed) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(compressed); + GZIPInputStream gzip = new GZIPInputStream(bais); + ByteArrayOutputStream baos = new ByteArrayOutputStream(compressed.length * 3)) { + byte[] buffer = new byte[8192]; + int len; + while ((len = gzip.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + return baos.toByteArray(); + } catch (IOException e) { + log.warn("Failed to decompress block from cache", e); + return null; + } + } + + @Override + public CacheEntry cacheBlock(String blockName, byte[] buf) { + byte[] compressed = compress(buf); + if (compressed == null) { + // compression failed, skip caching + return null; + } + + if (log.isTraceEnabled()) { + log.trace("Caching block {} compressed: {} -> {} bytes (ratio {:.2f}", blockName, buf.length, + compressed.length, (double) buf.length / compressed.length); + } + CacheEntry entry = delegate.cacheBlock(blockName, compressed); + if (entry == null) { + return null; + } + return new DecompressingCacheEntry(entry, buf); + } + + @Override + public CacheEntry getBlock(String blockName) { + CacheEntry entry = delegate.getBlock(blockName); + if (entry == null) { + return null; + } + byte[] uncompressed = uncompress(entry.getBuffer()); + return new DecompressingCacheEntry(entry, uncompressed); + } + + @Override + public CacheEntry getBlock(String blockName, Loader loader) { + CacheEntry existing = delegate.getBlock(blockName); + if (existing != null) { + byte[] uncompressed = uncompress(existing.getBuffer()); + return new DecompressingCacheEntry(existing, uncompressed); + } + + Loader compressingLoader = new Loader() { + @Override + public Map getDependencies() { + return loader.getDependencies(); + } + + @Override + public byte[] load(int maxSize, Map dependencies) { + byte[] uncompressed = loader.load(maxSize, dependencies); + if (uncompressed == null) { + return null; + } + byte[] compressed = compress(uncompressed); + return compressed; + } + }; + + CacheEntry entry = delegate.getBlock(blockName, compressingLoader); + if (entry == null) { + return null; + } + byte[] uncompressed = uncompress(entry.getBuffer()); + return new DecompressingCacheEntry(entry, uncompressed); + } + + @Override + public long getMaxHeapSize() { + return 0; + } + + @Override + public long getMaxSize() { + return delegate.getMaxSize(); + } + + @Override + public Stats getStats() { + return delegate.getStats(); + } + + private static final class DecompressingCacheEntry implements CacheEntry { + + private final CacheEntry compressedEntry; + private final byte[] uncompressedBuffer; + + public DecompressingCacheEntry(CacheEntry compressedEntry, byte[] uncompressedBuffer) { + this.compressedEntry = compressedEntry; + this.uncompressedBuffer = uncompressedBuffer; + } + + @Override + public byte[] getBuffer() { + return uncompressedBuffer; + } + + @Override + public T getIndex(Supplier supplier) { + return null; + } + + @Override + public void indexWeightChanged() { + compressedEntry.indexWeightChanged(); + } + } +} diff --git a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/BasicCacheProvider.java b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/BasicCacheProvider.java index e8890640ade..c8a30fd9027 100644 --- a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/BasicCacheProvider.java +++ b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/BasicCacheProvider.java @@ -24,10 +24,17 @@ public class BasicCacheProvider implements CacheProvider { private final BlockCache indexCache; private final BlockCache dataCache; + private final BlockCache compressedDataCache; public BasicCacheProvider(BlockCache indexCache, BlockCache dataCache) { + this(indexCache, dataCache, null); + } + + public BasicCacheProvider(BlockCache indexCache, BlockCache dataCache, + BlockCache compressedDataCache) { this.indexCache = indexCache; this.dataCache = dataCache; + this.compressedDataCache = compressedDataCache; } @Override @@ -40,4 +47,8 @@ public BlockCache getIndexCache() { return indexCache; } + @Override + public BlockCache getCompressedDataCache() { + return compressedDataCache; + } } diff --git a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CachableBlockFile.java b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CachableBlockFile.java index 39ee8efcf89..1c059cdab3b 100644 --- a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CachableBlockFile.java +++ b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CachableBlockFile.java @@ -456,6 +456,15 @@ public CachedBlockRead getMetaBlock(long offset, long compressedSize, long rawSi * It is intended that once the BlockRead object is returned to the caller, that the caller will * read the entire block and then call close on the BlockRead class. * + *

+ * Lookup order: + *

    + *
  1. Primary (uncompressed) data cache
  2. + *
  3. Secondary (compressed) data cache
  4. + *
  5. Disk
  6. + *
+ *

+ * * NOTE: In the case of multi-read threads: This method can do redundant work where an entry is * read from disk and other threads check the cache before it has been inserted. */ @@ -470,6 +479,21 @@ public CachedBlockRead getDataBlock(int blockIndex) throws IOException { } } + // Check secondary + BlockCache _cCache = cacheProvider.getCompressedDataCache(); + if (_cCache != null) { + String _lookup = this.cacheId + "O" + blockIndex; + CacheEntry ce = _cCache.getBlock(_lookup); + if (ce != null) { + // promote the block to the primary cache + byte[] uncompressed = ce.getBuffer(); + if (_dCache != null) { + _dCache.cacheBlock(_lookup, uncompressed); + } + return new CachedBlockRead(new SimpleCacheEntry(uncompressed), uncompressed); + } + } + BlockReader _currBlock = getBCFile().getDataBlock(blockIndex); incrementCacheBypass(CacheType.DATA); return new CachedBlockRead(_currBlock); @@ -487,6 +511,21 @@ public CachedBlockRead getDataBlock(long offset, long compressedSize, long rawSi } } + // Check secondary compressed cache + BlockCache _cCache = cacheProvider.getCompressedDataCache(); + if (_cCache != null) { + String _lookup = this.cacheId + "R" + offset; + CacheEntry ce = _cCache.getBlock(_lookup); + if (ce != null) { + // promote the block to the primary cache + byte[] uncompressed = ce.getBuffer(); + if (_dCache != null) { + _dCache.cacheBlock(_lookup, uncompressed); + } + return new CachedBlockRead(new SimpleCacheEntry(uncompressed), uncompressed); + } + } + BlockReader _currBlock = getBCFile().getDataBlock(offset, compressedSize, rawSize); incrementCacheBypass(CacheType.DATA); return new CachedBlockRead(_currBlock); @@ -524,6 +563,29 @@ public void setCacheProvider(CacheProvider cacheProvider) { } + private static final class SimpleCacheEntry implements CacheEntry { + private final byte[] buffer; + + SimpleCacheEntry(byte[] buffer) { + this.buffer = buffer; + } + + @Override + public byte[] getBuffer() { + return buffer; + } + + @Override + public T getIndex(Supplier supplier) { + return null; + } + + @Override + public void indexWeightChanged() { + // no-op not backed by a real cache slot + } + } + public static class CachedBlockRead extends DataInputStream { private static InputStream wrapForTrace(InputStream inputStream) { diff --git a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CacheProvider.java b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CacheProvider.java index 4a64f9f2a1b..e4cbd49f74e 100644 --- a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CacheProvider.java +++ b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/CacheProvider.java @@ -26,4 +26,10 @@ public interface CacheProvider { BlockCache getDataCache(); BlockCache getIndexCache(); + + // Returns an optional secondary data cache that stores block in compressed for. When non-null + // blocks that miss the primary data cache will be looked up here before falling through to disk. + default BlockCache getCompressedDataCache() { + return null; + } } diff --git a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/ScanCacheProvider.java b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/ScanCacheProvider.java index 45a375aae10..50277e3cb69 100644 --- a/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/ScanCacheProvider.java +++ b/core/src/main/java/org/apache/accumulo/core/file/blockfile/impl/ScanCacheProvider.java @@ -30,8 +30,10 @@ public final class ScanCacheProvider implements CacheProvider { private final BlockCache indexCache; private final BlockCache dataCache; + private final BlockCache compressedDataCache; + public ScanCacheProvider(AccumuloConfiguration tableConfig, ScanDispatch dispatch, - BlockCache indexCache, BlockCache dataCache) { + BlockCache indexCache, BlockCache dataCache, BlockCache compressedDataCache) { var loggingIndexCache = BlockCacheUtil.instrument(CacheType.INDEX, indexCache); var loggingDataCache = BlockCacheUtil.instrument(CacheType.DATA, dataCache); @@ -57,16 +59,20 @@ public ScanCacheProvider(AccumuloConfiguration tableConfig, ScanDispatch dispatc switch (dispatch.getDataCacheUsage()) { case ENABLED: this.dataCache = loggingDataCache; + this.compressedDataCache = compressedDataCache; break; case DISABLED: this.dataCache = null; + this.compressedDataCache = null; break; case OPPORTUNISTIC: this.dataCache = new OpportunisticBlockCache(loggingDataCache); + this.compressedDataCache = null; break; case TABLE: - this.dataCache = - tableConfig.getBoolean(Property.TABLE_BLOCKCACHE_ENABLED) ? loggingDataCache : null; + boolean tableDataCacheEnabled = tableConfig.getBoolean(Property.TABLE_BLOCKCACHE_ENABLED); + this.dataCache = tableDataCacheEnabled ? loggingDataCache : null; + this.compressedDataCache = tableDataCacheEnabled ? compressedDataCache : null; break; default: throw new IllegalStateException(); @@ -82,4 +88,9 @@ public BlockCache getDataCache() { public BlockCache getIndexCache() { return indexCache; } + + @Override + public BlockCache getCompressedDataCache() { + return compressedDataCache; + } } diff --git a/core/src/test/java/org/apache/accumulo/core/file/blockfile/cache/CompressedBlockCacheTest.java b/core/src/test/java/org/apache/accumulo/core/file/blockfile/cache/CompressedBlockCacheTest.java new file mode 100644 index 00000000000..e60e482c101 --- /dev/null +++ b/core/src/test/java/org/apache/accumulo/core/file/blockfile/cache/CompressedBlockCacheTest.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.accumulo.core.file.blockfile.cache; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.apache.accumulo.core.file.blockfile.cache.impl.CompressedBlockCache; +import org.apache.accumulo.core.spi.cache.BlockCache; +import org.apache.accumulo.core.spi.cache.CacheEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CompressedBlockCacheTest { + private static class MapBlockCache implements BlockCache { + private final ConcurrentHashMap map = new ConcurrentHashMap<>(); + + @Override + public CacheEntry cacheBlock(String blockName, byte[] buf) { + map.put(blockName, buf.clone()); + return wrap(buf); + } + + @Override + public CacheEntry getBlock(String blockName) { + byte[] data = map.get(blockName); + return data == null ? null : wrap(data); + } + + @Override + public CacheEntry getBlock(String blockName, Loader loader) { + byte[] data = map.computeIfAbsent(blockName, k -> { + byte[] loaded = loader.load(Integer.MAX_VALUE, Collections.emptyMap()); + return loaded; + }); + return data == null ? null : wrap(data); + } + + private CacheEntry wrap(byte[] data) { + return new CacheEntry() { + @Override + public byte[] getBuffer() { + return data; + } + + @Override + public T getIndex(Supplier supplier) { + return null; + } + + @Override + public void indexWeightChanged() {} + }; + } + + @Override + public long getMaxHeapSize() { + return Long.MAX_VALUE; + } + + @Override + public long getMaxSize() { + return Long.MAX_VALUE; + } + + @Override + public Stats getStats() { + return new Stats() { + @Override + public long hitCount() { + return 0; + } + + @Override + public long requestCount() { + return 0; + } + + @Override + public long evictionCount() { + return 0; + } + }; + } + + int size() { + return map.size(); + } + + byte[] getRaw(String key) { + return map.get(key); + } + } + + private MapBlockCache underlying; + private CompressedBlockCache cache; + + @BeforeEach + public void setUp() { + underlying = new MapBlockCache(); + cache = new CompressedBlockCache(underlying); + } + + @Test + public void testCacheAndRetrieve() { + byte[] data = new byte[1000]; + Arrays.fill(data, (byte) 'A'); + + CacheEntry stored = cache.cacheBlock("block1", data); + assertNotNull(stored); + assertArrayEquals(data, stored.getBuffer()); + + byte[] compressed = underlying.getRaw("block1"); + assertNotNull(compressed); + assert compressed.length < data.length + : "Expected compressed size < original for repetitive data"; + } + + @Test + public void testRetrieveMiss() { + CacheEntry ce = cache.getBlock("nonexistent"); + assertNull(ce); + } + + @Test + public void testRetrieveHit() { + byte[] data = new byte[500]; + Arrays.fill(data, (byte) 'Z'); + + cache.cacheBlock("blockZ", data); + + CacheEntry ce = cache.getBlock("blockZ"); + assertNotNull(ce); + assertArrayEquals(data, ce.getBuffer()); + } + + @Test + public void testGetBlockWithLoader() { + byte[] data = new byte[800]; + Arrays.fill(data, (byte) 0x42); + + BlockCache.Loader loader = new BlockCache.Loader() { + @Override + public Map getDependencies() { + return Collections.emptyMap(); + } + + @Override + public byte[] load(int maxSize, Map dependencies) { + return data.clone(); + } + }; + + CacheEntry ce = cache.getBlock("blockLoader", loader); + assertNotNull(ce); + assertArrayEquals(data, ce.getBuffer()); + assert underlying.size() == 1 : "Expected one entry in underlying cache"; + + CacheEntry ce2 = cache.getBlock("blockLoader", loader); + assertNotNull(ce2); + assertArrayEquals(data, ce2.getBuffer()); + } + + @Test + public void testGetIndexReturnsNull() { + byte[] data = new byte[100]; + cache.cacheBlock("blockIdx", data); + + CacheEntry ce = cache.getBlock("blockIdx"); + assertNotNull(ce); + assertNull(ce.getIndex(() -> null)); + } + + @Test + public void testLoaderNullReturnIsHandled() { + BlockCache.Loader nullLoader = new BlockCache.Loader() { + @Override + public Map getDependencies() { + return Collections.emptyMap(); + } + + @Override + public byte[] load(int maxSize, Map dependencies) { + return null; + } + }; + + CacheEntry ce = cache.getBlock("blockNull", nullLoader); + assertNull(ce); + assert underlying.size() == 0 : "No entries should be stored when loader returns null"; + } + + @Test + public void testConcurrentAccess() throws InterruptedException { + final int THREADS = 20; + final int OPS_PER_THREAD = 50; + byte[] data = new byte[256]; + Arrays.fill(data, (byte) 0xFF); + + Thread[] threads = new Thread[THREADS]; + for (int i = 0; i < THREADS; i++) { + final String key = "block-" + (i % 5); + threads[i] = new Thread(() -> { + for (int j = 0; j < OPS_PER_THREAD; j++) { + cache.cacheBlock(key, data); + CacheEntry ce = cache.getBlock(key); + if (ce != null) { + assertArrayEquals(data, ce.getBuffer()); + } + } + }); + } + + for (Thread t : threads) { + t.start(); + } + for (Thread t : threads) { + t.join(); + } + // If we reach here without exceptions, thread safety is satisfied + } +} diff --git a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java index e6779ada309..eed6345f1ee 100644 --- a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java +++ b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java @@ -69,7 +69,10 @@ import org.apache.accumulo.core.conf.Property; import org.apache.accumulo.core.data.TableId; import org.apache.accumulo.core.dataImpl.KeyExtent; +import org.apache.accumulo.core.file.blockfile.cache.impl.BlockCacheConfiguration; import org.apache.accumulo.core.file.blockfile.cache.impl.BlockCacheManagerFactory; +import org.apache.accumulo.core.file.blockfile.cache.impl.CompressedBlockCache; +import org.apache.accumulo.core.file.blockfile.cache.tinylfu.TinyLfuBlockCache; import org.apache.accumulo.core.file.blockfile.impl.ScanCacheProvider; import org.apache.accumulo.core.metadata.schema.Ample; import org.apache.accumulo.core.spi.cache.BlockCache; @@ -139,6 +142,7 @@ public class TabletServerResourceManager { private final BlockCache _dCache; private final BlockCache _iCache; private final BlockCache _sCache; + private final BlockCache _compressedDCache; private final ServerContext context; private Cache fileLenCache; @@ -286,32 +290,64 @@ public TabletServerResourceManager(ServerContext context, TabletHostingServer ts throw new IllegalStateException("Error creating BlockCacheManager", e); } - cacheManager.start(tserver.getBlockCacheConfiguration(acuConf)); + BlockCacheConfiguration blockCacheConf = + (BlockCacheConfiguration) tserver.getBlockCacheConfiguration(acuConf); + cacheManager.start(blockCacheConf); _iCache = cacheManager.getBlockCache(CacheType.INDEX); _dCache = cacheManager.getBlockCache(CacheType.DATA); _sCache = cacheManager.getBlockCache(CacheType.SUMMARY); + // Build the optional secondary compressed data cache. + long compressedCacheSize = blockCacheConf.getCompressedDataMaxSize(); + if (compressedCacheSize > 0) { + BlockCacheManager.Configuration compressedConf = new BlockCacheManager.Configuration() { + @Override + public long getMaxSize(CacheType type) { + return compressedCacheSize; + } + + @Override + public long getBlockSize() { + return blockCacheConf.getBlockSize(); + } + + @Override + public Map getProperties(String prefix, CacheType type) { + return Collections.emptyMap(); + } + }; + BlockCache underlying = new TinyLfuBlockCache(compressedConf, CacheType.DATA); + _compressedDCache = new CompressedBlockCache(underlying); + log.info("Secondary compressed data cache enabled, max size: {} bytes", compressedCacheSize); + } else { + _compressedDCache = null; + } + long dCacheSize = _dCache == null ? 0 : _dCache.getMaxHeapSize(); long iCacheSize = _iCache == null ? 0 : _iCache.getMaxHeapSize(); long sCacheSize = _sCache == null ? 0 : _sCache.getMaxHeapSize(); + long compressedCacheHeapSize = _compressedDCache == null ? 0 : compressedCacheSize; Runtime runtime = Runtime.getRuntime(); if (usingNativeMap) { // Still check block cache sizes when using native maps. - if (dCacheSize + iCacheSize + sCacheSize + totalQueueSize > runtime.maxMemory()) { + if (dCacheSize + iCacheSize + sCacheSize + compressedCacheHeapSize + totalQueueSize + > runtime.maxMemory()) { throw new IllegalArgumentException(String.format( "Block cache sizes %,d and mutation queue size %,d is too large for this JVM" + " configuration %,d", - dCacheSize + iCacheSize + sCacheSize, totalQueueSize, runtime.maxMemory())); + dCacheSize + iCacheSize + sCacheSize, compressedCacheHeapSize, totalQueueSize, + runtime.maxMemory())); } - } else if (maxMemory + dCacheSize + iCacheSize + sCacheSize + totalQueueSize - > runtime.maxMemory()) { + } else if (maxMemory + dCacheSize + iCacheSize + sCacheSize + compressedCacheHeapSize + + totalQueueSize > runtime.maxMemory()) { throw new IllegalArgumentException(String.format( "Maximum tablet server" + " map memory %,d block cache sizes %,d and mutation queue size %,d is" + " too large for this JVM configuration %,d", - maxMemory, dCacheSize + iCacheSize + sCacheSize, totalQueueSize, runtime.maxMemory())); + maxMemory, dCacheSize + iCacheSize + sCacheSize, compressedCacheHeapSize, totalQueueSize, + runtime.maxMemory())); } runtime.gc(); @@ -708,7 +744,7 @@ public synchronized ScanFileManager newScanFileManager(ScanDispatch scanDispatch } return fileManager.newScanFileManager(extent, - new ScanCacheProvider(tableConf, scanDispatch, _iCache, _dCache)); + new ScanCacheProvider(tableConf, scanDispatch, _iCache, _dCache, _compressedDCache)); } // END methods that Tablets call to manage their set of open data files @@ -885,6 +921,10 @@ public BlockCache getSummaryCache() { return _sCache; } + public BlockCache getCompressedDataCache() { + return _compressedDCache; + } + public Cache getFileLenCache() { return fileLenCache; }