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

com.netflix.spectator.impl.LfuCache Maven / Gradle / Ivy

There is a newer version: 1.7.21
Show newest version
/*
 * Copyright 2014-2021 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.spectator.impl;

import com.netflix.spectator.api.Counter;
import com.netflix.spectator.api.Registry;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * LFU implementation of {@link Cache}.
 */
class LfuCache implements Cache {

  private final Counter hits;
  private final Counter misses;
  private final Counter compactions;

  private final ConcurrentHashMap> data;
  private final int baseSize;
  private final int compactionSize;

  // Collections that are reused for each compaction operation
  private final PriorityQueue> mostFrequentItems;
  private final List mostFrequentKeys;

  private final AtomicInteger size;

  private final Lock lock;

  LfuCache(Registry registry, String id, int baseSize, int compactionSize) {
    this.hits = registry.counter("spectator.cache.requests", "id", id, "result", "hit");
    this.misses = registry.counter("spectator.cache.requests", "id", id, "result", "miss");
    this.compactions = registry.counter("spectator.cache.compactions", "id", id);
    data = new ConcurrentHashMap<>();
    this.baseSize = baseSize;
    this.compactionSize = compactionSize;
    this.mostFrequentItems = new PriorityQueue<>(baseSize, SNAPSHOT_COMPARATOR);
    this.mostFrequentKeys = new ArrayList<>(baseSize);
    this.size = new AtomicInteger();
    this.lock = new ReentrantLock();
  }

  private void addIfMoreFrequent(K key, Pair value) {
    long count = value.snapshot();
    if (mostFrequentItems.size() >= baseSize) {
      // Queue is full, add new item if it is more frequently used than the least
      // frequent item currently in the queue.
      Snapshot leastFrequentItem = mostFrequentItems.peek();
      if (leastFrequentItem != null && count > leastFrequentItem.count()) {
        mostFrequentItems.poll();
        mostFrequentItems.offer(new Snapshot<>(key, count));
      }
    } else {
      mostFrequentItems.offer(new Snapshot<>(key, count));
    }
  }

  private void compact() {
    int numToRemove = size.get() - baseSize;
    if (numToRemove > 0) {
      mostFrequentItems.clear();
      mostFrequentKeys.clear();

      data.forEach(this::addIfMoreFrequent);
      mostFrequentItems.forEach(s -> mostFrequentKeys.add(s.get()));
      data.keySet().retainAll(mostFrequentKeys);

      size.set(data.size());
      compactions.increment();
    }
  }

  private void tryCompact() {
    if (lock.tryLock()) {
      try {
        compact();
      } finally {
        lock.unlock();
      }
    }
  }

  @Override
  public V get(K key) {
    Pair value = data.get(key);
    if (value == null) {
      misses.increment();
      return null;
    } else {
      hits.increment();
      return value.get();
    }
  }

  @Override public V peek(K key) {
    Pair value = data.get(key);
    return value == null ? null : value.peek();
  }

  @Override public void put(K key, V value) {
    Pair prev = data.put(key, new Pair<>(value));
    if (prev == null && size.incrementAndGet() > compactionSize) {
      tryCompact();
    }
  }

  @Override public V computeIfAbsent(K key, Function f) {
    Pair value = data.get(key);
    if (value == null) {
      misses.increment();
      Pair tmp = new Pair<>(f.apply(key));
      value = data.putIfAbsent(key, tmp);
      if (value == null) {
        value = tmp;
        if (size.incrementAndGet() > compactionSize) {
          tryCompact();
        }
      }
    } else {
      hits.increment();
    }
    return value.get();
  }

  @Override public void clear() {
    size.set(0);
    data.clear();
  }

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

  @Override public Map asMap() {
    return data.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().peek()));
  }

  private static class Pair {
    private final V value;
    private final LongAdder count;

    Pair(V value) {
      this.value = value;
      this.count = new LongAdder();
    }

    V get() {
      count.increment();
      return value;
    }

    V peek() {
      return value;
    }

    long snapshot() {
      return count.sum();
    }
  }

  private static class Snapshot {
    private final K key;
    private final long count;

    Snapshot(K key, long count) {
      this.key = key;
      this.count = count;
    }

    K get() {
      return key;
    }

    long count() {
      return count;
    }
  }

  // Comparator for finding the least frequent items with the priority queue
  private static final Comparator> SNAPSHOT_COMPARATOR =
      (a, b) -> Long.compare(b.count(), a.count());
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy