1 /* 2 * Copyright (C) 2019, Marc Strapetz <marc.strapetz@syntevo.com> 3 * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> 4 * and other copyright owners as documented in the project's IP log. 5 * 6 * This program and the accompanying materials are made available 7 * under the terms of the Eclipse Distribution License v1.0 which 8 * accompanies this distribution, is reproduced below, and is 9 * available at http://www.eclipse.org/org/documents/edl-v10.php 10 * 11 * All rights reserved. 12 * 13 * Redistribution and use in source and binary forms, with or 14 * without modification, are permitted provided that the following 15 * conditions are met: 16 * 17 * - Redistributions of source code must retain the above copyright 18 * notice, this list of conditions and the following disclaimer. 19 * 20 * - Redistributions in binary form must reproduce the above 21 * copyright notice, this list of conditions and the following 22 * disclaimer in the documentation and/or other materials provided 23 * with the distribution. 24 * 25 * - Neither the name of the Eclipse Foundation, Inc. nor the 26 * names of its contributors may be used to endorse or promote 27 * products derived from this software without specific prior 28 * written permission. 29 * 30 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 31 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 32 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 33 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 34 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 35 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 36 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 37 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 38 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 39 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 40 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 41 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 42 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 */ 44 package org.eclipse.jgit.util; 45 46 import java.text.MessageFormat; 47 import java.util.ArrayList; 48 import java.util.Collections; 49 import java.util.Comparator; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.concurrent.ConcurrentHashMap; 53 import java.util.concurrent.locks.Lock; 54 import java.util.concurrent.locks.ReentrantLock; 55 56 import org.eclipse.jgit.annotations.NonNull; 57 import org.eclipse.jgit.internal.JGitText; 58 59 /** 60 * Simple limited size cache based on ConcurrentHashMap purging entries in LRU 61 * order when reaching size limit 62 * 63 * @param <K> 64 * the type of keys maintained by this cache 65 * @param <V> 66 * the type of mapped values 67 * 68 * @since 5.1.9 69 */ 70 public class SimpleLruCache<K, V> { 71 72 private static class Entry<K, V> { 73 74 private final K key; 75 76 private final V value; 77 78 // pseudo clock timestamp of the last access to this entry 79 private volatile long lastAccessed; 80 81 private long lastAccessedSorting; 82 83 Entry(K key, V value, long lastAccessed) { 84 this.key = key; 85 this.value = value; 86 this.lastAccessed = lastAccessed; 87 } 88 89 void copyAccessTime() { 90 lastAccessedSorting = lastAccessed; 91 } 92 93 @SuppressWarnings("nls") 94 @Override 95 public String toString() { 96 return "Entry [lastAccessed=" + lastAccessed + ", key=" + key 97 + ", value=" + value + "]"; 98 } 99 } 100 101 private Lock lock = new ReentrantLock(); 102 103 private Map<K, Entry<K,V>> map = new ConcurrentHashMap<>(); 104 105 private volatile int maximumSize; 106 107 private int purgeSize; 108 109 // pseudo clock to implement LRU order of access to entries 110 private volatile long time = 0L; 111 112 private static void checkPurgeFactor(float purgeFactor) { 113 if (purgeFactor <= 0 || purgeFactor >= 1) { 114 throw new IllegalArgumentException( 115 MessageFormat.format(JGitText.get().invalidPurgeFactor, 116 Float.valueOf(purgeFactor))); 117 } 118 } 119 120 private static int purgeSize(int maxSize, float purgeFactor) { 121 return (int) ((1 - purgeFactor) * maxSize); 122 } 123 124 /** 125 * Create a new cache 126 * 127 * @param maxSize 128 * maximum size of the cache, to reduce need for synchronization 129 * this is not a hard limit. The real size of the cache could be 130 * slightly above this maximum if multiple threads put new values 131 * concurrently 132 * @param purgeFactor 133 * when the size of the map reaches maxSize the oldest entries 134 * will be purged to free up some space for new entries, 135 * {@code purgeFactor} is the fraction of {@code maxSize} to 136 * purge when this happens 137 */ 138 public SimpleLruCache(int maxSize, float purgeFactor) { 139 checkPurgeFactor(purgeFactor); 140 this.maximumSize = maxSize; 141 this.purgeSize = purgeSize(maxSize, purgeFactor); 142 } 143 144 /** 145 * Returns the value to which the specified key is mapped, or {@code null} 146 * if this map contains no mapping for the key. 147 * 148 * <p> 149 * More formally, if this cache contains a mapping from a key {@code k} to a 150 * value {@code v} such that {@code key.equals(k)}, then this method returns 151 * {@code v}; otherwise it returns {@code null}. (There can be at most one 152 * such mapping.) 153 * 154 * @param key 155 * the key 156 * 157 * @throws NullPointerException 158 * if the specified key is null 159 * 160 * @return value mapped for this key, or {@code null} if no value is mapped 161 */ 162 public V get(Object key) { 163 Entry<K, V> entry = map.get(key); 164 if (entry != null) { 165 entry.lastAccessed = ++time; 166 return entry.value; 167 } 168 return null; 169 } 170 171 /** 172 * Maps the specified key to the specified value in this cache. Neither the 173 * key nor the value can be null. 174 * 175 * <p> 176 * The value can be retrieved by calling the {@code get} method with a key 177 * that is equal to the original key. 178 * 179 * @param key 180 * key with which the specified value is to be associated 181 * @param value 182 * value to be associated with the specified key 183 * @return the previous value associated with {@code key}, or {@code null} 184 * if there was no mapping for {@code key} 185 * @throws NullPointerException 186 * if the specified key or value is null 187 */ 188 public V put(@NonNull K key, @NonNull V value) { 189 map.put(key, new Entry<>(key, value, ++time)); 190 if (map.size() > maximumSize) { 191 purge(); 192 } 193 return value; 194 } 195 196 /** 197 * Returns the current size of this cache 198 * 199 * @return the number of key-value mappings in this cache 200 */ 201 public int size() { 202 return map.size(); 203 } 204 205 /** 206 * Reconfigures the cache. If {@code maxSize} is reduced some entries will 207 * be purged. 208 * 209 * @param maxSize 210 * maximum size of the cache 211 * 212 * @param purgeFactor 213 * when the size of the map reaches maxSize the oldest entries 214 * will be purged to free up some space for new entries, 215 * {@code purgeFactor} is the fraction of {@code maxSize} to 216 * purge when this happens 217 */ 218 public void configure(int maxSize, float purgeFactor) { 219 lock.lock(); 220 try { 221 checkPurgeFactor(purgeFactor); 222 this.maximumSize = maxSize; 223 this.purgeSize = purgeSize(maxSize, purgeFactor); 224 if (map.size() >= maximumSize) { 225 purge(); 226 } 227 } finally { 228 lock.unlock(); 229 } 230 } 231 232 private void purge() { 233 // don't try to compete if another thread already has the lock 234 if (lock.tryLock()) { 235 try { 236 List<Entry> entriesToPurge = new ArrayList<>(map.values()); 237 // copy access times to avoid other threads interfere with 238 // sorting 239 for (Entry e : entriesToPurge) { 240 e.copyAccessTime(); 241 } 242 Collections.sort(entriesToPurge, 243 Comparator.comparingLong(o -> -o.lastAccessedSorting)); 244 for (int index = purgeSize; index < entriesToPurge 245 .size(); index++) { 246 map.remove(entriesToPurge.get(index).key); 247 } 248 } finally { 249 lock.unlock(); 250 } 251 } 252 } 253 }