/hudson-core/src/main/java/hudson/util/KeyedDataStorage.java

http://github.com/hudson/hudson · Java · 257 lines · 100 code · 21 blank · 136 comment · 10 complexity · d93e4d3c6d653e455619c0334b07d1f6 MD5 · raw file

  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. package hudson.util;
  25. import hudson.model.Fingerprint;
  26. import hudson.model.FingerprintMap;
  27. import java.io.IOException;
  28. import java.lang.ref.SoftReference;
  29. import java.text.MessageFormat;
  30. import java.util.concurrent.ConcurrentHashMap;
  31. import java.util.concurrent.atomic.AtomicInteger;
  32. /**
  33. * Convenient base class for implementing data storage.
  34. *
  35. * <p>
  36. * One typical pattern of data storage in Hudson is the one that {@link Fingerprint}
  37. * uses, where each data is keyed by an unique key (MD5 sum), and that key is used
  38. * to determine the file system location of the data.
  39. *
  40. * On memory, each data is represented by one object ({@link Fingerprint}), and
  41. * write access to the same data is coordinated by using synchronization.
  42. *
  43. * <p>
  44. * With such storage, care has to be taken to ensure that there's only one data
  45. * object in memory for any given key. That means load and create operation
  46. * needs to be synchronized. This class implements this logic in a fairly efficient
  47. * way, and thus intends to help plugins that want to use such data storage.
  48. *
  49. * @since 1.87
  50. * @author Kohsuke Kawaguchi
  51. * @see FingerprintMap
  52. */
  53. public abstract class KeyedDataStorage<T,P> {
  54. /**
  55. * The value is either {@code SoftReference<Fingerprint>} or {@link Loading}.
  56. *
  57. * If it's {@link SoftReference}, that represents the currently available value.
  58. * If it's {@link Loading}, then that indicates the fingerprint is being loaded.
  59. * The thread can wait on this object to be notified when the loading completes.
  60. */
  61. private final ConcurrentHashMap<String,Object> core = new ConcurrentHashMap<String,Object>();
  62. /**
  63. * Used in {@link KeyedDataStorage#core} to indicate that the loading of a fingerprint
  64. * is in progress, so that we can avoid creating two {@link Fingerprint}s for the same hash code,
  65. * but do so without having a single lock.
  66. */
  67. private static class Loading<T> {
  68. private T value;
  69. private boolean set;
  70. public synchronized void set(T value) {
  71. this.set = true;
  72. this.value = value;
  73. notifyAll();
  74. }
  75. /**
  76. * Blocks until the value is {@link #set(Object)} by another thread
  77. * and returns the value.
  78. */
  79. public synchronized T get() {
  80. try {
  81. while(!set)
  82. wait();
  83. return value;
  84. } catch (InterruptedException e) {
  85. // assume the loading failed, but make sure we process interruption properly later
  86. Thread.currentThread().interrupt();
  87. return null;
  88. }
  89. }
  90. }
  91. /**
  92. * Atomically gets the existing data object if any, or if it doesn't exist
  93. * {@link #create(String,Object) create} it and return it.
  94. *
  95. * @return
  96. * Never null.
  97. * @param createParams
  98. * Additional parameters needed to create a new data object. Can be null.
  99. */
  100. public T getOrCreate(String key, P createParams) throws IOException {
  101. return get(key,true,createParams);
  102. }
  103. /**
  104. * Finds the data object that matches the given key if available, or null
  105. * if not found.
  106. */
  107. public T get(String key) throws IOException {
  108. return get(key,false,null);
  109. }
  110. /**
  111. * Implementation of get/getOrCreate.
  112. */
  113. protected T get(String key, boolean createIfNotExist, P createParams) throws IOException {
  114. while(true) {
  115. totalQuery.incrementAndGet();
  116. Object value = core.get(key);
  117. if(value instanceof SoftReference) {
  118. SoftReference<T> wfp = (SoftReference<T>) value;
  119. T t = wfp.get();
  120. if(t!=null) {
  121. cacheHit.incrementAndGet();
  122. return t; // found it
  123. }
  124. weakRefLost.incrementAndGet();
  125. }
  126. if(value instanceof Loading) {
  127. // another thread is loading it. get the value from there.
  128. T t = ((Loading<T>)value).get();
  129. if(t!=null || !createIfNotExist)
  130. return t; // found it (t!=null) or we are just 'get' (!createIfNotExist)
  131. }
  132. // the fingerprint doesn't seem to be loaded thus far, so let's load it now.
  133. // the care needs to be taken that other threads might be trying to do the same.
  134. Loading<T> l = new Loading<T>();
  135. if(value==null ? core.putIfAbsent(key,l)!=null : !core.replace(key,value,l)) {
  136. // the value has changed since then. another thread is attempting to do the same.
  137. // go back to square 1 and try it again.
  138. continue;
  139. }
  140. T t = null;
  141. try {
  142. t = load(key);
  143. if(t==null && createIfNotExist) {
  144. t = create(key,createParams); // create the new data
  145. if(t==null)
  146. throw new IllegalStateException(); // bug in the derived classes
  147. }
  148. } catch(IOException e) {
  149. loadFailure.incrementAndGet();
  150. throw e;
  151. } finally {
  152. // let other threads know that the value is available now.
  153. // when the original thread failed to load, this should set it to null.
  154. l.set(t);
  155. }
  156. // the map needs to be updated to reflect the result of loading
  157. if(t!=null)
  158. core.put(key,new SoftReference<T>(t));
  159. else
  160. core.remove(key);
  161. return t;
  162. }
  163. }
  164. /**
  165. * Attempts to load an existing data object from disk.
  166. *
  167. * <p>
  168. * {@link KeyedDataStorage} class serializes the requests so that
  169. * no two threads call the {@link #load(String)} method with the
  170. * same parameter concurrently. This ensures that there's only
  171. * up to one data object for any key.
  172. *
  173. * @return
  174. * null if no such data exists.
  175. * @throws IOException
  176. * if load operation fails. This exception will be
  177. * propagated to the caller.
  178. */
  179. protected abstract T load(String key) throws IOException;
  180. /**
  181. * Creates a new data object.
  182. *
  183. * <p>
  184. * This method is called by {@link #getOrCreate(String,Object)}
  185. * if the data that matches the specified key does not exist.
  186. * <p>
  187. * Because of concurrency, another thread might call {@link #get(String)}
  188. * as soon as a new data object is created, so it's important that
  189. * this method returns a properly initialized "valid" object.
  190. *
  191. * @return
  192. * never null. If construction fails, abort with an exception.
  193. * @throws IOException
  194. * if the method fails to create a new data object, it can throw
  195. * {@link IOException} (or any other exception) and that will be
  196. * propagated to the caller.
  197. */
  198. protected abstract T create(String key, P createParams) throws IOException;
  199. public void resetPerformanceStats() {
  200. totalQuery.set(0);
  201. cacheHit.set(0);
  202. weakRefLost.set(0);
  203. loadFailure.set(0);
  204. }
  205. /**
  206. * Gets the short summary of performance statistics.
  207. */
  208. public String getPerformanceStats() {
  209. int total = totalQuery.get();
  210. int hit = cacheHit.get();
  211. int weakRef = weakRefLost.get();
  212. int failure = loadFailure.get();
  213. int miss = total-hit-weakRef;
  214. return MessageFormat.format("total={0} hit={1}% lostRef={2}% failure={3}% miss={4}%",
  215. total,hit,weakRef,failure,miss);
  216. }
  217. /**
  218. * Total number of queries into this storage.
  219. */
  220. public final AtomicInteger totalQuery = new AtomicInteger();
  221. /**
  222. * Number of cache hits (of all the total queries.)
  223. */
  224. public final AtomicInteger cacheHit = new AtomicInteger();
  225. /**
  226. * Among cache misses, number of times when we had {@link SoftReference}
  227. * but lost its value due to GC.
  228. *
  229. * <tt>totalQuery-cacheHit-weakRefLost</tt> means cache miss.
  230. */
  231. public final AtomicInteger weakRefLost = new AtomicInteger();
  232. /**
  233. * Number of failures in loading data.
  234. */
  235. public final AtomicInteger loadFailure = new AtomicInteger();
  236. }