All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.facebook.concurrency.ExpiringConcurrentCache Maven / Gradle / Ivy

There is a newer version: 0.1.32
Show newest version
/*
 * Copyright (C) 2012 Facebook, Inc.
 *
 * 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.facebook.concurrency;

import com.facebook.collectionsbase.Mapper;
import com.facebook.collections.TranslatingIterator;
import com.facebook.util.exceptions.ExceptionHandler;
import org.apache.log4j.Logger;
import org.joda.time.DateTimeUtils;

import java.util.AbstractMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

public class ExpiringConcurrentCache
  implements ConcurrentCache {
  private static final Logger LOG = Logger.getLogger(ExpiringConcurrentCache.class);

  private final ConcurrentCache, E> baseCache;
  private final long maxAgeMillis;
  private final ExecutorService executor;

  // track the last time a prune operation was performed. Objects 
  // will only be pruned after maxAgeMillis has passed
  private final AtomicLong lastPrune = new AtomicLong(
    DateTimeUtils.currentTimeMillis()
  );
  private final AtomicBoolean pruning = new AtomicBoolean(false);
  // an EvictionListener provides a memory efficient way for clients of this
  // object to receive information about both the key and value evicted.
  private final EvictionListener evictionListener;

  public ExpiringConcurrentCache(
    ValueFactory valueFactory,
    long maxAge,
    TimeUnit maxAgeUnit,
    EvictionListener evictionListener,
    ExceptionHandler exceptionHandler,
    ExecutorService executor
  ) {
    this.evictionListener = evictionListener;
    this.baseCache =
      new CoreConcurrentCache, E>(
        new CacheEntryValueFactory(valueFactory),
        exceptionHandler
      );
    this.maxAgeMillis = maxAgeUnit.toMillis(maxAge);
    this.executor = executor;
  }

  public ExpiringConcurrentCache(
    ValueFactory valueFactory,
    long maxAge,
    TimeUnit maxAgeUnit,
    EvictionListener evictionListener,
    ExceptionHandler exceptionHandler
  ) {
    this(
      valueFactory,
      maxAge,
      maxAgeUnit,
      evictionListener,
      exceptionHandler,
      Executors.newSingleThreadExecutor()
    );
  }

  /**
   * compatibility function for use with legacy implementations that use
   * Reapable to be notified of evictions
   * 
   * @param valueFactory
   * @param maxAge
   * @param maxAgeUnit
   * @param exceptionHandler
   * @param executor
   * @param 
   * @param 
   * @param 
   * @return
   */
  public static , E extends Exception>
  ExpiringConcurrentCache createWithReapableValue(
    ValueFactory valueFactory,
    long maxAge,
    TimeUnit maxAgeUnit,
    ExceptionHandler exceptionHandler,
    ExecutorService executor
  ) {
    return new ExpiringConcurrentCache(
      valueFactory,
      maxAge,
      maxAgeUnit,
      new EvictionListener() {
        @Override
        public void evicted(K key, V value) {
          try {
            value.shutdown();
          } catch (Throwable t) {
            LOG.error("error shutting down reapable", t);
          }
        }
      },
      exceptionHandler,
      executor
    );
  }

  @Override
  public V get(K key) throws E {
    CacheEntry cacheEntry = baseCache.get(key);

    CallableSnapshot snapshot = cacheEntry.touch();

    // prune after getting the value
    pruneIfNeeded();

    return snapshot.get();
  }

  @Override
  public V put(K key, V value) throws E {
    pruneIfNeeded();
    
    CacheEntry cacheEntry = new CacheEntry(value);
    CacheEntry existingCacheEntry = baseCache.put(key, cacheEntry);

    return existingCacheEntry == null ? null : existingCacheEntry.getSnapshot().get();
  }

  @Override
  public V remove(K key) throws E {
    pruneIfNeeded();

    CacheEntry cacheEntry = baseCache.remove(key);
    
    return cacheEntry == null ? null : cacheEntry.getSnapshot().get();
  }

  @Override
  public boolean removeIfError(K key) {
    return baseCache.removeIfError(key);
  }

  @Override
  public void clear() {
    baseCache.clear();
  }

  @Override
  public void prune() throws E {
    pruneIfNeeded();
  }

  @Override
  public int size() {
    return baseCache.size();
  }

  /**
   * non-blocking, thread-safe prune operation that only enters pruning block
   * after enough time has elapsed
   *
   * @throws E
   */
  private void pruneIfNeeded() {
    // only prune if sufficient time has elapsed and another thread isn't
    // already pruning
    if (DateTimeUtils.currentTimeMillis() - lastPrune.get() >= maxAgeMillis &&
      pruning.compareAndSet(false, true)) {
      try {
        Iterator, E>>> iterator =
          baseCache.iterator();

        while (iterator.hasNext()) {
          final K key;
          final CacheEntry cacheEntry;
          try {
            Map.Entry, E>> entry =
              iterator.next();
            key = entry.getKey();
            cacheEntry = entry.getValue().get();
          } catch (Exception e) {
            // We control the creation process, so should not get an exception
            throw new RuntimeException("CacheEntry create should not fail");
          }

          if (cacheEntry.hasExpired(maxAgeMillis)) {
            // remove the item from the cache
            iterator.remove();

            // do any shutdown() tasks asynchronously so we don't block access
            // to the cache
            executor.execute(
              new Runnable() {
                @Override
                public void run() {
                  // now reap the entry
                  try {
                    V value = cacheEntry.getSnapshot().get();

                    try {
                      evictionListener.evicted(key, value);
                    } catch (Throwable t) {
                      LOG.error(
                        "Error reaping cache element-- may not be properly closed",
                        t
                      );
                    }
                  } catch (Exception e) {
                    LOG.info(
                      "Unable to get cache value for key " + key
                    );
                    // still notify that key is evicted
                    evictionListener.evicted(key, null);
                  }
                }
              }
            );

          }
        }
      } finally {
        lastPrune.set(DateTimeUtils.currentTimeMillis());
        pruning.set(false);
      }
    }
  }

  @Override
  public Iterator>> iterator() {
    return new TranslatingIterator<
      Map.Entry, E>>,
      Map.Entry>
      >(
      new ValueMapper(), baseCache.iterator()
    );
  }

  @Override
  public CallableSnapshot getIfPresent(K key) {
    pruneIfNeeded();
    CallableSnapshot, E> snapshot =
      baseCache.getIfPresent(key);

    if (snapshot == null) {
      return null;
    } else {
      try {
        return snapshot.get().getSnapshot();
      } catch (Exception e) {
        throw new RuntimeException("this shouldn't happen", e);
      }
    }
  }

  private class ValueMapper implements
    Mapper<
      Map.Entry, E>>,
      Map.Entry>
      > {
    @Override
    public Map.Entry> map(
      Map.Entry, E>> input
    ) {
      CallableSnapshot snapshot;
      try {
        snapshot = input.getValue().get().touch();
      } catch (Exception e) {
        // We control the creation process, so we should not get an exception
        throw new RuntimeException("CacheEntry create should not fail");
      }
      return new AbstractMap.SimpleImmutableEntry>(
        input.getKey(),
        snapshot
      );
    }
  }

  private class CacheEntryValueFactory
    implements ValueFactory, E> {
    CallableSnapshotFunction snapshotFunction;

    private CacheEntryValueFactory(ValueFactory valueFactory) {
      snapshotFunction =
        new PrivateCallableSnapshotFunction(valueFactory);
    }

    @Override
    public CacheEntry create(K input) {
      return new CacheEntry(snapshotFunction.apply(input));
    }
  }

  /**
   * a cache entry is a value and it's last accessed time (create, read).
   * The last accessed is used for expiring entire older than a configured
   * TTL by the cache
   *
   * @param  value type
   * @param  exception type
   */
  @SuppressWarnings({"unchecked"})
  private static class CacheEntry {
    // mtime guarded by this
    private long mtime = DateTimeUtils.currentTimeMillis();
    private volatile Object snapshotOrValue;

    private CacheEntry(V value) {
      this.snapshotOrValue = value;
    }

    private CacheEntry(CallableSnapshot snapshot) {
      // if the snapshot indicates no error, store just the value. This
      // will save us about 24 bytes on a 64-bit box:  2 x 8 byte ptr and
      // the 8-byte overhead java adds for each object (saved by not
      // having the CallableSnapshot)
      if (snapshot.getException() == null) {
        try {
          snapshotOrValue = snapshot.get();
        } catch (Exception e) {
          LOG.error("this should NEVER be seen", e);
          snapshotOrValue = snapshot;
        }
      } else {
        snapshotOrValue = snapshot;
      }
    }

    public CallableSnapshot getSnapshot() throws E {
      return getCallableSnapshot();
    }

    public synchronized CallableSnapshot touch() {
      mtime = DateTimeUtils.currentTimeMillis();

      return getCallableSnapshot();
    }

    public synchronized boolean hasExpired(long maxAgeMillis) {
      return DateTimeUtils.currentTimeMillis() - mtime >= maxAgeMillis;
    }

    /**
     * we store either the result of the Callable if there is no 
     * exception (saves memory). Otherewise, we keep the whole CallableSnapshot
     *
     * @return
     */
    private CallableSnapshot getCallableSnapshot() {
      if (snapshotOrValue instanceof PrivateCallableSnapshot) {
        return (CallableSnapshot) snapshotOrValue;
      } else {
        // we can use NullExceptionHandler since we know
        // this is a value
        return new PrivateCallableSnapshot(
          new FixedValueCallable((V) snapshotOrValue),
          new NullExceptionHandler()
        );
      }
    }
  }

  // this private class is using it's class type as a boolean flag (to save
  // memory). If the value stored is of this type in the CacheEntry,
  // then it means we couldn't store the value and need to call 
  // CallableSnapshot.get(). By making it private, we guarantee that O
  // cannot be this type
  private static class PrivateCallableSnapshotFunction
    
    implements CallableSnapshotFunction {

    private final ValueFactory valueFactory;
    private final ExceptionHandler exceptionHandler;

    private PrivateCallableSnapshotFunction(
      ValueFactory valueFactory, ExceptionHandler exceptionHandler
    ) {
      this.valueFactory = valueFactory;
      this.exceptionHandler = exceptionHandler;
    }

    private PrivateCallableSnapshotFunction(ValueFactory valueFactory) {
      // We can cast exceptions because the value factory declares which type
      // of exceptions it can throw on creation
      this(valueFactory, new CastingExceptionHandler());
    }

    @Override
    public CallableSnapshot apply(final I input) {
      return new PrivateCallableSnapshot(
        new Callable() {
          @Override
          public O call() throws E {
            return valueFactory.create(input);
          }
        },
        exceptionHandler
      );
    }

  }

  private static class PrivateCallableSnapshot
    extends CallableSnapshot {
    private PrivateCallableSnapshot(
      Callable callable,
      ExceptionHandler exceptionHandler
    ) {
      super(callable, exceptionHandler);
    }
  }


  // protected for unit testing
  protected long getNow() {
    return DateTimeUtils.currentTimeMillis();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy