You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by nf...@apache.org on 2023/05/24 15:44:38 UTC
[camel] branch main updated: CAMEL-19295: Basic thread-safe LRU cache (#10157)
This is an automated email from the ASF dual-hosted git repository.
nfilotto pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 2f46ca6dc5e CAMEL-19295: Basic thread-safe LRU cache (#10157)
2f46ca6dc5e is described below
commit 2f46ca6dc5e1ca15ac179ac3f48d56c77f9dd2d8
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed May 24 17:44:27 2023 +0200
CAMEL-19295: Basic thread-safe LRU cache (#10157)
* CAMEL-19295: Experiment with sync LRUCache and test case that otherwise will OOME
* CAMEL-19295: Basic thread-safe LRU cache
---------
Co-authored-by: Nicolas Filotto <nf...@talend.com>
---
.../camel/impl/DefaultEndpointRegistryTest.java | 54 ++++
.../camel/support/DefaultLRUCacheFactoryTest.java | 292 +++++++++++++++++++++
.../camel/support/DefaultLRUCacheFactory.java | 253 +++++++++++++++++-
3 files changed, 590 insertions(+), 9 deletions(-)
diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultEndpointRegistryTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultEndpointRegistryTest.java
index d70fa1d996b..4bcae075a55 100644
--- a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultEndpointRegistryTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultEndpointRegistryTest.java
@@ -16,9 +16,17 @@
*/
package org.apache.camel.impl;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.ServiceStatus;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.engine.DefaultEndpointRegistry;
+import org.apache.camel.impl.engine.SimpleCamelContext;
import org.apache.camel.spi.EndpointRegistry;
+import org.apache.camel.support.NormalizedUri;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -68,4 +76,50 @@ public class DefaultEndpointRegistryTest {
assertTrue(reg.isStatic("file:error"));
}
+ //Testing the issue https://issues.apache.org/jira/browse/CAMEL-19295
+ @Test
+ public void testConcurrency() throws InterruptedException {
+
+ SimpleCamelContext context = new SimpleCamelContext();
+ context.start();
+
+ ProducerTemplate producerTemplate = context.createProducerTemplate();
+ EndpointRegistry<NormalizedUri> endpointRegistry = context.getEndpointRegistry();
+
+ int nThreads = 4;
+ ExecutorService executorService = Executors.newFixedThreadPool(nThreads);
+ int iterations = 500;
+
+ for (int j = 0; j < iterations; j++) {
+ CountDownLatch allThreadCompletionSemaphore = new CountDownLatch(nThreads);
+ for (int i = 0; i < nThreads; i++) {
+
+ executorService.submit(() -> {
+
+ producerTemplate.requestBody("controlbus:route?routeId=route1&action=ACTION_STATUS&loggingLevel=off", null,
+ ServiceStatus.class);
+ producerTemplate.requestBody("controlbus:route?routeId=route2&action=ACTION_STATUS&loggingLevel=off", null,
+ ServiceStatus.class);
+ producerTemplate.requestBody("controlbus:route?routeId=route3&action=ACTION_STATUS&loggingLevel=off", null,
+ ServiceStatus.class);
+ producerTemplate.requestBody("controlbus:route?routeId=route4&action=ACTION_STATUS&loggingLevel=off", null,
+ ServiceStatus.class);
+ producerTemplate.requestBody("controlbus:route?routeId=route5&action=ACTION_STATUS&loggingLevel=off", null,
+ ServiceStatus.class);
+
+ allThreadCompletionSemaphore.countDown();
+
+ });
+ }
+
+ allThreadCompletionSemaphore.await();
+
+ assertTrue(endpointRegistry.values().toArray() != null);
+
+ }
+
+ executorService.shutdown();
+
+ }
+
}
diff --git a/core/camel-core/src/test/java/org/apache/camel/support/DefaultLRUCacheFactoryTest.java b/core/camel-core/src/test/java/org/apache/camel/support/DefaultLRUCacheFactoryTest.java
new file mode 100644
index 00000000000..c9f7fdda9b4
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/support/DefaultLRUCacheFactoryTest.java
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.camel.support;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The test class for {@link DefaultLRUCacheFactory}.
+ */
+class DefaultLRUCacheFactoryTest {
+
+ private final List<String> consumed = new ArrayList<>();
+ private final DefaultLRUCacheFactory.SimpleLRUCache<String, String> map
+ = (DefaultLRUCacheFactory.SimpleLRUCache<String, String>) new DefaultLRUCacheFactory().<String,
+ String> createLRUCache(3, consumed::add);
+
+ @Test
+ void forbiddenOperations() {
+ assertThrows(UnsupportedOperationException.class, () -> map.values().add("foo"));
+ assertThrows(UnsupportedOperationException.class, () -> map.keySet().add("foo"));
+ assertThrows(UnsupportedOperationException.class, () -> map.entrySet().add(Map.entry("x", "y")));
+ }
+
+ @Test
+ void setValue() {
+ assertNull(map.put("1", "One"));
+ assertEquals(1, map.size());
+ assertEquals(1, map.getQueueSize());
+ map.entrySet().iterator().next().setValue("bar");
+ assertEquals(1, map.size());
+ assertEquals(2, map.getQueueSize());
+ }
+
+ @Test
+ void queueSize() {
+ assertEquals(0, map.getQueueSize());
+ map.put("1", "1");
+ assertEquals(1, map.size());
+ assertEquals(1, map.getQueueSize());
+ map.put("1", "2");
+ assertEquals(1, map.size());
+ assertEquals(2, map.getQueueSize());
+ map.put("1", "3");
+ assertEquals(1, map.size());
+ assertEquals(3, map.getQueueSize());
+ map.put("1", "4");
+ assertEquals(1, map.size());
+ assertEquals(4, map.getQueueSize());
+ map.put("1", "5");
+ assertEquals(1, map.size());
+ assertEquals(5, map.getQueueSize());
+ map.put("1", "6");
+ assertEquals(1, map.size());
+ assertEquals(6, map.getQueueSize());
+ map.put("1", "7");
+ assertEquals(1, map.size());
+ assertEquals(6, map.getQueueSize());
+ map.put("1", "8");
+ assertEquals(1, map.size());
+ assertEquals(6, map.getQueueSize());
+ }
+
+ @Test
+ void put() {
+ assertEquals(0, map.size());
+ assertNull(map.put("1", "One"));
+ assertEquals(1, map.size());
+ assertNull(map.put("2", "Two"));
+ assertEquals(2, map.size());
+ assertNull(map.put("3", "Three"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertNull(map.put("4", "Four"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertFalse(map.containsKey("1"));
+ assertTrue(consumed.contains("One"));
+ assertEquals("Two", map.put("2", "Two v2"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertTrue(map.containsKey("2"));
+ assertEquals("Two v2", map.get("2"));
+ }
+
+ @Test
+ void putIfAbsent() {
+ assertEquals(0, map.size());
+ assertNull(map.putIfAbsent("1", "One"));
+ assertEquals(1, map.size());
+ assertNull(map.putIfAbsent("2", "Two"));
+ assertEquals(2, map.size());
+ assertNull(map.putIfAbsent("3", "Three"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertNull(map.putIfAbsent("4", "Four"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertFalse(map.containsKey("1"));
+ assertTrue(consumed.contains("One"));
+ assertEquals("Two", map.putIfAbsent("2", "Two v2"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertTrue(map.containsKey("2"));
+ assertEquals("Two", map.get("2"));
+ assertNull(map.putIfAbsent("5", "Five"));
+ assertEquals(3, map.size());
+ assertEquals(2, consumed.size());
+ assertFalse(map.containsKey("2"));
+ assertTrue(consumed.contains("Two"));
+ }
+
+ @Test
+ void computeIfAbsent() {
+ assertEquals(0, map.size());
+ assertEquals("One", map.computeIfAbsent("1", k -> "One"));
+ assertEquals(1, map.size());
+ assertEquals("Two", map.computeIfAbsent("2", k -> "Two"));
+ assertEquals(2, map.size());
+ assertEquals("Three", map.computeIfAbsent("3", k -> "Three"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertEquals("Four", map.computeIfAbsent("4", k -> "Four"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertFalse(map.containsKey("1"));
+ assertTrue(consumed.contains("One"));
+ assertNull(map.computeIfAbsent("1", k -> null));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertNull(map.computeIfAbsent("5", k -> null));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertEquals("Two", map.computeIfAbsent("2", k -> "Two v2"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertTrue(map.containsKey("2"));
+ assertEquals("Two", map.get("2"));
+ assertEquals("Five", map.computeIfAbsent("5", k -> "Five"));
+ assertEquals(3, map.size());
+ assertEquals(2, consumed.size());
+ assertFalse(map.containsKey("2"));
+ assertTrue(consumed.contains("Two"));
+ assertEquals("Five", map.computeIfAbsent("5", k -> null));
+ assertEquals(3, map.size());
+ assertEquals(2, consumed.size());
+ }
+
+ @Test
+ void computeIfPresent() {
+ assertEquals(0, map.size());
+ map.putIfAbsent("1", "One");
+ assertEquals(1, map.size());
+ map.putIfAbsent("2", "Two");
+ assertEquals(2, map.size());
+ map.putIfAbsent("3", "Three");
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertNull(map.computeIfPresent("4", (k, v) -> "Four"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertFalse(map.containsKey("4"));
+ assertEquals("One v2", map.computeIfPresent("1", (k, v) -> "One v2"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertTrue(map.containsKey("1"));
+ assertEquals("One v2", map.get("1"));
+ assertNull(map.computeIfPresent("1", (k, v) -> null));
+ assertEquals(2, map.size());
+ assertEquals(0, consumed.size());
+ assertFalse(map.containsKey("1"));
+ }
+
+ @Test
+ void compute() {
+ assertEquals(0, map.size());
+ assertEquals("One", map.compute("1", (k, v) -> "One"));
+ assertEquals(1, map.size());
+ assertEquals("Two", map.compute("2", (k, v) -> "Two"));
+ assertEquals(2, map.size());
+ assertEquals("Three", map.compute("3", (k, v) -> "Three"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertEquals("Four", map.compute("4", (k, v) -> "Four"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertFalse(map.containsKey("1"));
+ assertTrue(consumed.contains("One"));
+ assertEquals("Two v2", map.compute("2", (k, v) -> "Two v2"));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertTrue(map.containsKey("2"));
+ assertEquals("Two v2", map.get("2"));
+ assertNull(map.compute("2", (k, v) -> null));
+ assertEquals(2, map.size());
+ assertEquals(1, consumed.size());
+ assertFalse(map.containsKey("2"));
+ }
+
+ @Test
+ void merge() {
+ assertEquals(0, map.size());
+ assertEquals("One", map.merge("1", "One", String::concat));
+ assertEquals(1, map.size());
+ assertEquals("Two", map.merge("2", "Two", String::concat));
+ assertEquals(2, map.size());
+ assertEquals("Three", map.merge("3", "Three", String::concat));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertEquals("Four", map.merge("4", "Four", String::concat));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertFalse(map.containsKey("1"));
+ assertTrue(consumed.contains("One"));
+ assertEquals("TwoV2", map.merge("2", "V2", String::concat));
+ assertEquals(3, map.size());
+ assertEquals(1, consumed.size());
+ assertNull(map.merge("2", "V2", (v1, v2) -> null));
+ assertEquals(2, map.size());
+ assertEquals(1, consumed.size());
+ }
+
+ @Test
+ void replace() {
+ assertEquals(0, map.size());
+ assertNull(map.replace("1", "One"));
+ assertEquals(0, map.size());
+ map.put("1", "One");
+ assertEquals(1, map.size());
+ map.put("2", "Two");
+ assertEquals(2, map.size());
+ map.put("3", "Three");
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertEquals("One", map.replace("1", "One v2"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertEquals("Three", map.replace("3", "Three v2"));
+ assertEquals("Three v2", map.get("3"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ }
+
+ @Test
+ void replaceWithOldValue() {
+ assertEquals(0, map.size());
+ map.put("1", "One");
+ map.put("2", "Two");
+ map.put("3", "Three");
+ assertEquals(3, map.size());
+ assertFalse(map.replace("1", "foo", "One"));
+ assertEquals(3, map.size());
+ assertFalse(map.replace("1", "foo", "One v2"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertEquals("One", map.get("1"));
+ assertTrue(map.replace("1", "One", "One v2"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertEquals("One v2", map.get("1"));
+ assertFalse(map.replace("3", "foo", "Three v2"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ assertTrue(map.replace("3", "Three", "Three v2"));
+ assertEquals("Three v2", map.get("3"));
+ assertEquals(3, map.size());
+ assertEquals(0, consumed.size());
+ }
+}
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/DefaultLRUCacheFactory.java b/core/camel-support/src/main/java/org/apache/camel/support/DefaultLRUCacheFactory.java
index 995a1405a2d..f52d7e60b45 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/DefaultLRUCacheFactory.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/DefaultLRUCacheFactory.java
@@ -16,10 +16,19 @@
*/
package org.apache.camel.support;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.function.BiFunction;
import java.util.function.Consumer;
+import java.util.function.Function;
import org.apache.camel.support.service.ServiceHelper;
import org.slf4j.Logger;
@@ -138,11 +147,28 @@ public class DefaultLRUCacheFactory extends LRUCacheFactory {
return new SimpleLRUCache<>(initialCapacity, maximumCacheSize, stopOnEviction);
}
- private class SimpleLRUCache<K, V> extends LinkedHashMap<K, V> {
+ class SimpleLRUCache<K, V> extends ConcurrentHashMap<K, V> {
static final float DEFAULT_LOAD_FACTOR = 0.75f;
-
+ /**
+ * The flag indicating that an eviction process is in progress.
+ */
+ private final AtomicBoolean eviction = new AtomicBoolean();
+ /**
+ * The maximum cache size.
+ */
private final int maximumCacheSize;
+ /**
+ * The last changes recorded.
+ */
+ private final Queue<Entry<K, V>> lastChanges = new ConcurrentLinkedQueue<>();
+ /**
+ * The total amount of changes recorded.
+ */
+ private final LongAdder totalChanges = new LongAdder();
+ /**
+ * The function to call when an entry is evicted.
+ */
private final Consumer<V> evict;
public SimpleLRUCache(int maximumCacheSize) {
@@ -159,21 +185,230 @@ public class DefaultLRUCacheFactory extends LRUCacheFactory {
}
public SimpleLRUCache(int initialCapacity, int maximumCacheSize, Consumer<V> evicted) {
- super(initialCapacity, DEFAULT_LOAD_FACTOR, true);
+ super(initialCapacity, DEFAULT_LOAD_FACTOR);
this.maximumCacheSize = maximumCacheSize;
this.evict = Objects.requireNonNull(evicted);
}
+ /**
+ * Adds a new change in case the mapping function doesn't return {@code null}.
+ *
+ * @param context the context of the write operation
+ * @param mappingFunction the mapping function to apply.
+ * @return the result of the mapping function.
+ */
+ private V addChange(OperationContext<K, V> context, Function<? super K, ? extends V> mappingFunction) {
+ K key = context.key;
+ V value = mappingFunction.apply(key);
+ if (value == null) {
+ return null;
+ }
+ lastChanges.add(Map.entry(key, value));
+ totalChanges.increment();
+ return value;
+ }
+
+ @Override
+ public V put(K key, V value) {
+ if (key == null || value == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ super.compute(
+ key,
+ (k, v) -> {
+ context.result = v;
+ return addChange(context, x -> value);
+ });
+ return context.result;
+ }
+ }
+
+ @Override
+ public V putIfAbsent(K key, V value) {
+ if (key == null || value == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ super.compute(
+ key,
+ (k, v) -> {
+ context.result = v;
+ if (v != null) {
+ return v;
+ }
+ return addChange(context, x -> value);
+ });
+ return context.result;
+ }
+ }
+
+ @Override
+ public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+ if (key == null || mappingFunction == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ return super.computeIfAbsent(key, k -> addChange(context, mappingFunction));
+ }
+ }
+
+ @Override
+ public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+ if (key == null || remappingFunction == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ return super.computeIfPresent(key, (k, v) -> addChange(context, x -> remappingFunction.apply(x, v)));
+ }
+ }
+
@Override
- protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
- if (size() > maximumCacheSize) {
- V value = eldest.getValue();
- evict.accept(value);
- return true;
+ public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+ if (key == null || remappingFunction == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ return super.compute(key, (k, v) -> addChange(context, x -> remappingFunction.apply(x, v)));
+ }
+ }
+
+ @Override
+ public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+ if (key == null || value == null || remappingFunction == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ return super.compute(
+ key,
+ (k, oldValue) -> {
+ V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
+ return addChange(context, x -> newValue);
+ });
}
- return false;
}
+ @Override
+ public boolean replace(K key, V oldValue, V newValue) {
+ if (key == null || oldValue == null || newValue == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ super.computeIfPresent(
+ key,
+ (k, v) -> {
+ if (Objects.equals(oldValue, v)) {
+ context.result = addChange(context, x -> newValue);
+ return context.result;
+ }
+ return v;
+ });
+ return context.result != null && Objects.equals(context.result, newValue);
+ }
+ }
+
+ @Override
+ public V replace(K key, V value) {
+ if (key == null || value == null) {
+ throw new NullPointerException();
+ }
+ try (OperationContext<K, V> context = new OperationContext<>(this, key)) {
+ super.computeIfPresent(
+ key,
+ (k, v) -> {
+ context.result = v;
+ return addChange(context, x -> value);
+ });
+ return context.result;
+ }
+ }
+
+ @Override
+ public void putAll(Map<? extends K, ? extends V> m) {
+ for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
+ put(e.getKey(), e.getValue());
+ }
+ }
+
+ @Override
+ public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
+ for (Map.Entry<? extends K, ? extends V> e : entrySet()) {
+ replace(e.getKey(), e.getValue(), function.apply(e.getKey(), e.getValue()));
+ }
+ }
+
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ return Collections.unmodifiableSet(super.entrySet());
+ }
+
+ /**
+ * @return the size of the queue of changes.
+ */
+ int getQueueSize() {
+ return totalChanges.intValue();
+ }
+
+ /**
+ * Indicates whether an eviction is needed. An eviction can be triggered if the size of the map or the queue of
+ * changes exceeds the maximum allowed size which is respectively {@code maximumCacheSize} and
+ * {@code 2 * maximumCacheSize}.
+ *
+ * @return {@code true} if an eviction is needed, {@code false} otherwise.
+ */
+ private boolean evictionNeeded() {
+ return size() > maximumCacheSize || getQueueSize() > 2 * maximumCacheSize;
+ }
+
+ /**
+ * @return the oldest existing change.
+ */
+ private Entry<K, V> nextOldestChange() {
+ Entry<K, V> oldest = lastChanges.poll();
+ if (oldest != null) {
+ totalChanges.decrement();
+ }
+ return oldest;
+ }
+
+ /**
+ * The internal context of all write operations.
+ */
+ private static class OperationContext<K, V> implements AutoCloseable {
+ /**
+ * The result of the corresponding operation when applicable.
+ */
+ V result;
+ /**
+ * The key against which the operation is made.
+ */
+ final K key;
+ /**
+ * The underlying cache.
+ */
+ private final SimpleLRUCache<K, V> cache;
+
+ OperationContext(SimpleLRUCache<K, V> cache, K key) {
+ this.cache = cache;
+ this.key = key;
+ }
+
+ @Override
+ public void close() {
+ if (cache.evictionNeeded() && cache.eviction.compareAndSet(false, true)) {
+ try {
+ while (cache.evictionNeeded()) {
+ Entry<K, V> oldest = cache.nextOldestChange();
+ if (oldest != null && cache.remove(oldest.getKey(), oldest.getValue())) {
+ cache.evict.accept(oldest.getValue());
+ }
+ }
+ } finally {
+ cache.eviction.set(false);
+ }
+ }
+ }
+ }
}
<V> void doNothing(V value) {