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 }