diff --git a/core/src/main/java/org/apache/iceberg/CatalogProperties.java b/core/src/main/java/org/apache/iceberg/CatalogProperties.java
index e9bc7d556ba3..95fe6a074c0a 100644
--- a/core/src/main/java/org/apache/iceberg/CatalogProperties.java
+++ b/core/src/main/java/org/apache/iceberg/CatalogProperties.java
@@ -57,6 +57,60 @@ private CatalogProperties() {}
public static final long CACHE_EXPIRATION_INTERVAL_MS_DEFAULT = TimeUnit.SECONDS.toMillis(30);
public static final long CACHE_EXPIRATION_INTERVAL_MS_OFF = -1;
+ /**
+ * Controls whether to use caching during manifest reads or not.
+ *
+ *
Enabling manifest file caching require the following configuration constraints to be true:
+ *
+ *
+ *
{@link #IO_MANIFEST_CACHE_EXPIRATION_INTERVAL_MS} must be a non-negative value.
+ *
{@link #IO_MANIFEST_CACHE_MAX_TOTAL_BYTES} must be a positive value.
+ *
{@link #IO_MANIFEST_CACHE_MAX_CONTENT_LENGTH} must be a positive value.
+ *
+ */
+ public static final String IO_MANIFEST_CACHE_ENABLED = "io.manifest.cache-enabled";
+
+ public static final boolean IO_MANIFEST_CACHE_ENABLED_DEFAULT = false;
+
+ /**
+ * Controls the maximum duration for which an entry stays in the manifest cache.
+ *
+ *
Must be a non-negative value. Following are specific behaviors of this config:
+ *
+ *
+ *
Zero - Cache entries expires only if it gets evicted due to memory pressure from {@link
+ * #IO_MANIFEST_CACHE_MAX_TOTAL_BYTES} setting.
+ *
Positive Values - Cache entries expire if not accessed via the cache after this many
+ * milliseconds
+ *
+ */
+ public static final String IO_MANIFEST_CACHE_EXPIRATION_INTERVAL_MS =
+ "io.manifest.cache.expiration-interval-ms";
+
+ public static final long IO_MANIFEST_CACHE_EXPIRATION_INTERVAL_MS_DEFAULT =
+ TimeUnit.SECONDS.toMillis(60);
+
+ /**
+ * Controls the maximum total amount of bytes to cache in manifest cache.
+ *
+ *
Must be a positive value.
+ */
+ public static final String IO_MANIFEST_CACHE_MAX_TOTAL_BYTES =
+ "io.manifest.cache.max-total-bytes";
+
+ public static final long IO_MANIFEST_CACHE_MAX_TOTAL_BYTES_DEFAULT = 100 * 1024 * 1024;
+
+ /**
+ * Controls the maximum length of file to be considered for caching.
+ *
+ *
An {@link org.apache.iceberg.io.InputFile} will not be cached if the length is longer than
+ * this limit. Must be a positive value.
+ */
+ public static final String IO_MANIFEST_CACHE_MAX_CONTENT_LENGTH =
+ "io.manifest.cache.max-content-length";
+
+ public static final long IO_MANIFEST_CACHE_MAX_CONTENT_LENGTH_DEFAULT = 8 * 1024 * 1024;
+
public static final String URI = "uri";
public static final String CLIENT_POOL_SIZE = "clients";
public static final int CLIENT_POOL_SIZE_DEFAULT = 2;
diff --git a/core/src/main/java/org/apache/iceberg/ManifestFiles.java b/core/src/main/java/org/apache/iceberg/ManifestFiles.java
index f039907b8682..85e268d43378 100644
--- a/core/src/main/java/org/apache/iceberg/ManifestFiles.java
+++ b/core/src/main/java/org/apache/iceberg/ManifestFiles.java
@@ -18,6 +18,8 @@
*/
package org.apache.iceberg;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.util.Map;
import org.apache.iceberg.ManifestReader.FileType;
@@ -25,16 +27,23 @@
import org.apache.iceberg.avro.AvroSchemaUtil;
import org.apache.iceberg.exceptions.RuntimeIOException;
import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.ContentCache;
import org.apache.iceberg.io.FileIO;
import org.apache.iceberg.io.InputFile;
import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.util.PropertyUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ManifestFiles {
private ManifestFiles() {}
+ private static final Logger LOG = LoggerFactory.getLogger(ManifestFiles.class);
+
private static final org.apache.avro.Schema MANIFEST_AVRO_SCHEMA =
AvroSchemaUtil.convert(
ManifestFile.schema(),
@@ -44,6 +53,36 @@ private ManifestFiles() {}
ManifestFile.PARTITION_SUMMARY_TYPE,
GenericPartitionFieldSummary.class.getName()));
+ @VisibleForTesting
+ static Cache newManifestCache() {
+ return Caffeine.newBuilder()
+ .weakKeys()
+ .softValues()
+ .maximumSize(maxFileIO())
+ .removalListener(
+ (io, contentCache, cause) ->
+ LOG.debug("Evicted {} from FileIO-level cache ({})", io, cause))
+ .recordStats()
+ .build();
+ }
+
+ private static final Cache CONTENT_CACHES = newManifestCache();
+
+ @VisibleForTesting
+ static ContentCache contentCache(FileIO io) {
+ return CONTENT_CACHES.get(
+ io,
+ fileIO ->
+ new ContentCache(
+ cacheDurationMs(fileIO), cacheTotalBytes(fileIO), cacheMaxContentLength(fileIO)));
+ }
+
+ /** Drop manifest file cache object for a FileIO if exists. */
+ public static synchronized void dropCache(FileIO fileIO) {
+ CONTENT_CACHES.invalidate(fileIO);
+ CONTENT_CACHES.cleanUp();
+ }
+
/**
* Returns a {@link CloseableIterable} of file paths in the {@link ManifestFile}.
*
@@ -86,7 +125,7 @@ public static ManifestReader read(
manifest.content() == ManifestContent.DATA,
"Cannot read a delete manifest with a ManifestReader: %s",
manifest);
- InputFile file = io.newInputFile(manifest.path(), manifest.length());
+ InputFile file = newInputFile(io, manifest.path(), manifest.length());
InheritableMetadata inheritableMetadata = InheritableMetadataFactory.fromManifest(manifest);
return new ManifestReader<>(file, specsById, inheritableMetadata, FileType.DATA_FILES);
}
@@ -140,7 +179,7 @@ public static ManifestReader readDeleteManifest(
manifest.content() == ManifestContent.DELETES,
"Cannot read a data manifest with a DeleteManifestReader: %s",
manifest);
- InputFile file = io.newInputFile(manifest.path(), manifest.length());
+ InputFile file = newInputFile(io, manifest.path(), manifest.length());
InheritableMetadata inheritableMetadata = InheritableMetadataFactory.fromManifest(manifest);
return new ManifestReader<>(file, specsById, inheritableMetadata, FileType.DELETE_FILES);
}
@@ -300,4 +339,67 @@ private static ManifestFile copyManifestInternal(
return writer.toManifestFile();
}
+
+ private static InputFile newInputFile(FileIO io, String path, long length) {
+ boolean enabled = false;
+
+ try {
+ enabled = cachingEnabled(io);
+ } catch (UnsupportedOperationException e) {
+ // There is an issue reading io.properties(). Disable caching.
+ enabled = false;
+ }
+
+ if (enabled) {
+ ContentCache cache = contentCache(io);
+ Preconditions.checkNotNull(
+ cache,
+ "ContentCache creation failed. Check that all manifest caching configurations has valid value.");
+ LOG.debug("FileIO-level cache stats: {}", CONTENT_CACHES.stats());
+ return cache.tryCache(io, path, length);
+ }
+
+ // caching is not enable for this io or caught RuntimeException.
+ return io.newInputFile(path, length);
+ }
+
+ private static int maxFileIO() {
+ String value = System.getProperty(SystemProperties.IO_MANIFEST_CACHE_MAX_FILEIO);
+ if (value != null) {
+ try {
+ return Integer.parseUnsignedInt(value);
+ } catch (NumberFormatException e) {
+ // will return the default
+ }
+ }
+ return SystemProperties.IO_MANIFEST_CACHE_MAX_FILEIO_DEFAULT;
+ }
+
+ static boolean cachingEnabled(FileIO io) {
+ return PropertyUtil.propertyAsBoolean(
+ io.properties(),
+ CatalogProperties.IO_MANIFEST_CACHE_ENABLED,
+ CatalogProperties.IO_MANIFEST_CACHE_ENABLED_DEFAULT);
+ }
+
+ static long cacheDurationMs(FileIO io) {
+ return PropertyUtil.propertyAsLong(
+ io.properties(),
+ CatalogProperties.IO_MANIFEST_CACHE_EXPIRATION_INTERVAL_MS,
+ CatalogProperties.IO_MANIFEST_CACHE_EXPIRATION_INTERVAL_MS_DEFAULT);
+ }
+
+ static long cacheTotalBytes(FileIO io) {
+ return PropertyUtil.propertyAsLong(
+ io.properties(),
+ CatalogProperties.IO_MANIFEST_CACHE_MAX_TOTAL_BYTES,
+ CatalogProperties.IO_MANIFEST_CACHE_MAX_TOTAL_BYTES_DEFAULT);
+ }
+
+ static long cacheMaxContentLength(FileIO io) {
+ return PropertyUtil.propertyAsLong(
+ io.properties(),
+ CatalogProperties.IO_MANIFEST_CACHE_MAX_CONTENT_LENGTH,
+ CatalogProperties.IO_MANIFEST_CACHE_MAX_CONTENT_LENGTH_DEFAULT);
+ }
}
diff --git a/core/src/main/java/org/apache/iceberg/SystemProperties.java b/core/src/main/java/org/apache/iceberg/SystemProperties.java
index 3d44b195ffe1..1d3c00b97cf7 100644
--- a/core/src/main/java/org/apache/iceberg/SystemProperties.java
+++ b/core/src/main/java/org/apache/iceberg/SystemProperties.java
@@ -33,6 +33,14 @@ private SystemProperties() {}
/** Whether to use the shared worker pool when planning table scans. */
public static final String SCAN_THREAD_POOL_ENABLED = "iceberg.scan.plan-in-worker-pool";
+ /**
+ * Maximum number of distinct {@link org.apache.iceberg.io.FileIO} that is allowed to have
+ * associated {@link org.apache.iceberg.io.ContentCache} in memory at a time.
+ */
+ public static final String IO_MANIFEST_CACHE_MAX_FILEIO = "iceberg.io.manifest.cache.fileio-max";
+
+ public static final int IO_MANIFEST_CACHE_MAX_FILEIO_DEFAULT = 8;
+
static boolean getBoolean(String systemProperty, boolean defaultValue) {
String value = System.getProperty(systemProperty);
if (value != null) {
diff --git a/core/src/main/java/org/apache/iceberg/hadoop/HadoopFileIO.java b/core/src/main/java/org/apache/iceberg/hadoop/HadoopFileIO.java
index fc4c0d2f4879..8f34994d6374 100644
--- a/core/src/main/java/org/apache/iceberg/hadoop/HadoopFileIO.java
+++ b/core/src/main/java/org/apache/iceberg/hadoop/HadoopFileIO.java
@@ -35,11 +35,13 @@
import org.apache.iceberg.io.SupportsPrefixOperations;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Streams;
+import org.apache.iceberg.util.SerializableMap;
import org.apache.iceberg.util.SerializableSupplier;
public class HadoopFileIO implements FileIO, HadoopConfigurable, SupportsPrefixOperations {
private SerializableSupplier hadoopConf;
+ private SerializableMap properties = SerializableMap.copyOf(ImmutableMap.of());
/**
* Constructor used for dynamic FileIO loading.
@@ -61,6 +63,11 @@ public Configuration conf() {
return hadoopConf.get();
}
+ @Override
+ public void initialize(Map props) {
+ this.properties = SerializableMap.copyOf(props);
+ }
+
@Override
public InputFile newInputFile(String path) {
return HadoopInputFile.fromLocation(path, hadoopConf.get());
@@ -89,7 +96,7 @@ public void deleteFile(String path) {
@Override
public Map properties() {
- return ImmutableMap.of();
+ return properties.immutableMap();
}
@Override
diff --git a/core/src/main/java/org/apache/iceberg/io/ContentCache.java b/core/src/main/java/org/apache/iceberg/io/ContentCache.java
new file mode 100644
index 000000000000..c999f3f333f6
--- /dev/null
+++ b/core/src/main/java/org/apache/iceberg/io/ContentCache.java
@@ -0,0 +1,311 @@
+/*
+ * 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
+ *
+ * http://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.iceberg.io;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Weigher;
+import com.github.benmanes.caffeine.cache.stats.CacheStats;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.List;
+import java.util.function.Function;
+import org.apache.iceberg.exceptions.NotFoundException;
+import org.apache.iceberg.exceptions.ValidationException;
+import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class that provides file-content caching during reading.
+ *
+ *
The file-content caching is initiated by calling {@link ContentCache#tryCache(FileIO, String,
+ * long)}. Given a FileIO, a file location string, and file length that is within allowed limit,
+ * ContentCache will return a {@link CachingInputFile} that is backed by the cache. Calling {@link
+ * CachingInputFile#newStream()} will return a {@link ByteBufferInputStream} backed by list of
+ * {@link ByteBuffer} from the cache if such file-content exist in the cache. If the file-content
+ * does not exist in the cache yet, a regular InputFile will be instantiated, read-ahead, and loaded
+ * into the cache before returning ByteBufferInputStream. The regular InputFile is also used as a
+ * fallback if cache loading fail.
+ */
+public class ContentCache {
+ private static final Logger LOG = LoggerFactory.getLogger(ContentCache.class);
+ private static final int BUFFER_CHUNK_SIZE = 4 * 1024 * 1024; // 4MB
+
+ private final long expireAfterAccessMs;
+ private final long maxTotalBytes;
+ private final long maxContentLength;
+ private final Cache cache;
+
+ /**
+ * Constructor for ContentCache class.
+ *
+ * @param expireAfterAccessMs controls the duration for which entries in the ContentCache are hold
+ * since last access. Must be greater or equal than 0. Setting 0 means cache entries expire
+ * only if it gets evicted due to memory pressure.
+ * @param maxTotalBytes controls the maximum total amount of bytes to cache in ContentCache. Must
+ * be greater than 0.
+ * @param maxContentLength controls the maximum length of file to be considered for caching. Must
+ * be greater than 0.
+ */
+ public ContentCache(long expireAfterAccessMs, long maxTotalBytes, long maxContentLength) {
+ ValidationException.check(expireAfterAccessMs >= 0, "expireAfterAccessMs is less than 0");
+ ValidationException.check(maxTotalBytes > 0, "maxTotalBytes is equal or less than 0");
+ ValidationException.check(maxContentLength > 0, "maxContentLength is equal or less than 0");
+ this.expireAfterAccessMs = expireAfterAccessMs;
+ this.maxTotalBytes = maxTotalBytes;
+ this.maxContentLength = maxContentLength;
+
+ Caffeine