/**
*
*/
package org.jboss.cache.loader;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheException;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig;
import org.jboss.cache.util.MapCopy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The AsyncCacheLoader is a delegating cache loader that extends
* AbstractDelegatingCacheLoader overriding methods to that should not
* just delegate the operation to the underlying cache loader.
* <p/>
* Read operations are done synchronously, while write (CRUD - Create, Remove,
* Update, Delete) operations are done asynchronously. There is no provision
* for exception handling at the moment for problems encountered with the
* underlying CacheLoader during a CRUD operation, and the exception is just
* logged.
* <p/>
* When configuring the CacheLoader, use the following attribute:
* <p/>
* <code>
* <attribute name="CacheLoaderAsynchronous">true</attribute>
* </code>
* <p/>
* to define whether cache loader operations are to be asynchronous. If not
* specified, a cache loader operation is assumed synchronous.
* <p/>
* <p/>
* The following additional parameters are available:
* <dl>
* <dt>cache.async.batchSize</dt>
* <dd>Number of modifications to commit in one transaction, default is
* 100. The minimum batch size is 1.</dd>
* <dt>cache.async.pollWait</dt>
* <dd>How long to wait before processing an incomplete batch, in
* milliseconds. Default is 100. Set this to 0 to not wait before processing
* available records.</dd>
* <dt>cache.async.returnOld</dt>
* <dd>If <code>true</code>, this loader returns the old values from {@link
* #put} and {@link #remove} methods. Otherwise, these methods always return
* null. Default is true. <code>false</code> improves the performance of these
* operations.</dd>
* <dt>cache.async.queueSize</dt>
* <dd>Maximum number of entries to enqueue for asynchronous processing.
* Lowering this size may help prevent out-of-memory conditions. It also may
* help to prevent less records lost in the case of JVM failure. Default is
* 10,000 operations.</dd>
* <dt>cache.async.put</dt>
* <dd>If set to false, all {@link #put} operations will be processed
* synchronously, and then only the {@link #remove} operations will be
* processed asynchronously. This mode may be useful for processing
* expiration of messages within a separate thread and keeping other
* operations synchronous for reliability.
* </dd>
* </dl>
* For increased performance for many smaller transactions, use higher values
* for <code>cache.async.batchSize</code> and
* <code>cache.async.pollWait</code>. For larger sized records, use a smaller
* value for <code>cache.async.queueSize</code>.
*
* @author Manik Surtani (manik.surtani@jboss.com)
*/
public class AsyncCacheLoader extends AbstractDelegatingCacheLoader
{
private static final Log log = LogFactory.getLog(AsyncCacheLoader.class);
private static AtomicInteger threadId = new AtomicInteger(0);
/**
* Default limit on entries to process asynchronously.
*/
public static final int DEFAULT_QUEUE_SIZE = 10000;
private AsyncCacheLoaderConfig config;
private AsyncProcessor processor;
private AtomicBoolean stopped = new AtomicBoolean(true);
private BlockingQueue<Modification> queue = new ArrayBlockingQueue<Modification>(DEFAULT_QUEUE_SIZE);
public AsyncCacheLoader()
{
super(null);
}
public AsyncCacheLoader(CacheLoader cacheLoader)
{
super(cacheLoader);
}
public void setConfig(IndividualCacheLoaderConfig base)
{
if (base instanceof AsyncCacheLoaderConfig)
{
config = (AsyncCacheLoaderConfig) base;
}
else
{
config = new AsyncCacheLoaderConfig(base);
}
if (config.getQueueSize() > 0)
{
queue = new ArrayBlockingQueue<Modification>(config.getQueueSize());
}
super.setConfig(base);
}
public Map get(Fqn name) throws Exception
{
try
{
return super.get(name);
}
catch (IOException e)
{
// FileCacheLoader sometimes does this apparently
log.trace(e);
return new HashMap(); // ?
}
}
Object get(Fqn name, Object key) throws Exception
{
if (config.getReturnOld())
{
try
{
Map map = super.get(name);
if (map != null)
{
return map.get(key);
}
}
catch (IOException e)
{
// FileCacheLoader sometimes does this apparently
log.trace(e);
}
}
return null;
}
public Object put(Fqn name, Object key, Object value) throws Exception
{
if (config.getUseAsyncPut())
{
Object oldValue = get(name, key);
Modification mod = new Modification(Modification.ModificationType.PUT_KEY_VALUE, name, key, value);
enqueue(mod);
return oldValue;
}
else
{
return super.put(name, key, value);
}
}
public void put(Fqn name, Map attributes) throws Exception
{
if (config.getUseAsyncPut())
{
// JBCACHE-769 -- make a defensive copy
Map attrs = (attributes == null ? null : new MapCopy(attributes));
Modification mod = new Modification(Modification.ModificationType.PUT_DATA, name, attrs);
enqueue(mod);
}
else
{
super.put(name, attributes); // Let delegate make its own defensive copy
}
}
public void put(List<Modification> modifications) throws Exception
{
if (config.getUseAsyncPut())
{
for (Modification modification : modifications)
{
enqueue(modification);
}
}
else
{
super.put(modifications);
}
}
public Object remove(Fqn name, Object key) throws Exception
{
Object oldValue = get(name, key);
Modification mod = new Modification(Modification.ModificationType.REMOVE_KEY_VALUE, name, key);
enqueue(mod);
return oldValue;
}
public void remove(Fqn name) throws Exception
{
Modification mod = new Modification(Modification.ModificationType.REMOVE_NODE, name);
enqueue(mod);
}
public void removeData(Fqn name) throws Exception
{
Modification mod = new Modification(Modification.ModificationType.REMOVE_DATA, name);
enqueue(mod);
}
public void start() throws Exception
{
if (log.isInfoEnabled()) log.info("Async cache loader starting: " + this);
stopped.set(false);
super.start();
processor = new AsyncProcessor();
processor.start();
}
public void stop()
{
stopped.set(true);
if (processor != null)
{
processor.stop();
}
super.stop();
}
private void enqueue(Modification mod)
throws CacheException, InterruptedException
{
if (stopped.get())
{
throw new CacheException("AsyncCacheLoader stopped; no longer accepting more entries.");
}
if (log.isTraceEnabled())
{
log.trace("Enqueuing modification " + mod);
}
queue.put(mod);
}
/**
* Processes (by batch if possible) a queue of {@link Modification}s.
*
* @author manik surtani
*/
private class AsyncProcessor implements Runnable
{
private Thread t;
// Modifications to process as a single put
private final List<Modification> mods = new ArrayList<Modification>(config.getBatchSize());
public void start()
{
if (t == null || !t.isAlive())
{
t = new Thread(this, "AsyncCacheLoader-" + threadId.getAndIncrement());
t.setDaemon(true);
t.start();
}
}
public void stop()
{
if (t != null)
{
t.interrupt();
try
{
t.join();
}
catch (InterruptedException e)
{
}
}
if (!queue.isEmpty())
{
log.warn("Async queue not yet empty, possibly interrupted");
}
}
public void run()
{
while (!Thread.interrupted())
{
try
{
run0();
}
catch (InterruptedException e)
{
break;
}
}
try
{
if (log.isTraceEnabled()) log.trace("process remaining batch " + mods.size());
put(mods);
if (log.isTraceEnabled()) log.trace("process remaining queued " + queue.size());
while (!queue.isEmpty())
{
run0();
}
}
catch (InterruptedException e)
{
log.trace("remaining interrupted");
}
}
private void run0() throws InterruptedException
{
log.trace("Checking for modifications");
int i = queue.drainTo(mods, config.getBatchSize());
if (i == 0)
{
Modification m = queue.take();
mods.add(m);
}
if (log.isTraceEnabled())
{
log.trace("Calling put(List) with " + mods.size() + " modifications");
}
put(mods);
mods.clear();
}
private void put(List<Modification> mods)
{
try
{
AsyncCacheLoader.super.put(mods);
}
catch (Exception e)
{
if (log.isWarnEnabled()) log.warn("Failed to process async modifications: " + e);
log.debug("Exception: ", e);
}
}
public String toString()
{
return "TQ t=" + t;
}
}
public String toString()
{
return super.toString() +
" delegate=[" + super.getCacheLoader() + "]" +
" processor=" + processor +
" stopped=" + stopped +
" batchSize=" + config.getBatchSize() +
" returnOld=" + config.getReturnOld() +
" asyncPut=" + config.getUseAsyncPut() +
" queue.remainingCapacity()=" + queue.remainingCapacity() +
" queue.peek()=" + queue.peek();
}
}