View Javadoc
1   package org.apache.commons.jcs3.utils.access;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.concurrent.ConcurrentHashMap;
23  import java.util.concurrent.ConcurrentMap;
24  
25  import org.apache.commons.jcs3.JCS;
26  import org.apache.commons.jcs3.access.CacheAccess;
27  import org.apache.commons.jcs3.access.GroupCacheAccess;
28  import org.apache.commons.jcs3.access.exception.CacheException;
29  import org.apache.commons.jcs3.log.Log;
30  import org.apache.commons.jcs3.log.LogManager;
31  
32  /**
33   * Utility class to encapsulate doing a piece of work, and caching the results
34   * in JCS. Simply construct this class with the region name for the Cache and
35   * keep a static reference to it instead of the JCS itself. Then make a new
36   * org.apache.commons.jcs3.utils.access.AbstractJCSWorkerHelper and implement Object
37   * doWork() and do the work in there, returning the object to be cached. Then
38   * call .getResult() with the key and the AbstractJCSWorkerHelper to get the
39   * result of the work. If the object isn't already in the Cache,
40   * AbstractJCSWorkerHelper.doWork() will get called, and the result will be put
41   * into the cache. If the object is already in cache, the cached result will be
42   * returned instead.
43   * <p>
44   * As an added bonus, multiple JCSWorkers with the same region, and key won't do
45   * the work multiple times: The first JCSWorker to get started will do the work,
46   * and all subsequent workers with the same region, group, and key will wait on
47   * the first one and use his resulting work instead of doing the work
48   * themselves.
49   * <p>
50   * This is ideal when the work being done is a query to the database where the
51   * results may take time to be retrieved.
52   * <p>
53   * For example:
54   *
55   * <pre>
56   *      public static JCSWorker cachingWorker = new JCSWorker(&quot;example region&quot;);
57   *   		public Object getSomething(Serializable aKey){
58   *        JCSWorkerHelper helper = new AbstractJCSWorkerHelper(){
59   *          public Object doWork(){
60   *            // Do some (DB?) work here which results in a list
61   *            // This only happens if the cache doesn't have a item in this region for aKey
62   *            // Note this is especially useful with Hibernate, which will cache individual
63   *            // Objects, but not entire query result sets.
64   *            List results = query.list();
65   *            // Whatever we return here get's cached with aKey, and future calls to
66   *            // getResult() on a CachedWorker with the same region and key will return that instead.
67   *            return results;
68   *        };
69   *        List result = worker.getResult(aKey, helper);
70   *      }
71   * </pre>
72   *
73   * This is essentially the same as doing:
74   *
75   * <pre>
76   * JCS jcs = JCS.getInstance( &quot;exampleregion&quot; );
77   * List results = (List) jcs.get( aKey );
78   * if ( results != null )
79   * {
80   *     //do the work here
81   *     results = query.list();
82   *     jcs.put( aKey, results );
83   * }
84   * </pre>
85   *
86   * <p>
87   * But has the added benefit of the work-load sharing; under normal
88   * circumstances if multiple threads all tried to do the same query at the same
89   * time, the same query would happen multiple times on the database, and the
90   * resulting object would get put into JCS multiple times.
91   * </p>
92   */
93  public class JCSWorker<K, V>
94  {
95      /** The logger */
96      private static final Log logger = LogManager.getLog( JCSWorker.class );
97  
98      /** The cache we are working with */
99      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 }