001package org.apache.commons.jcs.utils.access;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.util.concurrent.ConcurrentHashMap;
023import java.util.concurrent.ConcurrentMap;
024
025import org.apache.commons.jcs.JCS;
026import org.apache.commons.jcs.access.CacheAccess;
027import org.apache.commons.jcs.access.GroupCacheAccess;
028import org.apache.commons.jcs.access.exception.CacheException;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031
032/**
033 * Utility class to encapsulate doing a piece of work, and caching the results
034 * in JCS. Simply construct this class with the region name for the Cache and
035 * keep a static reference to it instead of the JCS itself. Then make a new
036 * org.apache.commons.jcs.utils.access.AbstractJCSWorkerHelper and implement Object
037 * doWork() and do the work in there, returning the object to be cached. Then
038 * call .getResult() with the key and the AbstractJCSWorkerHelper to get the
039 * result of the work. If the object isn't already in the Cache,
040 * AbstractJCSWorkerHelper.doWork() will get called, and the result will be put
041 * into the cache. If the object is already in cache, the cached result will be
042 * returned instead.
043 * <p>
044 * As an added bonus, multiple JCSWorkers with the same region, and key won't do
045 * the work multiple times: The first JCSWorker to get started will do the work,
046 * and all subsequent workers with the same region, group, and key will wait on
047 * the first one and use his resulting work instead of doing the work
048 * themselves.
049 * <p>
050 * This is ideal when the work being done is a query to the database where the
051 * results may take time to be retrieved.
052 * <p>
053 * For example:
054 *
055 * <pre>
056 *      public static JCSWorker cachingWorker = new JCSWorker(&quot;example region&quot;);
057 *              public Object getSomething(Serializable aKey){
058 *        JCSWorkerHelper helper = new AbstractJCSWorkerHelper(){
059 *          public Object doWork(){
060 *            // Do some (DB?) work here which results in a list
061 *            // This only happens if the cache dosn't have a item in this region for aKey
062 *            // Note this is especially useful with Hibernate, which will cache indiviual
063 *            // Objects, but not entire query result sets.
064 *            List results = query.list();
065 *            // Whatever we return here get's cached with aKey, and future calls to
066 *            // getResult() on a CachedWorker with the same region and key will return that instead.
067 *            return results;
068 *        };
069 *        List result = worker.getResult(aKey, helper);
070 *      }
071 * </pre>
072 *
073 * This is essentially the same as doing:
074 *
075 * <pre>
076 * JCS jcs = JCS.getInstance( &quot;exampleregion&quot; );
077 * List results = (List) jcs.get( aKey );
078 * if ( results != null )
079 * {
080 *     //do the work here
081 *     results = query.list();
082 *     jcs.put( aKey, results );
083 * }
084 * </pre>
085 *
086 * <p>
087 * But has the added benefit of the work-load sharing; under normal
088 * circumstances if multiple threads all tried to do the same query at the same
089 * time, the same query would happen multiple times on the database, and the
090 * resulting object would get put into JCS multiple times.
091 * <p>
092 * @author Travis Savo
093 */
094public class JCSWorker<K, V>
095{
096    /** The logger */
097    private static final Log logger = LogFactory.getLog( JCSWorker.class );
098
099    /** The cache we are working with */
100    private CacheAccess<K, V> cache;
101
102    /** The cache we are working with */
103    private GroupCacheAccess<K, V> groupCache;
104
105    /**
106     * Map to hold who's doing work presently.
107     */
108    private volatile ConcurrentMap<String, JCSWorkerHelper<V>> map = new ConcurrentHashMap<String, JCSWorkerHelper<V>>();
109
110    /**
111     * Region for the JCS cache.
112     */
113    private final String region;
114
115    /**
116     * Constructor which takes a region for the JCS cache.
117     * @param aRegion
118     *            The Region to use for the JCS cache.
119     */
120    public JCSWorker( final String aRegion )
121    {
122        region = aRegion;
123        try
124        {
125            cache = JCS.getInstance( aRegion );
126            groupCache = JCS.getGroupCacheInstance( aRegion );
127        }
128        catch ( CacheException e )
129        {
130            throw new RuntimeException( e.getMessage() );
131        }
132    }
133
134    /**
135     * Getter for the region of the JCS Cache.
136     * @return The JCS region in which the result will be cached.
137     */
138    public String getRegion()
139    {
140        return region;
141    }
142
143    /**
144     * Gets the cached result for this region/key OR does the work and caches
145     * the result, returning the result. If the result has not been cached yet,
146     * this calls doWork() on the JCSWorkerHelper to do the work and cache the
147     * result. This is also an opportunity to do any post processing of the
148     * result in your CachedWorker implementation.
149     * @param aKey
150     *            The key to get/put with on the Cache.
151     * @param aWorker
152     *            The JCSWorkerHelper implementing Object doWork(). This gets
153     *            called if the cache get misses, and the result is put into
154     *            cache.
155     * @return The result of doing the work, or the cached result.
156     * @throws Exception
157     *             Throws an exception if anything goes wrong while doing the
158     *             work.
159     */
160    public V getResult( K aKey, JCSWorkerHelper<V> aWorker )
161        throws Exception
162    {
163        return run( aKey, null, aWorker );
164    }
165
166    /**
167     * Gets the cached result for this region/key OR does the work and caches
168     * the result, returning the result. If the result has not been cached yet,
169     * this calls doWork() on the JCSWorkerHelper to do the work and cache the
170     * result. This is also an opportunity to do any post processing of the
171     * result in your CachedWorker implementation.
172     * @param aKey
173     *            The key to get/put with on the Cache.
174     * @param aGroup
175     *            The cache group to put the result in.
176     * @param aWorker
177     *            The JCSWorkerHelper implementing Object doWork(). This gets
178     *            called if the cache get misses, and the result is put into
179     *            cache.
180     * @return The result of doing the work, or the cached result.
181     * @throws Exception
182     *             Throws an exception if anything goes wrong while doing the
183     *             work.
184     */
185    public V getResult( K aKey, String aGroup, JCSWorkerHelper<V> aWorker )
186        throws Exception
187    {
188        return run( aKey, aGroup, aWorker );
189    }
190
191    /**
192     * Try and get the object from the cache, and if it's not there, do the work
193     * and cache it. This also ensures that only one CachedWorker is doing the
194     * work and subsequent calls to a CachedWorker with identical
195     * region/key/group will wait on the results of this call. It will call the
196     * JCSWorkerHelper.doWork() if the cache misses, and will put the result.
197     * @param aKey
198     * @param aGroup
199     * @param aHelper
200     * @return Either the result of doing the work, or the cached result.
201     * @throws Exception
202     *             If something goes wrong while doing the work, throw an
203     *             exception.
204     */
205    private V run( K aKey, String aGroup, JCSWorkerHelper<V> aHelper )
206        throws Exception
207    {
208        V result = null;
209        // long start = 0;
210        // long dbTime = 0;
211        JCSWorkerHelper<V> helper = map.putIfAbsent(getRegion() + aKey, aHelper);
212
213        if ( helper != null )
214        {
215            synchronized ( helper )
216            {
217                if ( logger.isDebugEnabled() )
218                {
219                    logger.debug( "Found a worker already doing this work (" + getRegion() + ":" + aKey + ")." );
220                }
221                while ( !helper.isFinished() )
222                {
223                    try
224                    {
225                        helper.wait();
226                    }
227                    catch (InterruptedException e)
228                    {
229                        // expected
230                    }
231                }
232                if ( logger.isDebugEnabled() )
233                {
234                    logger.debug( "Another thread finished our work for us. Using those results instead. ("
235                        + getRegion() + ":" + aKey + ")." );
236                }
237            }
238        }
239        // Do the work
240        try
241        {
242            if ( logger.isDebugEnabled() )
243            {
244                logger.debug( getRegion() + " is doing the work." );
245            }
246
247            // Try to get the item from the cache
248            if ( aGroup != null )
249            {
250                result = groupCache.getFromGroup( aKey, aGroup );
251            }
252            else
253            {
254                result = cache.get( aKey );
255            }
256            // If the cache dosn't have it, do the work.
257            if ( result == null )
258            {
259                result = aHelper.doWork();
260                if ( logger.isDebugEnabled() )
261                {
262                    logger.debug( "Work Done, caching: key:" + aKey + ", group:" + aGroup + ", result:" + result + "." );
263                }
264                // Stick the result of the work in the cache.
265                if ( aGroup != null )
266                {
267                    groupCache.putInGroup( aKey, aGroup, result );
268                }
269                else
270                {
271                    cache.put( aKey, result );
272                }
273            }
274            // return the result
275            return result;
276        }
277        finally
278        {
279            if ( logger.isDebugEnabled() )
280            {
281                logger.debug( getRegion() + ":" + aKey + " entered finally." );
282            }
283
284            // Remove ourselves as the worker.
285            if ( helper == null )
286            {
287                map.remove( getRegion() + aKey );
288            }
289            synchronized ( aHelper )
290            {
291                aHelper.setFinished( true );
292                // Wake everyone waiting on us
293                aHelper.notifyAll();
294            }
295        }
296    }
297}