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

com.netflix.servo.util.ExpiringCache Maven / Gradle / Ivy

There is a newer version: 0.13.2
Show newest version
/*
 * Copyright 2014 Netflix, 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.netflix.servo.util;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * A semi-persistent mapping from keys to values. Values are automatically loaded
 * by the cache, and are stored in the cache until evicted.
 *
 * @param  The type of keys maintained
 * @param  The type of values maintained
 */
public class ExpiringCache {
  private final ConcurrentHashMap> map;
  private final long expireAfterMs;
  private final Function> entryGetter;
  private final Clock clock;

  private static final class Entry {
    private volatile long accessTime;
    private final V value;
    private final Clock clock;

    private Entry(V value, long accessTime, Clock clock) {
      this.value = value;
      this.accessTime = accessTime;
      this.clock = clock;
    }

    private V getValue() {
      accessTime = clock.now();
      return value;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }

      Entry entry = (Entry) o;

      return accessTime == entry.accessTime && value.equals(entry.value);
    }

    @Override
    public int hashCode() {
      int result = (int) (accessTime ^ (accessTime >>> 32));
      result = 31 * result + value.hashCode();
      return result;
    }

    @Override
    public String toString() {
      return "Entry{accessTime=" + accessTime + ", value=" + value + '}';
    }
  }

  private static final ScheduledExecutorService SERVICE =
      Executors.newSingleThreadScheduledExecutor(ThreadFactories.withName("expiringMap-%d"));

  /**
   * Create a new ExpiringCache that will expire entries after a given number of milliseconds
   * computing the values as needed using the given getter.
   *
   * @param expireAfterMs Number of milliseconds after which entries will be evicted
   * @param getter        Function that will be used to compute the values
   */
  public ExpiringCache(final long expireAfterMs, final Function getter) {
    this(expireAfterMs, getter, TimeUnit.MINUTES.toMillis(1), ClockWithOffset.INSTANCE);
  }

  /**
   * For unit tests.
   * Create a new ExpiringCache that will expire entries after a given number of milliseconds
   * computing the values as needed using the given getter.
   *
   * @param expireAfterMs    Number of milliseconds after which entries will be evicted
   * @param getter           Function that will be used to compute the values
   * @param expirationFreqMs Frequency at which to schedule the job that evicts entries
   *                         from the cache.
   */
  public ExpiringCache(final long expireAfterMs, final Function getter,
                       final long expirationFreqMs, final Clock clock) {
    Preconditions.checkArgument(expireAfterMs > 0, "expireAfterMs must be positive.");
    Preconditions.checkArgument(expirationFreqMs > 0, "expirationFreqMs must be positive.");
    this.map = new ConcurrentHashMap<>();
    this.expireAfterMs = expireAfterMs;
    this.entryGetter = toEntry(getter);
    this.clock = clock;
    final Runnable expirationJob = () -> {
      long tooOld = clock.now() - expireAfterMs;
      map.entrySet().stream().filter(entry -> entry.getValue().accessTime < tooOld)
          .forEach(entry -> map.remove(entry.getKey(), entry.getValue()));
    };
    SERVICE.scheduleWithFixedDelay(expirationJob, 1, expirationFreqMs, TimeUnit.MILLISECONDS);
  }

  private Function> toEntry(final Function underlying) {
    return key -> new Entry<>(underlying.apply(key), 0L, clock);
  }

  /**
   * This method should be used instead of the
   * {@link ConcurrentHashMap#computeIfAbsent(Object, Function)} call to minimize
   * thread contention. This method does not require locking for the common case
   * where the key exists, but potentially performs additional computation when
   * absent.
   */
  private Entry computeIfAbsent(K key) {
    Entry v = map.get(key);
    if (v == null) {
      Entry tmp = entryGetter.apply(key);
      v = map.putIfAbsent(key, tmp);
      if (v == null) {
        v = tmp;
      }
    }
    return v;
  }

  /**
   * Get the (possibly cached) value for a given key.
   */
  public V get(final K key) {
    Entry entry = computeIfAbsent(key);
    return entry.getValue();
  }

  /**
   * Get the list of all values that are members of this cache. Does not
   * affect the access time used for eviction.
   */
  public List values() {
    final Collection> values = map.values();
    // Note below that e.value avoids updating the access time
    final List res = values.stream().map(e -> e.value).collect(Collectors.toList());
    return Collections.unmodifiableList(res);
  }

  /**
   * Return the number of entries in the cache.
   */
  public int size() {
    return map.size();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    return "ExpiringCache{"
        + "map=" + map
        + ", expireAfterMs=" + expireAfterMs
        + '}';
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy