package thredds.crawlabledataset.s3; import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; import com.amazonaws.services.s3.model.ObjectListing; import com.amazonaws.services.s3.model.ObjectMetadata; import com.google.common.base.Optional; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.io.Files; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A ThreddsS3Client that wraps another ThreddsS3Client and caches its methods' return values for efficiency. * * @author cwardgar * @since 2015/08/22 */ public class CachingThreddsS3Client implements ThreddsS3Client { private static final Logger logger = LoggerFactory.getLogger(CachingThreddsS3Client.class); private static final long ENTRY_EXPIRATION_TIME = 1; // In hours. private static final long MAX_METADATA_ENTRIES = 10000; private static final long MAX_FILE_ENTRIES = 100; private final ThreddsS3Client threddsS3Client; private final Cache<S3URI, Optional<ObjectMetadata>> objectMetadataCache; private final Cache<S3URI, Optional<ObjectListing>> objectListingCache; private final Cache<S3URI, Optional<File>> objectFileCache; public CachingThreddsS3Client(ThreddsS3Client threddsS3Client) { this(threddsS3Client, new ObjectFileCacheRemovalListener()); } public CachingThreddsS3Client( ThreddsS3Client threddsS3Client, RemovalListener<S3URI, Optional<File>> removalListener) { this.threddsS3Client = threddsS3Client; // We can't reuse the builder because each of the caches we're creating has different type parameters. this.objectMetadataCache = CacheBuilder.newBuilder() .expireAfterAccess(ENTRY_EXPIRATION_TIME, TimeUnit.HOURS) .maximumSize(MAX_METADATA_ENTRIES) .build(); this.objectListingCache = CacheBuilder.newBuilder() .expireAfterAccess(ENTRY_EXPIRATION_TIME, TimeUnit.HOURS) .maximumSize(MAX_METADATA_ENTRIES) .build(); this.objectFileCache = CacheBuilder.newBuilder() .expireAfterAccess(ENTRY_EXPIRATION_TIME, TimeUnit.HOURS) .maximumSize(MAX_FILE_ENTRIES) .removalListener(removalListener) .build(); } private static class ObjectFileCacheRemovalListener implements RemovalListener<S3URI, Optional<File>> { @Override public void onRemoval(RemovalNotification<S3URI, Optional<File>> notification) { Optional<File> file = notification.getValue(); assert file != null : "Silence a silly IntelliJ warning. Of course the Optional isn't null."; if (file.isPresent()) { file.get().delete(); } } } @Override public ObjectMetadata getObjectMetadata(S3URI s3uri) { Optional<ObjectMetadata> metadata = objectMetadataCache.getIfPresent(s3uri); if (metadata == null) { logger.debug(String.format("ObjectMetadata cache MISS: '%s'", s3uri)); metadata = Optional.fromNullable(threddsS3Client.getObjectMetadata(s3uri)); objectMetadataCache.put(s3uri, metadata); } else { logger.debug(String.format("ObjectMetadata cache hit: '%s'", s3uri)); } return metadata.orNull(); } @Override public ObjectListing listObjects(S3URI s3uri) { Optional<ObjectListing> objectListing = objectListingCache.getIfPresent(s3uri); if (objectListing == null) { logger.debug(String.format("ObjectListing cache MISS: '%s'", s3uri)); objectListing = Optional.fromNullable(threddsS3Client.listObjects(s3uri)); objectListingCache.put(s3uri, objectListing); } else { logger.debug(String.format("ObjectListing cache hit: '%s'", s3uri)); } return objectListing.orNull(); } /** * {@inheritDoc} * <p> * WARNING: If the content at {@code s3uri} was previously saved using this method, and the old file to which it was * saved is <b>not</b> the same as {@code file}, the object content will be copied to the new file and the old file * will be deleted. */ @Override public File saveObjectToFile(S3URI s3uri, File file) throws IOException { Optional<File> cachedFile = objectFileCache.getIfPresent(s3uri); if (cachedFile == null) { logger.debug("Object cache MISS: '%s'", s3uri); // Do download below. } else { logger.debug("Object cache hit: '%s'", s3uri); if (!cachedFile.isPresent()) { return null; } else if (!cachedFile.get().exists()) { logger.info(String.format("Found cache entry {'%s'-->'%s'}, but local file doesn't exist. " + "Was it deleted? Re-downloading.", s3uri, cachedFile.get())); objectFileCache.invalidate(s3uri); // Evict old entry. Re-download below. } else if (!cachedFile.get().equals(file)) { // Copy content of cachedFile to file. Evict cachedFile from the cache. Files.copy(cachedFile.get(), file); objectFileCache.put(s3uri, Optional.of(file)); return file; } else { return file; // File already contains the content of the object at s3uri. } } cachedFile = Optional.fromNullable(threddsS3Client.saveObjectToFile(s3uri, file)); objectFileCache.put(s3uri, cachedFile); return cachedFile.orNull(); } /** * Discards all entries from all caches. Any files created by {@link #saveObjectToFile} will be deleted. */ public void clear() { objectMetadataCache.invalidateAll(); objectListingCache.invalidateAll(); objectFileCache.invalidateAll(); } }