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("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 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( "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 */
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 }