protected <E extends Throwable> void rethrow(RetryContext context, String message) throws E { if (this.throwLastExceptionOnExhausted) { @SuppressWarnings("unchecked") E rethrow = (E) context.getLastThrowable(); throw rethrow; } else { throw new ExhaustedRetryException(message, context.getLastThrowable()); } }
private RetryContext doOpenInternal(RetryPolicy retryPolicy, RetryState state) { RetryContext context = retryPolicy.open(RetrySynchronizationManager.getContext()); if (state != null) { context.setAttribute(RetryContext.STATE_KEY, state.getKey()); } if (context.hasAttribute(GLOBAL_STATE)) { registerContext(context, state); } return context; }
/** * Clean up the cache if necessary and close the context provided (if the flag indicates that * processing was successful). * * @param retryPolicy the {@link RetryPolicy} * @param context the {@link RetryContext} * @param state the {@link RetryState} * @param succeeded whether the close succeeded */ protected void close( RetryPolicy retryPolicy, RetryContext context, RetryState state, boolean succeeded) { if (state != null) { if (succeeded) { if (!context.hasAttribute(GLOBAL_STATE)) { this.retryContextCache.remove(state.getKey()); } retryPolicy.close(context); context.setAttribute(RetryContext.CLOSED, true); } } else { retryPolicy.close(context); context.setAttribute(RetryContext.CLOSED, true); } }
/** * Actions to take after final attempt has failed. If there is state clean up the cache. If there * is a recovery callback, execute that and return its result. Otherwise throw an exception. * * @param recoveryCallback the callback for recovery (might be null) * @param context the current retry context * @param state the {@link RetryState} * @param <T> the type to classify * @throws Exception if the callback does, and if there is no callback and the state is null then * the last exception from the context * @throws ExhaustedRetryException if the state is not null and there is no recovery callback * @return T the payload to return */ protected <T> T handleRetryExhausted( RecoveryCallback<T> recoveryCallback, RetryContext context, RetryState state) throws Throwable { context.setAttribute(RetryContext.EXHAUSTED, true); if (state != null && !context.hasAttribute(GLOBAL_STATE)) { this.retryContextCache.remove(state.getKey()); } if (recoveryCallback != null) { context.setAttribute(RetryContext.RECOVERED, true); return recoveryCallback.recover(context); } if (state != null) { this.logger.debug("Retry exhausted after last attempt with no recovery path."); rethrow(context, "Retry exhausted after last attempt with no recovery path"); } throw wrapIfNecessary(context.getLastThrowable()); }
@Override public Object doWithRetry(RetryContext context) throws Exception { context.setAttribute(RetryContext.NAME, label); try { return this.invocation.proceed(); } catch (Exception e) { throw e; } catch (Error e) { throw e; } catch (Throwable e) { throw new IllegalStateException(e); } }
private void registerContext(RetryContext context, RetryState state) { if (state != null) { Object key = state.getKey(); if (key != null) { if (context.getRetryCount() > 1 && !this.retryContextCache.containsKey(key)) { throw new RetryException( "Inconsistent state for failed item key: cache key has changed. " + "Consider whether equals() or hashCode() for the key might be inconsistent, " + "or if you need to supply a better key"); } this.retryContextCache.put(key, context); } } }
/** * Delegate to the {@link RetryPolicy} having checked in the cache for an existing value if the * state is not null. * * @param state a {@link RetryState} * @param retryPolicy a {@link RetryPolicy} to delegate the context creation * @return a retry context, either a new one or the one used last time the same state was * encountered */ protected RetryContext open(RetryPolicy retryPolicy, RetryState state) { if (state == null) { return doOpenInternal(retryPolicy); } Object key = state.getKey(); if (state.isForceRefresh()) { return doOpenInternal(retryPolicy, state); } // If there is no cache hit we can avoid the possible expense of the // cache re-hydration. if (!this.retryContextCache.containsKey(key)) { // The cache is only used if there is a failure. return doOpenInternal(retryPolicy, state); } RetryContext context = this.retryContextCache.get(key); if (context == null) { if (this.retryContextCache.containsKey(key)) { throw new RetryException( "Inconsistent state for failed item: no history found. " + "Consider whether equals() or hashCode() for the item might be inconsistent, " + "or if you need to supply a better ItemKeyGenerator"); } // The cache could have been expired in between calls to // containsKey(), so we have to live with this: return doOpenInternal(retryPolicy, state); } // Start with a clean slate for state that others may be inspecting context.removeAttribute(RetryContext.CLOSED); context.removeAttribute(RetryContext.EXHAUSTED); context.removeAttribute(RetryContext.RECOVERED); return context; }
/** * Extension point for subclasses to decide on behaviour after catching an exception in a {@link * RetryCallback}. Normal stateless behaviour is not to rethrow, and if there is state we rethrow. * * @param retryPolicy the retry policy * @param context the current context * @param state the current retryState * @return true if the state is not null but subclasses might choose otherwise */ protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context, RetryState state) { return state != null && state.rollbackFor(context.getLastThrowable()); }
/** * Execute the callback once if the policy dictates that we can, otherwise execute the recovery * callback. * * @param recoveryCallback the {@link RecoveryCallback} * @param retryCallback the {@link RetryCallback} * @param state the {@link RetryState} * @param <T> the type of the return value * @param <E> the exception type to throw * @see RetryOperations#execute(RetryCallback, RecoveryCallback, RetryState) * @throws ExhaustedRetryException if the retry has been exhausted. * @throws E an exception if the retry operation fails * @return T the retried value */ protected <T, E extends Throwable> T doExecute( RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState state) throws E, ExhaustedRetryException { RetryPolicy retryPolicy = this.retryPolicy; BackOffPolicy backOffPolicy = this.backOffPolicy; // Allow the retry policy to initialise itself... RetryContext context = open(retryPolicy, state); if (this.logger.isTraceEnabled()) { this.logger.trace("RetryContext retrieved: " + context); } // Make sure the context is available globally for clients who need // it... RetrySynchronizationManager.register(context); Throwable lastException = null; boolean exhausted = false; try { // Give clients a chance to enhance the context... boolean running = doOpenInterceptors(retryCallback, context); if (!running) { throw new TerminatedRetryException( "Retry terminated abnormally by interceptor before first attempt"); } // Get or Start the backoff context... BackOffContext backOffContext = null; Object resource = context.getAttribute("backOffContext"); if (resource instanceof BackOffContext) { backOffContext = (BackOffContext) resource; } if (backOffContext == null) { backOffContext = backOffPolicy.start(context); if (backOffContext != null) { context.setAttribute("backOffContext", backOffContext); } } /* * We allow the whole loop to be skipped if the policy or context already * forbid the first try. This is used in the case of external retry to allow a * recovery in handleRetryExhausted without the callback processing (which * would throw an exception). */ while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { try { if (this.logger.isDebugEnabled()) { this.logger.debug("Retry: count=" + context.getRetryCount()); } // Reset the last exception, so if we are successful // the close interceptors will not think we failed... lastException = null; return retryCallback.doWithRetry(context); } catch (Throwable e) { lastException = e; try { registerThrowable(retryPolicy, state, context, e); } catch (Exception ex) { throw new TerminatedRetryException("Could not register throwable", ex); } finally { doOnErrorInterceptors(retryCallback, context, e); } if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { try { backOffPolicy.backOff(backOffContext); } catch (BackOffInterruptedException ex) { lastException = e; // back off was prevented by another thread - fail the retry if (this.logger.isDebugEnabled()) { this.logger.debug( "Abort retry because interrupted: count=" + context.getRetryCount()); } throw ex; } } if (this.logger.isDebugEnabled()) { this.logger.debug("Checking for rethrow: count=" + context.getRetryCount()); } if (shouldRethrow(retryPolicy, context, state)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount()); } throw RetryTemplate.<E>wrapIfNecessary(e); } } /* * A stateful attempt that can retry may rethrow the exception before now, * but if we get this far in a stateful retry there's a reason for it, * like a circuit breaker or a rollback classifier. */ if (state != null && context.hasAttribute(GLOBAL_STATE)) { break; } } if (state == null && this.logger.isDebugEnabled()) { this.logger.debug("Retry failed last attempt: count=" + context.getRetryCount()); } exhausted = true; return handleRetryExhausted(recoveryCallback, context, state); } catch (Throwable e) { throw RetryTemplate.<E>wrapIfNecessary(e); } finally { close(retryPolicy, context, state, lastException == null || exhausted); doCloseInterceptors(retryCallback, context, lastException); RetrySynchronizationManager.clear(); } }
public Object recover(RetryContext context) { if (recoverer != null) { return recoverer.recover(args, context.getLastThrowable()); } throw new ExhaustedRetryException("Retry was exhausted but there was no recovery path."); }
@Override public Object recover(RetryContext context) { return this.recoverer.recover(this.args, context.getLastThrowable()); }