1 /** 2 * Copyright 2005 Steve Molloy 3 * 4 * This file is part of OV4J. 5 * 6 * OV4J is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as 7 * published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 8 * 9 * OV4J is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 10 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 11 * 12 * You should have received a copy of the GNU General Public License along with OV4J; if not, write to the Free Software 13 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 14 * 15 */ 16 package org.ov4j.retry; 17 18 import java.io.IOException; 19 import java.lang.reflect.Method; 20 import java.util.Arrays; 21 import java.util.Hashtable; 22 import java.util.Random; 23 import java.util.logging.Level; 24 import java.util.logging.Logger; 25 26 import org.ov4j.Config; 27 import org.ov4j.Controler; 28 import org.ov4j.IContainer; 29 import org.ov4j.data.ClassComparable; 30 import org.ov4j.data.Item; 31 import org.ov4j.data.Version; 32 33 /** 34 * This is the main handler of all retried calls. 35 * 36 * @author smolloy 37 * 38 */ 39 public class RetryHandler { 40 /** 41 * Logger for this class 42 */ 43 private static final Logger logger = 44 Logger 45 .getLogger(RetryHandler.class 46 .getName()); 47 48 /** Author used for retries. */ 49 private static final String RETRY_AUTHOR = 50 Config 51 .getString("OV4J.retry.Author"); 52 53 /** Comment used for retries. */ 54 private static final String RETRY_COMMENT = 55 Config 56 .getString("OV4J.retry.Comment"); 57 58 /** Comment for cached versions */ 59 private static final String RETRY_CACHED_COMMENT = 60 Config 61 .getString("OV4J.retry.CachedVersionComment"); 62 63 /** Number of versions to keep in history. */ 64 private static final int RETRY_VERSION_ROLLOVER = 65 Integer 66 .parseInt(Config 67 .getString("OV4J.RetryVersionRollover")); 68 69 /** Randomizer for cached args IDs. */ 70 private static volatile Random theRandom = new Random(); 71 72 /** Controler used to store retry data. */ 73 private final Controler<ClassComparable, RetryableCallId> theCtrl; 74 75 /** Controler used to store arguments. */ 76 private final Controler<ClassComparable, Long> theArgCtrl; 77 78 /** Hashtable containing live instances for given classes. */ 79 private final Hashtable<String, IRetryable> liveInstances = 80 new Hashtable<String, IRetryable>(); 81 82 /** Interval between retries. */ 83 private long retryInterval = 84 Long 85 .parseLong(Config 86 .getString("OV4J.DefaultRetryInterval")); 87 88 /** Maximum number of retries. */ 89 private int maximumRetries = 90 Integer 91 .parseInt(Config 92 .getString("OV4J.maximumRetries")); 93 94 /** 95 * Constructor. 96 * 97 * @param cont 98 * The container in which to store data. 99 * @param argCont 100 * The container in which to store arguments 101 * @throws IOException 102 */ 103 public RetryHandler(final IContainer<ClassComparable, RetryableCallId> cont, 104 final IContainer<ClassComparable, Long> argCont) throws IOException { 105 theCtrl = new Controler<ClassComparable, RetryableCallId>(cont); 106 theArgCtrl = new Controler<ClassComparable, Long>(argCont); 107 setVersionRollover(RetryHandler.RETRY_VERSION_ROLLOVER); 108 retryFromCache(); 109 } 110 111 /** 112 * Cache arguments of the call. 113 * 114 * @param args 115 * Arguments to be cached. 116 * @return Unique ID to retrieve the arguments. 117 */ 118 public Long cacheArgs(final Object... args) { 119 if (RetryHandler.logger.isLoggable(Level.FINER)) { 120 RetryHandler.logger.entering("RetryHandler", "cacheArgs(Object=" + Arrays.toString(args) + ")", "start"); 121 } 122 123 Long id = null; 124 try { 125 id = RetryHandler.theRandom.nextLong(); 126 final Version<ClassComparable> theCachedVersion = new Version<ClassComparable>(); 127 theCachedVersion.setAuthor(RetryHandler.RETRY_AUTHOR); 128 theCachedVersion.setComment(RetryHandler.RETRY_CACHED_COMMENT); 129 final ClassComparable cc = new ClassComparable(); 130 cc.setTheObject(args); 131 theCachedVersion.setVersionedObject(cc); 132 if (theArgCtrl.addNCommit(id, theCachedVersion)) { 133 theArgCtrl.saveChanges(); 134 } else { 135 theArgCtrl.discardChanges(); 136 id = null; 137 } 138 } catch (final Exception e) { 139 if (RetryHandler.logger.isLoggable(Level.FINE)) { 140 RetryHandler.logger.logp(Level.FINE, "RetryHandler", "cacheArgs(Object=" + Arrays.toString(args) + ")", 141 "Exception caught", e); 142 } 143 144 id = null; 145 } 146 147 if (RetryHandler.logger.isLoggable(Level.FINER)) { 148 RetryHandler.logger.exiting("RetryHandler", "cacheArgs(Object=" + Arrays.toString(args) + ")", 149 "end - return value=" + id); 150 } 151 return id; 152 } 153 154 /** 155 * The call is done, remove it from cache. 156 * 157 * @param call 158 * Call which is done. 159 * @throws IOException 160 */ 161 protected void callDone(final RetryableCall call) throws IOException { 162 if (RetryHandler.logger.isLoggable(Level.FINER)) { 163 RetryHandler.logger.entering("RetryHandler", "callDone(RetryableCall=" + call + ")", "start"); 164 } 165 166 final Long argId = call.getArgs(); 167 final RetryableCallId theId = new RetryableCallId(); 168 theId.setArgs(call.getArgs()); 169 theId.setClassName(call.getClassName()); 170 theId.setMethodName(call.getMethod().toString()); 171 if (theCtrl.delete(theId)) { 172 if (theArgCtrl.delete(argId)) { 173 theCtrl.saveChanges(); 174 theArgCtrl.saveChanges(); 175 } else { 176 theCtrl.discardChanges(); 177 theArgCtrl.discardChanges(); 178 } 179 } 180 181 if (RetryHandler.logger.isLoggable(Level.FINER)) { 182 RetryHandler.logger.exiting("RetryHandler", "callDone(RetryableCall=" + call + ")", "end"); 183 } 184 } 185 186 /** 187 * Clear all retry data. 188 */ 189 public void clear() { 190 if (RetryHandler.logger.isLoggable(Level.FINER)) { 191 RetryHandler.logger.entering("RetryHandler", "clear()", "start"); 192 } 193 194 theCtrl.clear(); 195 theArgCtrl.clear(); 196 197 if (RetryHandler.logger.isLoggable(Level.FINER)) { 198 RetryHandler.logger.exiting("RetryHandler", "clear()", "end"); 199 } 200 } 201 202 /** 203 * Close the handler. 204 */ 205 public void close() { 206 if (RetryHandler.logger.isLoggable(Level.FINER)) { 207 RetryHandler.logger.entering("RetryHandler", "close()", "start"); 208 } 209 210 theCtrl.close(); 211 theArgCtrl.close(); 212 213 if (RetryHandler.logger.isLoggable(Level.FINER)) { 214 RetryHandler.logger.exiting("RetryHandler", "close()", "end"); 215 } 216 } 217 218 /** 219 * Find an instance of the given class. 220 * 221 * @param className 222 * @return An instance of the specified classname, or null if none was found. 223 */ 224 protected IRetryable findInstance(final String className) { 225 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 226 RetryHandler.logger.entering("RetryHandler", "findInstance(String=" + className + ")", "start"); 227 } 228 229 final IRetryable returnIRetryable = liveInstances.get(className); 230 231 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 232 RetryHandler.logger.exiting("RetryHandler", "findInstance(String=" + className + ")", "end - return value=" 233 + returnIRetryable); 234 } 235 return returnIRetryable; 236 } 237 238 /** 239 * Retrieve the arguments from cache. 240 * 241 * @param id 242 * Unique id to fetch. 243 * @return The arguments associated with this id. 244 * @throws IOException 245 */ 246 public Object[] getArgs(final Long id) throws IOException { 247 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 248 RetryHandler.logger.entering("RetryHandler", "getArgs(Long=" + id + ")", "start"); 249 } 250 251 final Version<ClassComparable> theVersion = theArgCtrl.latest(id); 252 final Object[] returnObjectArray = (Object[]) theVersion.getVersionedObject().getTheObject(); 253 254 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 255 RetryHandler.logger.exiting("RetryHandler", "getArgs(Long=" + id + ")", "end - return value=" 256 + Arrays.toString(returnObjectArray)); 257 } 258 return returnObjectArray; 259 } 260 261 /** 262 * @return Returns the maximumRetries. 263 */ 264 public int getMaximumRetries() { 265 return maximumRetries; 266 } 267 268 /** 269 * @return Returns the retryInterval. 270 */ 271 public long getRetryInterval() { 272 return retryInterval; 273 } 274 275 /** 276 * 277 * @return The version rollover. 278 */ 279 public int getVersionRollover() { 280 return theCtrl.getVersionRollover(); 281 } 282 283 /** 284 * Registers the instance to receive retry callbacks. 285 * 286 * NOTES: - Registering a second instance of the same class will override the first one. - Make sure to unregister 287 * the instances when they are no longer needed or they will not get garbage collected as the RetryHandler will keep 288 * a reference. 289 * 290 * @param cls 291 * Class handled by this instance 292 * @param instance 293 */ 294 public void register(final Class<?> cls, final IRetryable instance) { 295 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 296 RetryHandler.logger.entering("RetryHandler", "register(Class<?>=" + cls + ", IRetryable=" + instance + ")", 297 "start"); 298 } 299 300 liveInstances.put(cls.getName(), instance); 301 302 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 303 RetryHandler.logger.exiting("RetryHandler", "register(Class<?>=" + cls + ", IRetryable=" + instance + ")", 304 "end"); 305 } 306 } 307 308 /** 309 * Retry a call. 310 * 311 * @param theMethod 312 * Method to be called. 313 * @param instance 314 * Object on which to call the method. 315 * @param args 316 * Arguments to pass to the call. 317 * @return Whether or not the call was successfully registered for retry. 318 */ 319 public boolean retry(final Method theMethod, final Object instance, final Object... args) { 320 if (RetryHandler.logger.isLoggable(Level.FINER)) { 321 RetryHandler.logger.entering("RetryHandler", "retry(Method=" + theMethod + ", Object=" + instance 322 + ", Object=" + Arrays.toString(args) + ")", "start"); 323 } 324 325 boolean res = false; 326 try { 327 final RetryableCallId theId = new RetryableCallId(); 328 theId.setArgs(cacheArgs(args)); 329 theId.setClassName(instance.getClass().getName()); 330 theId.setMethodName(theMethod.toString()); 331 final RetryableCall call = new RetryableCall(); 332 call.setArgs(theId.getArgs()); 333 call.setClassName(theId.getClassName()); 334 call.setMethod(theMethod); 335 call.setNumRetries(0); 336 final Version<ClassComparable> v = new Version<ClassComparable>(); 337 v.setAuthor(RetryHandler.RETRY_AUTHOR); 338 v.setComment(RetryHandler.RETRY_COMMENT); 339 v.setVersionNumber(0); 340 final ClassComparable cc = new ClassComparable(); 341 cc.setTheObject(call); 342 v.setVersionedObject(cc); 343 res = theCtrl.addNCommit(theId, v); 344 if (res) { 345 theCtrl.saveChanges(); 346 final RetryCaller theCaller = new RetryCaller(call, this); 347 final Thread t = new Thread(theCaller); 348 t.setPriority(Thread.MIN_PRIORITY); 349 Thread.sleep(50); 350 t.start(); 351 } else { 352 theCtrl.discardChanges(); 353 } 354 } catch (final IOException e) { 355 if (RetryHandler.logger.isLoggable(Level.FINE)) { 356 RetryHandler.logger.logp(Level.FINE, "RetryHandler", "retry(Method=" + theMethod + ", Object=" 357 + instance + ", Object=" + Arrays.toString(args) + ")", "exception ignored", e); 358 } 359 } catch (final InterruptedException e) { 360 if (RetryHandler.logger.isLoggable(Level.FINE)) { 361 RetryHandler.logger.logp(Level.FINE, "RetryHandler", "retry(Method=" + theMethod + ", Object=" 362 + instance + ", Object=" + Arrays.toString(args) + ")", "exception ignored", e); 363 } 364 } 365 366 if (RetryHandler.logger.isLoggable(Level.FINER)) { 367 RetryHandler.logger.exiting("RetryHandler", "retry(Method=" + theMethod + ", Object=" + instance 368 + ", Object=" + Arrays.toString(args) + ")", "end - return value=" + res); 369 } 370 return res; 371 } 372 373 /** 374 * Loads the retryable calls previously cached so they can be retried after a VM shutdown. 375 * 376 * @throws IOException 377 * 378 */ 379 private void retryFromCache() throws IOException { 380 if (RetryHandler.logger.isLoggable(Level.FINER)) { 381 RetryHandler.logger.entering("RetryHandler", "retryFromCache()", "start"); 382 } 383 384 final RetryableCallId dummy = new RetryableCallId(); 385 final Item<ClassComparable, RetryableCallId> it = new Item<ClassComparable, RetryableCallId>(); 386 it.setId(dummy); 387 final Version<ClassComparable>[] calls = theCtrl.modifiedSince(-1); 388 for (int i = 0; i < calls.length; i++) { 389 if (calls[i].getVersionedObject().getTheObject() instanceof RetryableCall) { 390 final RetryableCall rc = (RetryableCall) calls[i].getVersionedObject().getTheObject(); 391 final RetryCaller theCaller = new RetryCaller(rc, this); 392 final Thread t = new Thread(theCaller); 393 t.setPriority(Thread.MIN_PRIORITY); 394 t.start(); 395 } 396 } 397 398 if (RetryHandler.logger.isLoggable(Level.FINER)) { 399 RetryHandler.logger.exiting("RetryHandler", "retryFromCache()", "end"); 400 } 401 } 402 403 /** 404 * @param maximumRetries 405 * The maximumRetries to set. 406 */ 407 public void setMaximumRetries(final int maximumRetries) { 408 this.maximumRetries = maximumRetries; 409 } 410 411 /** 412 * @param retryInterval 413 * The retryInterval to set. 414 */ 415 public void setRetryInterval(final long retryInterval) { 416 this.retryInterval = retryInterval; 417 } 418 419 /** 420 * 421 * @param versionRollover 422 * The version rollover to set. 423 */ 424 public void setVersionRollover(final int versionRollover) { 425 theCtrl.setVersionRollover(versionRollover); 426 theArgCtrl.setVersionRollover(versionRollover); 427 } 428 429 /** 430 * Unregisters the instance. 431 * 432 * @see #register(IRetryable instance) 433 * 434 * @param cls 435 */ 436 public void unregister(final Class<?> cls) { 437 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 438 RetryHandler.logger.entering("RetryHandler", "unregister(Class<?>=" + cls + ")", "start"); 439 } 440 441 liveInstances.remove(cls.getName()); 442 443 if (RetryHandler.logger.isLoggable(Level.FINEST)) { 444 RetryHandler.logger.exiting("RetryHandler", "unregister(Class<?>=" + cls + ")", "end"); 445 } 446 } 447 }