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 }