001package org.apache.commons.jcs3.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.jcs3.JCS;
026import org.apache.commons.jcs3.access.CacheAccess;
027import org.apache.commons.jcs3.access.GroupCacheAccess;
028import org.apache.commons.jcs3.access.exception.CacheException;
029import org.apache.commons.jcs3.log.Log;
030import org.apache.commons.jcs3.log.LogManager;
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.jcs3.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 doesn't have a item in this region for aKey
062 *            // Note this is especially useful with Hibernate, which will cache individual
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 */
093public class JCSWorker<K, V>
094{
095    /** The logger */
096    private static final Log logger = LogManager.getLog( JCSWorker.class );
097
098    /** The cache we are working with */
099    private final CacheAccess<K, V> cache;
100
101    /** The cache we are working with */
102    private final GroupCacheAccess<K, V> groupCache;
103
104    /**
105     * Map to hold who's doing work presently.
106     */
107    private final ConcurrentMap<String, JCSWorkerHelper<V>> map = new ConcurrentHashMap<>();
108
109    /**
110     * Region for the JCS cache.
111     */
112    private final String region;
113
114    /**
115     * Constructor which takes a region for the JCS cache.
116     * @param aRegion
117     *            The Region to use for the JCS cache.
118     */
119    public JCSWorker( final String aRegion )
120    {
121        region = aRegion;
122        try
123        {
124            cache = JCS.getInstance( aRegion );
125            groupCache = JCS.getGroupCacheInstance( aRegion );
126        }
127        catch ( final CacheException e )
128        {
129            throw new RuntimeException( e.getMessage() );
130        }
131    }
132
133    /**
134     * Getter for the region of the JCS Cache.
135     * @return The JCS region in which the result will be cached.
136     */
137    public String getRegion()
138    {
139        return region;
140    }
141
142    /**
143     * Gets the cached result for this region/key OR does the work and caches
144     * the result, returning the result. If the result has not been cached yet,
145     * this calls doWork() on the JCSWorkerHelper to do the work and cache the
146     * result. This is also an opportunity to do any post processing of the
147     * result in your CachedWorker implementation.
148     * @param aKey
149     *            The key to get/put with on the Cache.
150     * @param aWorker
151     *            The JCSWorkerHelper implementing Object doWork(). This gets
152     *            called if the cache get misses, and the result is put into
153     *            cache.
154     * @return The result of doing the work, or the cached result.
155     * @throws Exception
156     *             Throws an exception if anything goes wrong while doing the
157     *             work.
158     */
159    public V getResult( final K aKey, final JCSWorkerHelper<V> aWorker )
160        throws Exception
161    {
162        return run( aKey, null, aWorker );
163    }
164
165    /**
166     * Gets the cached result for this region/key OR does the work and caches
167     * the result, returning the result. If the result has not been cached yet,
168     * this calls doWork() on the JCSWorkerHelper to do the work and cache the
169     * result. This is also an opportunity to do any post processing of the
170     * result in your CachedWorker implementation.
171     * @param aKey
172     *            The key to get/put with on the Cache.
173     * @param aGroup
174     *            The cache group to put the result in.
175     * @param aWorker
176     *            The JCSWorkerHelper implementing Object doWork(). This gets
177     *            called if the cache get misses, and the result is put into
178     *            cache.
179     * @return The result of doing the work, or the cached result.
180     * @throws Exception
181     *             Throws an exception if anything goes wrong while doing the
182     *             work.
183     */
184    public V getResult( final K aKey, final String aGroup, final JCSWorkerHelper<V> aWorker )
185        throws Exception
186    {
187        return run( aKey, aGroup, aWorker );
188    }
189
190    /**
191     * Try and get the object from the cache, and if it's not there, do the work
192     * and cache it. This also ensures that only one CachedWorker is doing the
193     * work and subsequent calls to a CachedWorker with identical
194     * region/key/group will wait on the results of this call. It will call the
195     * JCSWorkerHelper.doWork() if the cache misses, and will put the result.
196     * @param aKey
197     * @param aGroup
198     * @param aHelper
199     * @return Either the result of doing the work, or the cached result.
200     * @throws Exception
201     *             If something goes wrong while doing the work, throw an
202     *             exception.
203     */
204    private V run( final K aKey, final String aGroup, final JCSWorkerHelper<V> aHelper )
205        throws Exception
206    {
207        V result = null;
208        // long start = 0;
209        // long dbTime = 0;
210        final JCSWorkerHelper<V> helper = map.putIfAbsent(getRegion() + aKey, aHelper);
211
212        if ( helper != null )
213        {
214            synchronized ( helper )
215            {
216                logger.debug( "Found a worker already doing this work ({0}:{1}).",
217                        this::getRegion, () -> aKey );
218                while ( !helper.isFinished() )
219                {
220                    try
221                    {
222                        helper.wait();
223                    }
224                    catch (final InterruptedException e)
225                    {
226                        // expected
227                    }
228                }
229                logger.debug( "Another thread finished our work for us. Using "
230                        + "those results instead. ({0}:{1}).",
231                        this::getRegion, () -> aKey );
232            }
233        }
234        // Do the work
235        try
236        {
237            logger.debug( "{0} is doing the work.", this::getRegion);
238
239            // Try to get the item from the cache
240            if ( aGroup != null )
241            {
242                result = groupCache.getFromGroup( aKey, aGroup );
243            }
244            else
245            {
246                result = cache.get( aKey );
247            }
248            // If the cache doesn't have it, do the work.
249            if ( result == null )
250            {
251                result = aHelper.doWork();
252                logger.debug( "Work Done, caching: key:{0}, group:{1}, result:{2}.",
253                        aKey, aGroup, result );
254                // Stick the result of the work in the cache.
255                if ( aGroup != null )
256                {
257                    groupCache.putInGroup( aKey, aGroup, result );
258                }
259                else
260                {
261                    cache.put( aKey, result );
262                }
263            }
264            // return the result
265            return result;
266        }
267        finally
268        {
269            logger.debug( "{0}:{1} entered finally.", this::getRegion,
270                    () -> aKey );
271
272            // Remove ourselves as the worker.
273            if ( helper == null )
274            {
275                map.remove( getRegion() + aKey );
276            }
277            synchronized ( aHelper )
278            {
279                aHelper.setFinished( true );
280                // Wake everyone waiting on us
281                aHelper.notifyAll();
282            }
283        }
284    }
285}