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 }