package com.apollographql.apollo.cache.normalized.lru; import com.apollographql.apollo.api.internal.Function; import com.apollographql.apollo.api.internal.Optional; import com.apollographql.apollo.cache.CacheHeaders; import com.apollographql.apollo.cache.ApolloCacheHeaders; import com.apollographql.apollo.cache.normalized.NormalizedCache; import com.apollographql.apollo.cache.normalized.NormalizedCacheFactory; import com.apollographql.apollo.cache.normalized.Record; import com.apollographql.apollo.cache.normalized.RecordFieldAdapter; import com.nytimes.android.external.cache.Cache; import com.nytimes.android.external.cache.CacheBuilder; import com.nytimes.android.external.cache.Weigher; import java.util.Collections; import java.util.Set; import java.util.concurrent.Callable; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * A {@link NormalizedCache} backed by an in memory {@link Cache}. Can be configured with an optional secondaryCache * {@link NormalizedCache}, which will be used as a backup if a {@link Record} is not present in the primary cache. * * A common configuration is to have secondary SQL cache. */ public final class LruNormalizedCache extends NormalizedCache { private final Cache<String, Record> lruCache; private final Optional<NormalizedCache> secondaryCache; LruNormalizedCache(final RecordFieldAdapter recordFieldAdapter, EvictionPolicy evictionPolicy, Optional<NormalizedCacheFactory> secondaryNormalizedCache) { super(recordFieldAdapter); this.secondaryCache = secondaryNormalizedCache.transform(new Function<NormalizedCacheFactory, NormalizedCache>() { @Nonnull @Override public NormalizedCache apply(@Nonnull NormalizedCacheFactory normalizedCacheFactory) { return normalizedCacheFactory.createNormalizedCache(recordFieldAdapter); } }); final CacheBuilder<Object, Object> lruCacheBuilder = CacheBuilder.newBuilder(); if (evictionPolicy.maxSizeBytes().isPresent()) { lruCacheBuilder.maximumWeight(evictionPolicy.maxSizeBytes().get()) .weigher(new Weigher<String, Record>() { @Override public int weigh(String key, Record value) { return key.getBytes().length + value.sizeEstimateBytes(); } }); } if (evictionPolicy.maxEntries().isPresent()) { lruCacheBuilder.maximumSize(evictionPolicy.maxEntries().get()); } if (evictionPolicy.expireAfterAccess().isPresent()) { lruCacheBuilder.expireAfterAccess(evictionPolicy.expireAfterAccess().get(), evictionPolicy.expireAfterAccessTimeUnit().get()); } if (evictionPolicy.expireAfterWrite().isPresent()) { lruCacheBuilder.expireAfterWrite(evictionPolicy.expireAfterWrite().get(), evictionPolicy.expireAfterWriteTimeUnit().get()); } lruCache = lruCacheBuilder.build(); } @Nullable public NormalizedCache secondaryCache() { return secondaryCache.get(); } @Nullable @Override public Record loadRecord(@Nonnull final String key, @Nonnull final CacheHeaders cacheHeaders) { final Record record; if (secondaryCache.isPresent()) { try { record = lruCache.get(key, new Callable<Record>() { @Override public Record call() throws Exception { Record record = secondaryCache.get().loadRecord(key, cacheHeaders); // get(key, callable) requires non-null. If null, an exception should be //thrown, which will be converted to null in the catch clause. if (record == null) { throw new Exception(String.format("Record{key=%s} not present in secondary cache", key)); } return record; } }); } catch (Exception e) { return null; } } else { record = lruCache.getIfPresent(key); } if (record != null && cacheHeaders.hasHeader(ApolloCacheHeaders.EVICT_AFTER_READ)) { lruCache.invalidate(key); } return record; } @Nonnull @Override public Set<String> merge(@Nonnull Record apolloRecord, @Nonnull CacheHeaders cacheHeaders) { if (cacheHeaders.hasHeader(ApolloCacheHeaders.DO_NOT_STORE)) { return Collections.emptySet(); } if (secondaryCache.isPresent()) { secondaryCache.get().merge(apolloRecord, cacheHeaders); } final Record oldRecord = lruCache.getIfPresent(apolloRecord.key()); if (oldRecord == null) { lruCache.put(apolloRecord.key(), apolloRecord); return Collections.emptySet(); } else { Set<String> changedKeys = oldRecord.mergeWith(apolloRecord); //re-insert to trigger new weight calculation lruCache.put(apolloRecord.key(), oldRecord); return changedKeys; } } @Override public void clearAll() { clearPrimaryCache(); clearSecondaryCache(); } /** * Clears all records from the in-memory LRU cache. The secondary cache will *not* be cleared. * * This method is **not** guaranteed to be thread safe. It should be run inside a write transaction in * {@link com.apollographql.apollo.cache.normalized.ApolloStore}, obtained from * {@link com.apollographql.apollo.ApolloClient#apolloStore()}. */ public void clearPrimaryCache() { lruCache.invalidateAll(); } /** * Clear all records from the secondary cache. Records in the in-memory LRU cache will remain. * * This method is **not** guaranteed to be thread safe. It should be run inside a write transaction in * {@link com.apollographql.apollo.cache.normalized.ApolloStore}, obtained from * {@link com.apollographql.apollo.ApolloClient#apolloStore()}. */ public void clearSecondaryCache() { if (secondaryCache.isPresent()) { secondaryCache.get().clearAll(); } } }