/* * Copyright (C) 2015 The Android Open Source Project * * Licensed 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 com.android.ide.common.caching; import static com.google.common.base.Preconditions.checkArgument; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.annotations.concurrency.GuardedBy; import com.google.common.collect.Maps; import java.util.Map; import java.util.concurrent.CountDownLatch; /** * A cache that handles creating the values when they are not present in the map. * * Calls to {@link #get(Object)} returns the value, calling into {@link ValueFactory} if it * was not created. If the creation takes a long time, other threads can still query the cache * for the same or different keys. Calls for the same key will block until the value has been * created. Calls for different keys will return right away if the key is available. * * This is very similar to Guava's LoadingCache, without the automated clean-up based on size * or time. * This is extracted from the PreDexCache of the Gradle plugin which has different requirements * (reloading cached info from disk) * * This class is thread-safe. * * TODO Move PreDexCache to be based on this. * */ public class CreatingCache<K, V> { @GuardedBy("this") private final Map<Object, V> mCache = Maps.newHashMap(); @GuardedBy("this") private final Map<Object, CountDownLatch> mProcessedValues = Maps.newHashMap(); @NonNull private final ValueFactory<K, V> mValueFactory; /** * A factory creating values based on keys. * @param <K> the type of the key * @param <V> the type of the value */ public interface ValueFactory<K, V> { /** * Creates a value based on a given key. * @param key the key * @return the value */ @NonNull V create(@NonNull K key); } public CreatingCache(@NonNull ValueFactory<K, V> valueFactory) { mValueFactory = valueFactory; } /** * Queries the cache for a given key. If the value is not present, this blocks until it is. * * If this is the first thread requesting the value, then this trigger creation of the value * through the {@link ValueFactory}. * * @param key the given key. * @return the value, or null if the thread was interrupted while waiting for the value to be created. */ @Nullable public V get(@NonNull K key) { return get(key, null); } /** * A Query Listener used for testing. * * @see #get(Object, QueryListener) */ @VisibleForTesting interface QueryListener { void onQueryState(@NonNull State state); } /** * Queries the cache for a given key. If the value is not present, this blocks until it is. * * This version allows for a listener that is notified when the state of the query is known. * This allows knowing the state while the method is blocked waiting for creation of the value * in this thread or another. This is used for testing. * * @param key the given key. * @param queryListener the listener. * @return the value, or null if the thread was interrupted while waiting for the value to be created. * * @see #get(Object) */ @VisibleForTesting V get(@NonNull K key, @Nullable QueryListener queryListener) { ValueState<V> state = findValueState(key); if (queryListener != null) { queryListener.onQueryState(state.getState()); } switch (state.getState()) { case EXISTING_VALUE: return state.getValue(); case NEW_VALUE: // create the actual value content. V value = mValueFactory.create(key); // add to cache, and enable other threads to use the value. addNewValue(key, value, state.getLatch()); return value; case PROCESSED_VALUE: // wait for value to become available try { state.getLatch().await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } synchronized (this) { // get it from the map cache. return mCache.get(key); } default: throw new IllegalStateException("unsupported ResultType: " + state.getState()); } } /** * Clears the cache of all values. * * @throws IllegalStateException if values are currently being created. */ public synchronized void clear() { if (!mProcessedValues.isEmpty()) { throw new IllegalStateException("Cache values are being processed"); } mCache.clear(); } /** * State of values. */ @VisibleForTesting enum State { EXISTING_VALUE, NEW_VALUE, PROCESSED_VALUE } /** * A Value State. This contains the Type as {@link State}, and a optional value {@link V} * or latch. * @param <V> the value type */ private static final class ValueState<V> { @NonNull private final State mType; private final V mValue; private final CountDownLatch mLatch; ValueState(V value) { this(State.EXISTING_VALUE, value, null); } ValueState(@NonNull State type, CountDownLatch latch) { this(type, null, latch); checkArgument(type != State.EXISTING_VALUE); } private ValueState(@NonNull State type, V value, CountDownLatch latch) { mType = type; mValue = value; mLatch = latch; } @NonNull public State getState() { return mType; } @NonNull public V getValue() { return mValue; } @NonNull public CountDownLatch getLatch() { return mLatch; } } /** * Returns the state of the value for a given key. * * If the value does not exist, prepares a latch to control availability of the value. * * @param key the key * @return a ValueState instance. */ @NonNull private synchronized ValueState<V> findValueState(@NonNull K key) { V value = mCache.get(key); // value exists, just return the state. if (value != null) { return new ValueState<V>(value); } // check if the value is currently being created CountDownLatch latch = mProcessedValues.get(key); if (latch != null) { // return the latch allowing to wait for end of creation. return new ValueState<V>(State.PROCESSED_VALUE, latch); } // new value: create a latch to allow others to wait until creation is done. latch = new CountDownLatch(1); mProcessedValues.put(key, latch); return new ValueState<V>(State.NEW_VALUE, latch); } /** * Adds a new value to the cache and release threads waiting for it. * @param key the key * @param value the value * @param latch the latch holding the threads. */ private synchronized void addNewValue( @NonNull K key, @NonNull V value, @NonNull CountDownLatch latch) { mCache.put(key, value); mProcessedValues.remove(key); latch.countDown(); } }