PageRenderTime 58ms CodeModel.GetById 24ms app.highlight 28ms RepoModel.GetById 1ms app.codeStats 0ms

/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 */
 24package hudson.util;
 25
 26import hudson.model.Fingerprint;
 27import hudson.model.FingerprintMap;
 28
 29import java.io.IOException;
 30import java.lang.ref.SoftReference;
 31import java.text.MessageFormat;
 32import java.util.concurrent.ConcurrentHashMap;
 33import java.util.concurrent.atomic.AtomicInteger;
 34
 35/**
 36 * Convenient base class for implementing data storage.
 37 *
 38 * <p>
 39 * One typical pattern of data storage in Hudson is the one that {@link Fingerprint}
 40 * uses, where each data is keyed by an unique key (MD5 sum), and that key is used
 41 * to determine the file system location of the data.
 42 *
 43 * On memory, each data is represented by one object ({@link Fingerprint}), and
 44 * write access to the same data is coordinated by using synchronization.
 45 *
 46 * <p>
 47 * With such storage, care has to be taken to ensure that there's only one data
 48 * object in memory for any given key. That means load and create operation
 49 * needs to be synchronized. This class implements this logic in a fairly efficient
 50 * way, and thus intends to help plugins that want to use such data storage.
 51 *
 52 * @since 1.87
 53 * @author Kohsuke Kawaguchi
 54 * @see FingerprintMap
 55 */
 56public abstract class KeyedDataStorage<T,P> {
 57    /**
 58     * The value is either {@code SoftReference<Fingerprint>} or {@link Loading}.
 59     *
 60     * If it's {@link SoftReference}, that represents the currently available value.
 61     * If it's {@link Loading}, then that indicates the fingerprint is being loaded.
 62     * The thread can wait on this object to be notified when the loading completes.
 63     */
 64    private final ConcurrentHashMap<String,Object> core = new ConcurrentHashMap<String,Object>();
 65
 66    /**
 67     * Used in {@link KeyedDataStorage#core} to indicate that the loading of a fingerprint
 68     * is in progress, so that we can avoid creating two {@link Fingerprint}s for the same hash code,
 69     * but do so without having a single lock.
 70     */
 71    private static class Loading<T> {
 72        private T value;
 73        private boolean set;
 74
 75        public synchronized void set(T value) {
 76            this.set = true;
 77            this.value = value;
 78            notifyAll();
 79        }
 80
 81        /**
 82         * Blocks until the value is {@link #set(Object)} by another thread
 83         * and returns the value.
 84         */
 85        public synchronized T get() {
 86            try {
 87                while(!set)
 88                    wait();
 89                return value;
 90            } catch (InterruptedException e) {
 91                // assume the loading failed, but make sure we process interruption properly later
 92                Thread.currentThread().interrupt();
 93                return null;
 94            }
 95        }
 96    }
 97
 98    /**
 99     * Atomically gets the existing data object if any, or if it doesn't exist
100     * {@link #create(String,Object) create} it and return it.
101     *
102     * @return
103     *      Never null.
104     * @param createParams
105     *      Additional parameters needed to create a new data object. Can be null.
106     */
107    public T getOrCreate(String key, P createParams) throws IOException {
108        return get(key,true,createParams);
109    }
110
111    /**
112     * Finds the data object that matches the given key if available, or null
113     * if not found.
114     */
115    public T get(String key) throws IOException {
116        return get(key,false,null);
117    }
118
119    /**
120     * Implementation of get/getOrCreate.
121     */
122    protected T get(String key, boolean createIfNotExist, P createParams) throws IOException {
123        while(true) {
124            totalQuery.incrementAndGet();
125            Object value = core.get(key);
126
127            if(value instanceof SoftReference) {
128                SoftReference<T> wfp = (SoftReference<T>) value;
129                T t = wfp.get();
130                if(t!=null) {
131                    cacheHit.incrementAndGet();
132                    return t;  // found it
133                }
134                weakRefLost.incrementAndGet();
135            }
136            if(value instanceof Loading) {
137                // another thread is loading it. get the value from there.
138                T t = ((Loading<T>)value).get();
139                if(t!=null || !createIfNotExist)
140                    return t;   // found it (t!=null) or we are just 'get' (!createIfNotExist)
141            }
142
143            // the fingerprint doesn't seem to be loaded thus far, so let's load it now.
144            // the care needs to be taken that other threads might be trying to do the same.
145            Loading<T> l = new Loading<T>();
146            if(value==null ? core.putIfAbsent(key,l)!=null : !core.replace(key,value,l)) {
147                // the value has changed since then. another thread is attempting to do the same.
148                // go back to square 1 and try it again.
149                continue;
150            }
151
152            T t = null;
153            try {
154                t = load(key);
155                if(t==null && createIfNotExist) {
156                    t = create(key,createParams);    // create the new data
157                    if(t==null)
158                        throw new IllegalStateException(); // bug in the derived classes
159                }
160            } catch(IOException e) {
161                loadFailure.incrementAndGet();
162                throw e;
163            } finally {
164                // let other threads know that the value is available now.
165                // when the original thread failed to load, this should set it to null.
166                l.set(t);
167            }
168
169            // the map needs to be updated to reflect the result of loading
170            if(t!=null)
171                core.put(key,new SoftReference<T>(t));
172            else
173                core.remove(key);
174
175            return t;
176        }
177
178    }
179
180    /**
181     * Attempts to load an existing data object from disk.
182     *
183     * <p>
184     * {@link KeyedDataStorage} class serializes the requests so that
185     * no two threads call the {@link #load(String)} method with the
186     * same parameter concurrently. This ensures that there's only
187     * up to one data object for any key.
188     *
189     * @return
190     *      null if no such data exists.
191     * @throws IOException
192     *      if load operation fails. This exception will be
193     *      propagated to the caller.
194     */
195    protected abstract T load(String key) throws IOException;
196
197    /**
198     * Creates a new data object.
199     *
200     * <p>
201     * This method is called by {@link #getOrCreate(String,Object)}
202     * if the data that matches the specified key does not exist.
203     * <p>
204     * Because of concurrency, another thread might call {@link #get(String)}
205     * as soon as a new data object is created, so it's important that
206     * this method returns a properly initialized "valid" object.
207     *
208     * @return
209     *      never null. If construction fails, abort with an exception.
210     * @throws IOException
211     *      if the method fails to create a new data object, it can throw
212     *      {@link IOException} (or any other exception) and that will be
213     *      propagated to the caller.
214     */
215    protected abstract T create(String key, P createParams) throws IOException;
216
217    public void resetPerformanceStats() {
218        totalQuery.set(0);
219        cacheHit.set(0);
220        weakRefLost.set(0);
221        loadFailure.set(0);
222    }
223
224    /**
225     * Gets the short summary of performance statistics.
226     */
227    public String getPerformanceStats() {
228        int total = totalQuery.get();
229        int hit = cacheHit.get();
230        int weakRef = weakRefLost.get();
231        int failure = loadFailure.get();
232        int miss = total-hit-weakRef;
233
234        return MessageFormat.format("total={0} hit={1}% lostRef={2}% failure={3}% miss={4}%",
235                total,hit,weakRef,failure,miss);
236    }
237
238    /**
239     * Total number of queries into this storage.
240     */
241    public final AtomicInteger totalQuery = new AtomicInteger();
242    /**
243     * Number of cache hits (of all the total queries.)
244     */
245    public final AtomicInteger cacheHit = new AtomicInteger();
246    /**
247     * Among cache misses, number of times when we had {@link SoftReference}
248     * but lost its value due to GC.
249     *
250     * <tt>totalQuery-cacheHit-weakRefLost</tt> means cache miss.
251     */
252    public final AtomicInteger weakRefLost = new AtomicInteger();
253    /**
254     * Number of failures in loading data.
255     */
256    public final AtomicInteger loadFailure = new AtomicInteger();
257}