/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi
*
*
*******************************************************************************/
package hudson.util;
import hudson.model.Fingerprint;
import hudson.model.FingerprintMap;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Convenient base class for implementing data storage.
*
* <p> One typical pattern of data storage in Hudson is the one that
* {@link Fingerprint} uses, where each data is keyed by an unique key (MD5
* sum), and that key is used to determine the file system location of the data.
*
* On memory, each data is represented by one object ({@link Fingerprint}), and
* write access to the same data is coordinated by using synchronization.
*
* <p> With such storage, care has to be taken to ensure that there's only one
* data object in memory for any given key. That means load and create operation
* needs to be synchronized. This class implements this logic in a fairly
* efficient way, and thus intends to help plugins that want to use such data
* storage.
*
* @since 1.87
* @author Kohsuke Kawaguchi
* @see FingerprintMap
*/
public abstract class KeyedDataStorage<T, P> {
/**
* The value is either {@code SoftReference<Fingerprint>} or
* {@link Loading}.
*
* If it's {@link SoftReference}, that represents the currently available
* value. If it's {@link Loading}, then that indicates the fingerprint is
* being loaded. The thread can wait on this object to be notified when the
* loading completes.
*/
private final ConcurrentHashMap<String, Object> core = new ConcurrentHashMap<String, Object>();
/**
* Used in {@link KeyedDataStorage#core} to indicate that the loading of a
* fingerprint is in progress, so that we can avoid creating two
* {@link Fingerprint}s for the same hash code, but do so without having a
* single lock.
*/
private static class Loading<T> {
private T value;
private boolean set;
public synchronized void set(T value) {
this.set = true;
this.value = value;
notifyAll();
}
/**
* Blocks until the value is {@link #set(Object)} by another thread and
* returns the value.
*/
public synchronized T get() {
try {
while (!set) {
wait();
}
return value;
} catch (InterruptedException e) {
// assume the loading failed, but make sure we process interruption properly later
Thread.currentThread().interrupt();
return null;
}
}
}
/**
* Atomically gets the existing data object if any, or if it doesn't exist
* {@link #create(String,Object) create} it and return it.
*
* @return Never null.
* @param createParams Additional parameters needed to create a new data
* object. Can be null.
*/
public T getOrCreate(String key, P createParams) throws IOException {
return get(key, true, createParams);
}
/**
* Finds the data object that matches the given key if available, or null if
* not found.
*/
public T get(String key) throws IOException {
return get(key, false, null);
}
/**
* Implementation of get/getOrCreate.
*/
protected T get(String key, boolean createIfNotExist, P createParams) throws IOException {
while (true) {
totalQuery.incrementAndGet();
Object value = core.get(key);
if (value instanceof SoftReference) {
SoftReference<T> wfp = (SoftReference<T>) value;
T t = wfp.get();
if (t != null) {
cacheHit.incrementAndGet();
return t; // found it
}
weakRefLost.incrementAndGet();
}
if (value instanceof Loading) {
// another thread is loading it. get the value from there.
T t = ((Loading<T>) value).get();
if (t != null || !createIfNotExist) {
return t; // found it (t!=null) or we are just 'get' (!createIfNotExist)
}
}
// the fingerprint doesn't seem to be loaded thus far, so let's load it now.
// the care needs to be taken that other threads might be trying to do the same.
Loading<T> l = new Loading<T>();
if (value == null ? core.putIfAbsent(key, l) != null : !core.replace(key, value, l)) {
// the value has changed since then. another thread is attempting to do the same.
// go back to square 1 and try it again.
continue;
}
T t = null;
try {
t = load(key);
if (t == null && createIfNotExist) {
t = create(key, createParams); // create the new data
if (t == null) {
throw new IllegalStateException(); // bug in the derived classes
}
}
} catch (IOException e) {
loadFailure.incrementAndGet();
throw e;
} finally {
// let other threads know that the value is available now.
// when the original thread failed to load, this should set it to null.
l.set(t);
}
// the map needs to be updated to reflect the result of loading
if (t != null) {
core.put(key, new SoftReference<T>(t));
} else {
core.remove(key);
}
return t;
}
}
/**
* Attempts to load an existing data object from disk.
*
* <p> {@link KeyedDataStorage} class serializes the requests so that no two
* threads call the {@link #load(String)} method with the same parameter
* concurrently. This ensures that there's only up to one data object for
* any key.
*
* @return null if no such data exists.
* @throws IOException if load operation fails. This exception will be
* propagated to the caller.
*/
protected abstract T load(String key) throws IOException;
/**
* Creates a new data object.
*
* <p> This method is called by {@link #getOrCreate(String,Object)} if the
* data that matches the specified key does not exist. <p> Because of
* concurrency, another thread might call {@link #get(String)} as soon as a
* new data object is created, so it's important that this method returns a
* properly initialized "valid" object.
*
* @return never null. If construction fails, abort with an exception.
* @throws IOException if the method fails to create a new data object, it
* can throw {@link IOException} (or any other exception) and that will be
* propagated to the caller.
*/
protected abstract T create(String key, P createParams) throws IOException;
public void resetPerformanceStats() {
totalQuery.set(0);
cacheHit.set(0);
weakRefLost.set(0);
loadFailure.set(0);
}
/**
* Gets the short summary of performance statistics.
*/
public String getPerformanceStats() {
int total = totalQuery.get();
int hit = cacheHit.get();
int weakRef = weakRefLost.get();
int failure = loadFailure.get();
int miss = total - hit - weakRef;
return MessageFormat.format("total={0} hit={1}% lostRef={2}% failure={3}% miss={4}%",
total, hit, weakRef, failure, miss);
}
/**
* Total number of queries into this storage.
*/
public final AtomicInteger totalQuery = new AtomicInteger();
/**
* Number of cache hits (of all the total queries.)
*/
public final AtomicInteger cacheHit = new AtomicInteger();
/**
* Among cache misses, number of times when we had {@link SoftReference} but
* lost its value due to GC.
*
* <tt>totalQuery-cacheHit-weakRefLost</tt> means cache miss.
*/
public final AtomicInteger weakRefLost = new AtomicInteger();
/**
* Number of failures in loading data.
*/
public final AtomicInteger loadFailure = new AtomicInteger();
}