View Javadoc
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 }