1 /*
2 * Copyright (C) 2019, Marc Strapetz <marc.strapetz@syntevo.com>
3 * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> and others
4 *
5 * This program and the accompanying materials are made available under the
6 * terms of the Eclipse Distribution License v. 1.0 which is available at
7 * https://www.eclipse.org/org/documents/edl-v10.php.
8 *
9 * SPDX-License-Identifier: BSD-3-Clause
10 */
11 package org.eclipse.jgit.util;
12
13 import java.text.MessageFormat;
14 import java.util.ArrayList;
15 import java.util.Collections;
16 import java.util.Comparator;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.locks.Lock;
21 import java.util.concurrent.locks.ReentrantLock;
22
23 import org.eclipse.jgit.annotations.NonNull;
24 import org.eclipse.jgit.internal.JGitText;
25
26 /**
27 * Simple limited size cache based on ConcurrentHashMap purging entries in LRU
28 * order when reaching size limit
29 *
30 * @param <K>
31 * the type of keys maintained by this cache
32 * @param <V>
33 * the type of mapped values
34 *
35 * @since 5.1.9
36 */
37 public class SimpleLruCache<K, V> {
38
39 private static class Entry<K, V> {
40
41 private final K key;
42
43 private final V value;
44
45 // pseudo clock timestamp of the last access to this entry
46 private volatile long lastAccessed;
47
48 private long lastAccessedSorting;
49
50 Entry(K key, V value, long lastAccessed) {
51 this.key = key;
52 this.value = value;
53 this.lastAccessed = lastAccessed;
54 }
55
56 void copyAccessTime() {
57 lastAccessedSorting = lastAccessed;
58 }
59
60 @SuppressWarnings("nls")
61 @Override
62 public String toString() {
63 return "Entry [lastAccessed=" + lastAccessed + ", key=" + key
64 + ", value=" + value + "]";
65 }
66 }
67
68 private Lock lock = new ReentrantLock();
69
70 private Map<K, Entry<K,V>> map = new ConcurrentHashMap<>();
71
72 private volatile int maximumSize;
73
74 private int purgeSize;
75
76 // pseudo clock to implement LRU order of access to entries
77 private volatile long time = 0L;
78
79 private static void checkPurgeFactor(float purgeFactor) {
80 if (purgeFactor <= 0 || purgeFactor >= 1) {
81 throw new IllegalArgumentException(
82 MessageFormat.format(JGitText.get().invalidPurgeFactor,
83 Float.valueOf(purgeFactor)));
84 }
85 }
86
87 private static int purgeSize(int maxSize, float purgeFactor) {
88 return (int) ((1 - purgeFactor) * maxSize);
89 }
90
91 /**
92 * Create a new cache
93 *
94 * @param maxSize
95 * maximum size of the cache, to reduce need for synchronization
96 * this is not a hard limit. The real size of the cache could be
97 * slightly above this maximum if multiple threads put new values
98 * concurrently
99 * @param purgeFactor
100 * when the size of the map reaches maxSize the oldest entries
101 * will be purged to free up some space for new entries,
102 * {@code purgeFactor} is the fraction of {@code maxSize} to
103 * purge when this happens
104 */
105 public SimpleLruCache(int maxSize, float purgeFactor) {
106 checkPurgeFactor(purgeFactor);
107 this.maximumSize = maxSize;
108 this.purgeSize = purgeSize(maxSize, purgeFactor);
109 }
110
111 /**
112 * Returns the value to which the specified key is mapped, or {@code null}
113 * if this map contains no mapping for the key.
114 *
115 * <p>
116 * More formally, if this cache contains a mapping from a key {@code k} to a
117 * value {@code v} such that {@code key.equals(k)}, then this method returns
118 * {@code v}; otherwise it returns {@code null}. (There can be at most one
119 * such mapping.)
120 *
121 * @param key
122 * the key
123 *
124 * @throws NullPointerException
125 * if the specified key is null
126 *
127 * @return value mapped for this key, or {@code null} if no value is mapped
128 */
129 @SuppressWarnings("NonAtomicVolatileUpdate")
130 public V get(Object key) {
131 Entry<K, V> entry = map.get(key);
132 if (entry != null) {
133 entry.lastAccessed = tick();
134 return entry.value;
135 }
136 return null;
137 }
138
139 /**
140 * Maps the specified key to the specified value in this cache. Neither the
141 * key nor the value can be null.
142 *
143 * <p>
144 * The value can be retrieved by calling the {@code get} method with a key
145 * that is equal to the original key.
146 *
147 * @param key
148 * key with which the specified value is to be associated
149 * @param value
150 * value to be associated with the specified key
151 * @return the previous value associated with {@code key}, or {@code null}
152 * if there was no mapping for {@code key}
153 * @throws NullPointerException
154 * if the specified key or value is null
155 */
156 @SuppressWarnings("NonAtomicVolatileUpdate")
157 public V put(@NonNull K key, @NonNull V value) {
158 map.put(key, new Entry<>(key, value, tick()));
159 if (map.size() > maximumSize) {
160 purge();
161 }
162 return value;
163 }
164
165 @SuppressWarnings("NonAtomicVolatileUpdate")
166 private long tick() {
167 return ++time;
168 }
169
170 /**
171 * Returns the current size of this cache
172 *
173 * @return the number of key-value mappings in this cache
174 */
175 public int size() {
176 return map.size();
177 }
178
179 /**
180 * Reconfigures the cache. If {@code maxSize} is reduced some entries will
181 * be purged.
182 *
183 * @param maxSize
184 * maximum size of the cache
185 *
186 * @param purgeFactor
187 * when the size of the map reaches maxSize the oldest entries
188 * will be purged to free up some space for new entries,
189 * {@code purgeFactor} is the fraction of {@code maxSize} to
190 * purge when this happens
191 */
192 public void configure(int maxSize, float purgeFactor) {
193 lock.lock();
194 try {
195 checkPurgeFactor(purgeFactor);
196 this.maximumSize = maxSize;
197 this.purgeSize = purgeSize(maxSize, purgeFactor);
198 if (map.size() >= maximumSize) {
199 purge();
200 }
201 } finally {
202 lock.unlock();
203 }
204 }
205
206 private void purge() {
207 // don't try to compete if another thread already has the lock
208 if (lock.tryLock()) {
209 try {
210 List<Entry> entriesToPurge = new ArrayList<>(map.values());
211 // copy access times to avoid other threads interfere with
212 // sorting
213 for (Entry e : entriesToPurge) {
214 e.copyAccessTime();
215 }
216 Collections.sort(entriesToPurge,
217 Comparator.comparingLong(o -> -o.lastAccessedSorting));
218 for (int index = purgeSize; index < entriesToPurge
219 .size(); index++) {
220 map.remove(entriesToPurge.get(index).key);
221 }
222 } finally {
223 lock.unlock();
224 }
225 }
226 }
227 }