1 package org.apache.commons.jcs.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.jcs.JCS;
26 import org.apache.commons.jcs.access.CacheAccess;
27 import org.apache.commons.jcs.access.GroupCacheAccess;
28 import org.apache.commons.jcs.access.exception.CacheException;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
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.jcs.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("example region");
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 dosn't have a item in this region for aKey
62 * // Note this is especially useful with Hibernate, which will cache indiviual
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( "exampleregion" );
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 * @author Travis Savo
93 */
94 public class JCSWorker<K, V>
95 {
96 /** The logger */
97 private static final Log logger = LogFactory.getLog( JCSWorker.class );
98
99 /** 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 }