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 @SuppressWarnings("NonAtomicVolatileUpdate")
163 public V get(Object key) {
164 Entry<K, V> entry = map.get(key);
165 if (entry != null) {
166 entry.lastAccessed = ++time;
167 return entry.value;
168 }
169 return null;
170 }
171
172 /**
173 * Maps the specified key to the specified value in this cache. Neither the
174 * key nor the value can be null.
175 *
176 * <p>
177 * The value can be retrieved by calling the {@code get} method with a key
178 * that is equal to the original key.
179 *
180 * @param key
181 * key with which the specified value is to be associated
182 * @param value
183 * value to be associated with the specified key
184 * @return the previous value associated with {@code key}, or {@code null}
185 * if there was no mapping for {@code key}
186 * @throws NullPointerException
187 * if the specified key or value is null
188 */
189 @SuppressWarnings("NonAtomicVolatileUpdate")
190 public V put(@NonNull K key, @NonNull V value) {
191 map.put(key, new Entry<>(key, value, ++time));
192 if (map.size() > maximumSize) {
193 purge();
194 }
195 return value;
196 }
197
198 /**
199 * Returns the current size of this cache
200 *
201 * @return the number of key-value mappings in this cache
202 */
203 public int size() {
204 return map.size();
205 }
206
207 /**
208 * Reconfigures the cache. If {@code maxSize} is reduced some entries will
209 * be purged.
210 *
211 * @param maxSize
212 * maximum size of the cache
213 *
214 * @param purgeFactor
215 * when the size of the map reaches maxSize the oldest entries
216 * will be purged to free up some space for new entries,
217 * {@code purgeFactor} is the fraction of {@code maxSize} to
218 * purge when this happens
219 */
220 public void configure(int maxSize, float purgeFactor) {
221 lock.lock();
222 try {
223 checkPurgeFactor(purgeFactor);
224 this.maximumSize = maxSize;
225 this.purgeSize = purgeSize(maxSize, purgeFactor);
226 if (map.size() >= maximumSize) {
227 purge();
228 }
229 } finally {
230 lock.unlock();
231 }
232 }
233
234 private void purge() {
235 // don't try to compete if another thread already has the lock
236 if (lock.tryLock()) {
237 try {
238 List<Entry> entriesToPurge = new ArrayList<>(map.values());
239 // copy access times to avoid other threads interfere with
240 // sorting
241 for (Entry e : entriesToPurge) {
242 e.copyAccessTime();
243 }
244 Collections.sort(entriesToPurge,
245 Comparator.comparingLong(o -> -o.lastAccessedSorting));
246 for (int index = purgeSize; index < entriesToPurge
247 .size(); index++) {
248 map.remove(entriesToPurge.get(index).key);
249 }
250 } finally {
251 lock.unlock();
252 }
253 }
254 }
255 }