This is an automated email from the ASF dual-hosted git repository. vy pushed a commit to branch scheduled-for-deletion/LOG4J2-930 in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 609e3005d249863d67668ca7e6f76409da1d0214 Author: rpopma <[email protected]> AuthorDate: Wed Dec 31 23:21:01 2014 +0900 Initial version --- .../log4j/core/util/ConcurrentLRUCache.java | 426 +++++++++++++++++++++ .../log4j/core/util/ConcurrentLRUCacheTest.java | 129 +++++++ 2 files changed, 555 insertions(+) diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ConcurrentLRUCache.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ConcurrentLRUCache.java new file mode 100644 index 0000000000..ec272c2e4f --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ConcurrentLRUCache.java @@ -0,0 +1,426 @@ +/* + * 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.logging.log4j.core.util; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Thread-safe implementation of a Least Recently Used cache. + * <p> + * The map part is supplied by {@code java.util.concurrent.ConcurrentHashMap}, the tracking of the least recently used + * element is done with code based on Doug Lea's <a + * href="http://www.java2s.com/Code/Java/Collections-Data-Structure/ConcurrentDoublyLinkedList.htm" + * >ConcurrentDoublyLinkedList</a>. + */ +public class ConcurrentLRUCache<K, V> { + + private final Map<K, ConcurrentLRUCache.Node<K, V>> table = new ConcurrentHashMap<K, ConcurrentLRUCache.Node<K, V>>(); + private final Node<K, V> header; + private final Node<K, V> trailer; // the least recently used element + private final int capacity; + + /** + * Constructs an empty cache. + * + * @param capacity the maximum number of key-value pairs this cache is allowed to contain + */ + public ConcurrentLRUCache(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Capacity must be a positive number but was " + capacity); + } + Node<K, V> h = new Node<K, V>(null, null, null, null); + Node<K, V> t = new Node<K, V>(null, null, null, h); + h.setNext(t); + this.header = h; + this.trailer = t; + this.capacity = capacity; + } + + public V get(final K key) { + Node<K, V> node = table.get(key); + if (node != null) { + if (node.delete()) { + // make this the new header + while (header.append(key, node.element) == null) { + // loop until success + } + } // delete failed: some other thread already re-inserted this at the head + return node.element; + } else { + return null; + } + } + + public void set(final K key, final V value) { + Node<K, V> cur = table.get(key); + if (cur != null) { + cur.delete(); // move the most recently used node to the front + while (header.append(key, value) == null) { + // loop until success + } + } else { + // most recently used node to the front + while ((cur = header.append(key, value)) == null) { + // loop until success + } + table.put(key, cur); + while (table.size() > capacity) { + final Node<K, V> leastRecentlyUsed = trailer.back(); + if (leastRecentlyUsed != null && leastRecentlyUsed.delete()) { + // At most one thread succeeds in deleting this LRU node. + // That thread then removes its key from the table. + // Worst case, multiple threads all remove LRU nodes from the table, + // resulting in table.size being (capacity - threadCount). + // This is not a problem. + table.remove(leastRecentlyUsed.key); + } + } + } + } + + /** + * Returns the maximum number of key-value mappings in this cache. + * + * @return the maximum number of key-value mappings in this cache + */ + public int capacity() { + return this.capacity; + } + + /** + * Returns the number of key-value mappings in this cache. + * + * @return the number of key-value mappings in this cache. + */ + public int size() { + return table.size(); + } + + /** + * Linked Nodes. As a minor efficiency hack, this class opportunistically inherits from AtomicReference, with the + * atomic ref used as the "next" link. + * <p> + * Nodes are in doubly-linked lists. There are three kinds of special nodes, distinguished by: + * <ul> + * <li>The list header has a null prev link + * <li>The list trailer has a null next link + * <li>A deletion marker has a prev link pointing to itself. + * </ul> + * All three kinds of special nodes have null element fields. + * <p> + * Regular nodes have non-null element, next, and prev fields. To avoid visible inconsistencies when deletions + * overlap element replacement, replacements are done by replacing the node, not just setting the element. + * <p> + * Nodes can be traversed by read-only ConcurrentLinkedDeque class operations just by following raw next pointers, + * so long as they ignore any special nodes seen along the way. (This is automated in method forward.) However, + * traversal using prev pointers is not guaranteed to see all live nodes since a prev pointer of a deleted node can + * become unrecoverably stale. + */ + static class Node<KEY, VAL> extends AtomicReference<Node<KEY, VAL>> { + private static final long serialVersionUID = -1798997222261941417L; + + private volatile Node<KEY, VAL> prev; + + final KEY key; + final VAL element; + + /** Creates a node with given contents */ + Node(KEY key, VAL element, Node<KEY, VAL> next, Node<KEY, VAL> prev) { + super(next); + this.prev = prev; + this.key = key; + this.element = element; + } + + /** Creates a marker node with given successor */ + Node(Node<KEY, VAL> next) { + super(next); + this.prev = this; + this.key = null; + this.element = null; + } + + /** + * Gets next link (which is actually the value held as atomic reference). + */ + private Node<KEY, VAL> getNext() { + return get(); + } + + /** + * Sets next link + * + * @param n the next node + */ + void setNext(Node<KEY, VAL> n) { + set(n); + } + + /** + * compareAndSet next link + */ + private boolean casNext(Node<KEY, VAL> cmp, Node<KEY, VAL> val) { + return compareAndSet(cmp, val); + } + + /** + * Gets prev link + */ + private Node<KEY, VAL> getPrev() { + return prev; + } + + /** + * Sets prev link + * + * @param b the previous node + */ + void setPrev(Node<KEY, VAL> b) { + prev = b; + } + + /** + * Returns true if this is a header, trailer, or marker node + */ + boolean isSpecial() { + return element == null; + } + + /** + * Returns true if this is a trailer node + */ + boolean isTrailer() { + return getNext() == null; + } + + /** + * Returns true if this is a header node + */ + boolean isHeader() { + return getPrev() == null; + } + + /** + * Returns true if this is a marker node + */ + boolean isMarker() { + return getPrev() == this; + } + + /** + * Returns true if this node is followed by a marker, meaning that it is deleted. + * + * @return true if this node is deleted + */ + boolean isDeleted() { + Node<KEY, VAL> f = getNext(); + return f != null && f.isMarker(); + } + + /** + * Returns next node, ignoring deletion marker + */ + private Node<KEY, VAL> nextNonmarker() { + Node<KEY, VAL> f = getNext(); + return (f == null || !f.isMarker()) ? f : f.getNext(); + } + + /** + * Returns the next non-deleted node, swinging next pointer around any encountered deleted nodes, and also + * patching up successor''s prev link to point back to this. Returns null if this node is trailer so has no + * successor. + * + * @return successor, or null if no such + */ + Node<KEY, VAL> successor() { + Node<KEY, VAL> f = nextNonmarker(); + for (;;) { + if (f == null) + return null; + if (!f.isDeleted()) { + if (f.getPrev() != this && !isDeleted()) + f.setPrev(this); // relink f's prev + return f; + } + Node<KEY, VAL> s = f.nextNonmarker(); + if (f == getNext()) + casNext(f, s); // unlink f + f = s; + } + } + + /** + * Returns the apparent predecessor of target by searching forward for it starting at this node, patching up + * pointers while traversing. Used by predecessor(). + * + * @return target's predecessor, or null if not found + */ + private Node<KEY, VAL> findPredecessorOf(Node<KEY, VAL> target) { + Node<KEY, VAL> n = this; + for (;;) { + Node<KEY, VAL> f = n.successor(); + if (f == target) + return n; + if (f == null) + return null; + n = f; + } + } + + /** + * Returns the previous non-deleted node, patching up pointers as needed. Returns null if this node is header so + * has no successor. May also return null if this node is deleted, so doesn't have a distinct predecessor. + * + * @return predecessor or null if not found + */ + Node<KEY, VAL> predecessor() { + Node<KEY, VAL> n = this; + for (;;) { + Node<KEY, VAL> b = n.getPrev(); + if (b == null) + return n.findPredecessorOf(this); + Node<KEY, VAL> s = b.getNext(); + if (s == this) + return b; + if (s == null || !s.isMarker()) { + Node<KEY, VAL> p = b.findPredecessorOf(this); + if (p != null) + return p; + } + n = b; + } + } + + /** + * Returns the next node containing a nondeleted user element. Use for forward list traversal. + * + * @return successor, or null if no such + */ + Node<KEY, VAL> forward() { + Node<KEY, VAL> f = successor(); + return (f == null || f.isSpecial()) ? null : f; + } + + /** + * Returns previous node containing a nondeleted user element, if possible. Use for backward list traversal, but + * beware that if this method is called from a deleted node, it might not be able to determine a usable + * predecessor. + * + * @return predecessor, or null if no such could be found + */ + Node<KEY, VAL> back() { + Node<KEY, VAL> f = predecessor(); + return (f == null || f.isSpecial()) ? null : f; + } + + /** + * Tries to insert a node holding element as successor, failing if this node is deleted. + * + * @param element the element + * @return the new node, or null on failure. + */ + Node<KEY, VAL> append(KEY key, VAL element) { + for (;;) { + Node<KEY, VAL> f = getNext(); + if (f == null || f.isMarker()) + return null; + Node<KEY, VAL> x = new Node<KEY, VAL>(key, element, f, this); + if (casNext(f, x)) { + f.setPrev(x); // optimistically link + return x; + } + } + } + + /** + * Tries to insert a node holding element as predecessor, failing if no live predecessor can be found to link + * to. + * + * @param element the element + * @return the new node, or null on failure. + */ + Node<KEY, VAL> prepend(KEY key, VAL element) { + for (;;) { + Node<KEY, VAL> b = predecessor(); + if (b == null) + return null; + Node<KEY, VAL> x = new Node<KEY, VAL>(key, element, this, b); + if (b.casNext(this, x)) { + setPrev(x); // optimistically link + return x; + } + } + } + + /** + * Tries to mark this node as deleted, failing if already deleted or if this node is header or trailer + * + * @return true if successful + */ + boolean delete() { + Node<KEY, VAL> b = getPrev(); + Node<KEY, VAL> f = getNext(); + if (b != null && f != null && !f.isMarker() && casNext(f, new Node<KEY, VAL>(f))) { + if (b.casNext(this, f)) + f.setPrev(b); + return true; + } + return false; + } + + /** + * Tries to insert a node holding element to replace this node. Failing if already deleted. + * + * @param newElement the new element + * @return the new node, or null on failure. + */ + Node<KEY, VAL> replace(KEY key, VAL newElement) { + for (;;) { + Node<KEY, VAL> b = getPrev(); + Node<KEY, VAL> f = getNext(); + if (b == null || f == null || f.isMarker()) + return null; + Node<KEY, VAL> x = new Node<KEY, VAL>(key, newElement, f, b); + if (casNext(f, new Node<KEY, VAL>(x))) { + b.successor(); // to relink b + x.successor(); // to relink f + return x; + } + } + } + + @Override + public String toString() { + if (isHeader()) { + return "header"; + } + if (isTrailer()) { + return "trailer"; + } + if (isMarker()) { + return key + "=" + element + " (marker)"; + } + if (isDeleted()) { + return key + "=" + element + " (deleted)"; + } + return key + "=" + element; + } + } +} \ No newline at end of file diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ConcurrentLRUCacheTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ConcurrentLRUCacheTest.java new file mode 100644 index 0000000000..f0cf24a9a0 --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ConcurrentLRUCacheTest.java @@ -0,0 +1,129 @@ +package org.apache.logging.log4j.core.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ConcurrentLRUCacheTest { + + @SuppressWarnings("rawtypes") + @Test(expected=IllegalArgumentException.class) + public void testConstructorDisallowsZeroCapacity() { + new ConcurrentLRUCache(0); + } + + @SuppressWarnings("rawtypes") + @Test(expected=IllegalArgumentException.class) + public void testConstructorDisallowsNegativeCapacity() { + new ConcurrentLRUCache(-1); + } + + @Test + public void testGetReturnsNullForUnknownKey() { + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(44); + assertNull("cache is empty", cache.get("a")); // cache is empty + assertNull("cache is empty", cache.get("b")); // cache is empty + } + + @Test + public void testGetReturnsValueForKnownKey() { + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(44); + assertNull("cache initially empty", cache.get("a")); // cache is initially empty + + cache.set("a", "aa"); + assertEquals("aa", cache.get("a")); // value now in cache + } + + @Test + public void testGetReturnsNullForLeastRecentlyUsedKeyWhenCapacityExceeded() { + final int INITIAL_CAPACITY = 3; + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(INITIAL_CAPACITY); + cache.set("a", "aa"); + assertEquals("aa", cache.get("a")); // "a" most recently used + + cache.set("b", "bb"); // "a" is now 2nd recently used + cache.set("c", "cc"); //"a" 3rd recently used + cache.set("d", "dd"); // "a" least recently used & size exceeded capacity... + assertNull("value for key a", cache.get("a")); // evicted from cache + + cache.set("e", "ee"); + assertNull("value for key b", cache.get("b")); // evicted from cache + } + + @Test + public void testSetAddsKeyValuePairToCache() { + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(3); + assertEquals(0, cache.size()); + + cache.set("a", "b"); + } + + @Test + public void testSizeInitiallyReturnsXero() { + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(3); + assertEquals(0, cache.size()); + } + + @Test + public void testSizeReturnsNumberOfElements() { + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(5); + cache.set("a", "ab"); + assertEquals(1, cache.size()); + + cache.set("b", "ab"); + assertEquals(2, cache.size()); + + cache.set("c", "ab"); + assertEquals(3, cache.size()); + } + + @Test + public void testSizeReturnsCapacityIfCapacityExceeded() { + final int INITIAL_CAPACITY = 3; + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(INITIAL_CAPACITY); + assertEquals(0, cache.size()); + cache.set("a", "ab"); + assertEquals(1, cache.size()); + cache.set("b", "ab"); + assertEquals(2, cache.size()); + cache.set("c", "ab"); + assertEquals(3, cache.size()); + cache.set("d", "ab"); + assertEquals(3, cache.size()); // does not increase + cache.set("e", "ab"); + assertEquals(3, cache.size()); // does not increase + } + + @Test + public void testCapacityReturnsConstructorValue() { + final int INITIAL_CAPACITY = 15; + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(INITIAL_CAPACITY); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + cache.set("a", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + + cache.set("b", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + + cache.set("c", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + } + + @Test + public void testCapacityReturnsInitialValueRegardlessOfSize() { + final int INITIAL_CAPACITY = 3; + ConcurrentLRUCache<String, String> cache = new ConcurrentLRUCache<String, String>(INITIAL_CAPACITY); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + cache.set("a", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + cache.set("b", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + cache.set("c", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + cache.set("d", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + cache.set("e", "ab"); + assertEquals(INITIAL_CAPACITY, cache.capacity()); + } + +}
