You are viewing a plain text version of this content. The canonical link for it is here.
Posted to jcs-dev@jakarta.apache.org by Travis Savo <ts...@IFILM.com> on 2004/07/17 02:03:44 UTC

CachingWorker

Did you want my CachingWorker?

It's a wrapper around JCS to encapsulate the most common set of actions
performed with JCS.

Given a region and a key, do the following psudo-code:

if(region.get(key) != null)
  return region.get(key)
otherwise
  doTheWork()
  region.put(result)
  return result

It also ensures that if a CachingWorker is in the middle of doing the work
for the same region and key, the second (or Nth) CachingWorker will wait and
use the results from that instead of doing the get/work/put again.

Using JCS to do this might look like:

final String key = "some:key";
MyObject myObject = null;
JCS myRegion = JCS.getInstance("myRegion");
if((myObject = (MyObject) myRegion.get(key)) == null){
  myObject = makeSomeObject(key);
  myRegion.put(key, myObject);
}

With the caching worker it's:

final String key = "some:key";
CachingWorker worker = new CachingWorker("myRegion", key) {
  public Object doWork() throws Exception {
      return makeSomeObject(key);
  }
};
MyObject myObject = (MyObject) worker.getResult();

The advantage is with the CachingWorker, if two (or more) threads execute
this code in parallel, only one will do the work, while the second
CachingWorker waits, and mooches off the result of the first.

I stuck it in the org.apache.jcs package in my project because it's just an
advanced interface for org.apache.jcs.JCS and has no dependencies on
anything but org.apache.jcs.JCS and commons.logging.

Here's the code. It's well javadoced. Of course I'd be happy to commit it
myself if you want.

/*
 * Created on Oct 8, 2003
 *  
 */
package org.apache.jcs;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
 * Utility class to encapsulate doing a piece of work, and caching the
results
 * in JCS. Simply implement this class and override protected Object
doWork(),
 * and do the work in there, returning the object to be cached. Then call
 * .getResult() to get the result of the work. If the object isn't allready
in
 * the Cache, doWork() will get called, and the result will be put into the
 * cache. If the object is allready in cache, the cached result will be
returned
 * instead. As an added bonus, multiple CachingWorkers with the same region,
 * group, and key won't do the work multiple times: The first CachingWorker
to
 * get started will do the work, and all subsequent workers with the same
 * region, group, and key will wait on the first one and use his resulting
work
 * instead of doing the work themselves.
 * 
 * This is ideal when the work being done is a query to the database where
the
 * results may take time to be retrieved.
 * 
 * For example: <br>
 * 
 * <code>
 *     CachingWorker cachingWorker = new CachingWorker("example region",
aKey) {<br> 
 *     		public Object doWork() throws Exception { <br>
 *          // Do some (DB?) work here which results in a list <br>
 *          // This only happens if the cache dosn't have a item in this
region for aKey <br>
 *          // Note this is especially useful with Hibernate, which will
cache indiviual <br>
 *          // Objects, but not entire query result sets. <br>
 *          List results = query.list(); <br>
 *          // Whatever we return here get's cached with aKey, and future
calls to <br>
 *          // getResult() on a CachedWorker with the same region and key
will return that instead. <br>
 *          return results; <br>
 *        } <br>
 *      }; <br>
 *      List results = (List) cachingWorker.getResult();<br>
 *     </code>
 * 
 * This is essentially the same as doing: JCS jcs = JCS.getInstance("example
 * region"); List results = (List) jcs.get(aKey); if(results != null){ //do
the
 * work here results = query.list(); jcs.put(aKey, results); }
 * 
 * 
 * But has the added benifit of the work-load sharing; under normal
 * circumstances if multiple threads all tried to do the same query at the
same
 * time, the same query would happen multiple times on the database, and the
 * resulting object would get put into JCS multiple times.
 * 
 * Using the Caching worker eliminates this senario entirely.
 * 
 * @author Travis Savo
 */
public abstract class CachingWorker {
	private static final Log logger =
LogFactory.getLog(CachingWorker.class);
	/**
	 * Object to hold the result of the work.
	 */
	private Object result = null;
	public boolean wasCached = false;
	/**
	 * Map to hold who's doing work presently.
	 */
	private static volatile Map map = new HashMap();
	/**
	 * Boolean to let us know if we need to wait or if it's finished.
	 */
	private volatile boolean finished = false;
	/**
	 * Region for the JCS cache.
	 */
	private String region;
	/**
	 * Key for caching the results with.
	 */
	private Serializable key;
	/**
	 * Optional Group to store the result under.
	 */
	private String group = null;
	/**
	 * Constructor which takes a region and a key, but no group.
	 * 
	 * @param aName
	 *          The Region to use for the JCS cache.
	 * @param aKey
	 *          The key to store the result under.
	 */
	public CachingWorker(String aRegion, Serializable aKey) {
		this(aRegion, aKey, null);
	}
	/**
	 * Constructor which takes a region, a key, and a group.
	 * 
	 * @param aName
	 *          The Region to use for the JCS cache.
	 * @param aKey
	 *          The key to store the result under.
	 * @param aGroup
	 *          The group to store the result under.
	 */
	public CachingWorker(String aRegion, Serializable aKey, String
aGroup) {
		region = aRegion;
		key = aKey;
		group = aGroup;
		finished = false;
	}
	/**
	 * Abstract method to implement for the case where the object has
not been
	 * cached yet, and we need to get it from somewhere else and stick
the result
	 * in the cache.
	 * 
	 * You should not be calling this yourself! Call getResult()
instead.
	 * 
	 * @return The result of doing the work to be cached and returned in
	 *         getResult()
	 * @throws Exception
	 *           If something goes wrong, throw an Exception and nothing
will get
	 *           cached.
	 */
	protected abstract Object doWork() throws Exception;
	/**
	 * Getter for the region of the JCS Cache.
	 * 
	 * @return The JCS region in which the result will be cached.
	 */
	public String getRegion() {
		return region;
	}
	/**
	 * Getter for the key used to store the result of doing the work in
JCS.
	 * 
	 * @return The key used to cache the result of the work.
	 */
	public Serializable getKey() {
		return key;
	}
	/**
	 * Getter for the group used to store the result of doing the work
in JCS.
	 * 
	 * @return The key group used to cache the result of the work (could
be null).
	 */
	public String getGroup() {
		return group;
	}
	/**
	 * Gets the cached result for this region/key OR does the work and
caches the
	 * result, returning the result. If the result has not been cached
yet, this
	 * calls doWork() to do the work and cache the result.
	 * 
	 * This is also an opertunity to do any post processing of the
result in your
	 * CachedWorker implementation.
	 * 
	 * @return The result of doing the work, or the cached result.
	 * @throws Exception
	 *           Throws an exception if anything goes wrong while doing
the work.
	 */
	public Object getResult() throws Exception {
		return run();
	}
	/**
	 * Try and get the object from the cache, and if it's not there, do
the work
	 * and cache it. This also ensures that only one CachedWorker is
doing the
	 * work and subsequent calls to a CachedWorker with identical
region/key/group
	 * will wait on the results of this call.
	 * 
	 * @return Either the result of doing the work, or the cached
result.
	 * @throws Exception
	 *           If something goes wrong while doing the work, throw an
exception.
	 */
	private Object run() throws Exception {
		long start = 0;
		long dbTime = 0;
		CachingWorker thread = null;
		synchronized (map) {
			//Check to see if we allready have a thread doing
this work.
			thread = (CachingWorker) map.get(getRegion() +
getKey());
			if (thread == null) {
				//If not, add ourselves as the Worker so
				//calls in another thread will use this
worker's result
				map.put(getRegion() + getKey(), this);
			}
		}
		if (thread != null) {
			synchronized (thread) {
				if (logger.isDebugEnabled()) {
					logger.debug("Found a worker
allready doing this work (" + thread.getRegion() + ":" + thread.getKey() +
").");
				}
				if (!thread.finished) {
					thread.wait();
				}
				if (logger.isDebugEnabled()) {
					logger.debug("Another thread
finished our work for us. Using thoes results instead. (" +
thread.getRegion() + ":" + thread.getKey() + ").");
				}
				//Get the result and return it.
				result = thread.result;
				wasCached = thread.wasCached;
				return result;
			}
		}
		//Do the work
		try {
			if (logger.isDebugEnabled()) {
				logger.debug(getRegion() + " is doing the
work.");
			}
			result = null;
			//Get the cache
			JCS cache = JCS.getInstance(getRegion());
			//Try to get the item from the cache
			if (group != null) {
				result = cache.getFromGroup(getKey(),
group);
			} else {
				result = cache.get(getKey());
			}
			//If the cache dosn't have it, do the work.
			wasCached = true;
			if (result == null) {
				wasCached = false;
				result = doWork();
				if (logger.isDebugEnabled()) {
					logger.debug("Work Done, caching:
key:" + getKey() + ", group:" + getGroup() + ", result:" + result + ".");
				}
				//Stick the result of the work in the cache.
				if (group != null) {
					cache.putInGroup(getKey(), group,
result);
				} else {
					cache.put(getKey(), result);
				}
			}
			//return the result
			return result;
		} finally {
			if (logger.isDebugEnabled()) {
				logger.debug(getRegion() + ":" + getKey() +
" entered finally.");
			}
			synchronized (map) {
				//Remove ourselves as the worker.
				map.remove(getRegion() + getKey());
				synchronized (this) {
					finished = true;
					//Wake everyone waiting on us
					notifyAll();
				}
			}
		}
	}
}

---------------------------------------------------------------------
To unsubscribe, e-mail: turbine-jcs-dev-unsubscribe@jakarta.apache.org
For additional commands, e-mail: turbine-jcs-dev-help@jakarta.apache.org


RE: CachingWorker

Posted by Aaron Smuts <aa...@wisc.edu>.
This is interesting.  I don't like getting the region everytime you do a
get.  This slows things down dramatically.  I posted on the javaworld
article on caching about this.  Their timings were way off because of
it.  We might be able to make it more efficient.  I'd prefer issuing a
get on a region I have has a static variable, and then calling something
like this if it returns null.  Would that work?

I see how it could be useful.  You can always put anything in the
experiemental package or, maybe better, something like this in a
subpackage of utils on the main src.  org.apache.jcs.utils.access or
worker or something that makes sense, say org.apache.control.loader .  

We should also look into the idea of implementing pluggable cacheLoaders
for a region.  

Have you looked at the FutureResult in Doug Lea's util concurrent
package.  It may be useful here.

I'm still looking over your idea.  

On another issue, we need to make items in groups returnable by jus the
key, without referencing the group.  Otherwise a group is just an
inefficient region.  To do this we need to create a CacheKey object for
the CacheElement.  The CacheKey and the GroupAttrName classes need
equals methods that can match on the same key.  

Cheers,

Aaron

> -----Original Message-----
> From: Travis Savo [mailto:tsavo@IFILM.com]
> Sent: Friday, July 16, 2004 7:04 PM
> To: 'Turbine JCS Developers List'
> Subject: CachingWorker
> 
> Did you want my CachingWorker?
> 
> It's a wrapper around JCS to encapsulate the most common set of
actions
> performed with JCS.
> 
> Given a region and a key, do the following psudo-code:
> 
> if(region.get(key) != null)
>   return region.get(key)
> otherwise
>   doTheWork()
>   region.put(result)
>   return result
> 
> It also ensures that if a CachingWorker is in the middle of doing the
work
> for the same region and key, the second (or Nth) CachingWorker will
wait
> and
> use the results from that instead of doing the get/work/put again.
> 
> Using JCS to do this might look like:
> 
> final String key = "some:key";
> MyObject myObject = null;
> JCS myRegion = JCS.getInstance("myRegion");
> if((myObject = (MyObject) myRegion.get(key)) == null){
>   myObject = makeSomeObject(key);
>   myRegion.put(key, myObject);
> }
> 
> With the caching worker it's:
> 
> final String key = "some:key";
> CachingWorker worker = new CachingWorker("myRegion", key) {
>   public Object doWork() throws Exception {
>       return makeSomeObject(key);
>   }
> };
> MyObject myObject = (MyObject) worker.getResult();
> 
> The advantage is with the CachingWorker, if two (or more) threads
execute
> this code in parallel, only one will do the work, while the second
> CachingWorker waits, and mooches off the result of the first.
> 
> I stuck it in the org.apache.jcs package in my project because it's
just
> an
> advanced interface for org.apache.jcs.JCS and has no dependencies on
> anything but org.apache.jcs.JCS and commons.logging.
> 
> Here's the code. It's well javadoced. Of course I'd be happy to commit
it
> myself if you want.
> 
> /*
>  * Created on Oct 8, 2003
>  *
>  */
> package org.apache.jcs;
> import java.io.Serializable;
> import java.util.HashMap;
> import java.util.Map;
> import org.apache.commons.logging.Log;
> import org.apache.commons.logging.LogFactory;
> /**
>  * Utility class to encapsulate doing a piece of work, and caching the
> results
>  * in JCS. Simply implement this class and override protected Object
> doWork(),
>  * and do the work in there, returning the object to be cached. Then
call
>  * .getResult() to get the result of the work. If the object isn't
> allready
> in
>  * the Cache, doWork() will get called, and the result will be put
into
> the
>  * cache. If the object is allready in cache, the cached result will
be
> returned
>  * instead. As an added bonus, multiple CachingWorkers with the same
> region,
>  * group, and key won't do the work multiple times: The first
> CachingWorker
> to
>  * get started will do the work, and all subsequent workers with the
same
>  * region, group, and key will wait on the first one and use his
resulting
> work
>  * instead of doing the work themselves.
>  *
>  * This is ideal when the work being done is a query to the database
where
> the
>  * results may take time to be retrieved.
>  *
>  * For example: <br>
>  *
>  * <code>
>  *     CachingWorker cachingWorker = new CachingWorker("example
region",
> aKey) {<br>
>  *     		public Object doWork() throws Exception { <br>
>  *          // Do some (DB?) work here which results in a list <br>
>  *          // This only happens if the cache dosn't have a item in
this
> region for aKey <br>
>  *          // Note this is especially useful with Hibernate, which
will
> cache indiviual <br>
>  *          // Objects, but not entire query result sets. <br>
>  *          List results = query.list(); <br>
>  *          // Whatever we return here get's cached with aKey, and
future
> calls to <br>
>  *          // getResult() on a CachedWorker with the same region and
key
> will return that instead. <br>
>  *          return results; <br>
>  *        } <br>
>  *      }; <br>
>  *      List results = (List) cachingWorker.getResult();<br>
>  *     </code>
>  *
>  * This is essentially the same as doing: JCS jcs =
> JCS.getInstance("example
>  * region"); List results = (List) jcs.get(aKey); if(results != null){
> //do
> the
>  * work here results = query.list(); jcs.put(aKey, results); }
>  *
>  *
>  * But has the added benifit of the work-load sharing; under normal
>  * circumstances if multiple threads all tried to do the same query at
the
> same
>  * time, the same query would happen multiple times on the database,
and
> the
>  * resulting object would get put into JCS multiple times.
>  *
>  * Using the Caching worker eliminates this senario entirely.
>  *
>  * @author Travis Savo
>  */
> public abstract class CachingWorker {
> 	private static final Log logger =
> LogFactory.getLog(CachingWorker.class);
> 	/**
> 	 * Object to hold the result of the work.
> 	 */
> 	private Object result = null;
> 	public boolean wasCached = false;
> 	/**
> 	 * Map to hold who's doing work presently.
> 	 */
> 	private static volatile Map map = new HashMap();
> 	/**
> 	 * Boolean to let us know if we need to wait or if it's
finished.
> 	 */
> 	private volatile boolean finished = false;
> 	/**
> 	 * Region for the JCS cache.
> 	 */
> 	private String region;
> 	/**
> 	 * Key for caching the results with.
> 	 */
> 	private Serializable key;
> 	/**
> 	 * Optional Group to store the result under.
> 	 */
> 	private String group = null;
> 	/**
> 	 * Constructor which takes a region and a key, but no group.
> 	 *
> 	 * @param aName
> 	 *          The Region to use for the JCS cache.
> 	 * @param aKey
> 	 *          The key to store the result under.
> 	 */
> 	public CachingWorker(String aRegion, Serializable aKey) {
> 		this(aRegion, aKey, null);
> 	}
> 	/**
> 	 * Constructor which takes a region, a key, and a group.
> 	 *
> 	 * @param aName
> 	 *          The Region to use for the JCS cache.
> 	 * @param aKey
> 	 *          The key to store the result under.
> 	 * @param aGroup
> 	 *          The group to store the result under.
> 	 */
> 	public CachingWorker(String aRegion, Serializable aKey, String
> aGroup) {
> 		region = aRegion;
> 		key = aKey;
> 		group = aGroup;
> 		finished = false;
> 	}
> 	/**
> 	 * Abstract method to implement for the case where the object
has
> not been
> 	 * cached yet, and we need to get it from somewhere else and
stick
> the result
> 	 * in the cache.
> 	 *
> 	 * You should not be calling this yourself! Call getResult()
> instead.
> 	 *
> 	 * @return The result of doing the work to be cached and
returned in
> 	 *         getResult()
> 	 * @throws Exception
> 	 *           If something goes wrong, throw an Exception and
nothing
> will get
> 	 *           cached.
> 	 */
> 	protected abstract Object doWork() throws Exception;
> 	/**
> 	 * Getter for the region of the JCS Cache.
> 	 *
> 	 * @return The JCS region in which the result will be cached.
> 	 */
> 	public String getRegion() {
> 		return region;
> 	}
> 	/**
> 	 * Getter for the key used to store the result of doing the work
in
> JCS.
> 	 *
> 	 * @return The key used to cache the result of the work.
> 	 */
> 	public Serializable getKey() {
> 		return key;
> 	}
> 	/**
> 	 * Getter for the group used to store the result of doing the
work
> in JCS.
> 	 *
> 	 * @return The key group used to cache the result of the work
(could
> be null).
> 	 */
> 	public String getGroup() {
> 		return group;
> 	}
> 	/**
> 	 * Gets the cached result for this region/key OR does the work
and
> caches the
> 	 * result, returning the result. If the result has not been
cached
> yet, this
> 	 * calls doWork() to do the work and cache the result.
> 	 *
> 	 * This is also an opertunity to do any post processing of the
> result in your
> 	 * CachedWorker implementation.
> 	 *
> 	 * @return The result of doing the work, or the cached result.
> 	 * @throws Exception
> 	 *           Throws an exception if anything goes wrong while
doing
> the work.
> 	 */
> 	public Object getResult() throws Exception {
> 		return run();
> 	}
> 	/**
> 	 * Try and get the object from the cache, and if it's not there,
do
> the work
> 	 * and cache it. This also ensures that only one CachedWorker is
> doing the
> 	 * work and subsequent calls to a CachedWorker with identical
> region/key/group
> 	 * will wait on the results of this call.
> 	 *
> 	 * @return Either the result of doing the work, or the cached
> result.
> 	 * @throws Exception
> 	 *           If something goes wrong while doing the work, throw
an
> exception.
> 	 */
> 	private Object run() throws Exception {
> 		long start = 0;
> 		long dbTime = 0;
> 		CachingWorker thread = null;
> 		synchronized (map) {
> 			//Check to see if we allready have a thread
doing
> this work.
> 			thread = (CachingWorker) map.get(getRegion() +
> getKey());
> 			if (thread == null) {
> 				//If not, add ourselves as the Worker so
> 				//calls in another thread will use this
> worker's result
> 				map.put(getRegion() + getKey(), this);
> 			}
> 		}
> 		if (thread != null) {
> 			synchronized (thread) {
> 				if (logger.isDebugEnabled()) {
> 					logger.debug("Found a worker
> allready doing this work (" + thread.getRegion() + ":" +
thread.getKey() +
> ").");
> 				}
> 				if (!thread.finished) {
> 					thread.wait();
> 				}
> 				if (logger.isDebugEnabled()) {
> 					logger.debug("Another thread
> finished our work for us. Using thoes results instead. (" +
> thread.getRegion() + ":" + thread.getKey() + ").");
> 				}
> 				//Get the result and return it.
> 				result = thread.result;
> 				wasCached = thread.wasCached;
> 				return result;
> 			}
> 		}
> 		//Do the work
> 		try {
> 			if (logger.isDebugEnabled()) {
> 				logger.debug(getRegion() + " is doing
the
> work.");
> 			}
> 			result = null;
> 			//Get the cache
> 			JCS cache = JCS.getInstance(getRegion());
> 			//Try to get the item from the cache
> 			if (group != null) {
> 				result = cache.getFromGroup(getKey(),
> group);
> 			} else {
> 				result = cache.get(getKey());
> 			}
> 			//If the cache dosn't have it, do the work.
> 			wasCached = true;
> 			if (result == null) {
> 				wasCached = false;
> 				result = doWork();
> 				if (logger.isDebugEnabled()) {
> 					logger.debug("Work Done,
caching:
> key:" + getKey() + ", group:" + getGroup() + ", result:" + result +
".");
> 				}
> 				//Stick the result of the work in the
cache.
> 				if (group != null) {
> 					cache.putInGroup(getKey(),
group,
> result);
> 				} else {
> 					cache.put(getKey(), result);
> 				}
> 			}
> 			//return the result
> 			return result;
> 		} finally {
> 			if (logger.isDebugEnabled()) {
> 				logger.debug(getRegion() + ":" +
getKey() +
> " entered finally.");
> 			}
> 			synchronized (map) {
> 				//Remove ourselves as the worker.
> 				map.remove(getRegion() + getKey());
> 				synchronized (this) {
> 					finished = true;
> 					//Wake everyone waiting on us
> 					notifyAll();
> 				}
> 			}
> 		}
> 	}
> }
> 
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: turbine-jcs-dev-unsubscribe@jakarta.apache.org
> For additional commands, e-mail:
turbine-jcs-dev-help@jakarta.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: turbine-jcs-dev-unsubscribe@jakarta.apache.org
For additional commands, e-mail: turbine-jcs-dev-help@jakarta.apache.org