
org.dellroad.stuff.spring.RetryTransaction Maven / Gradle / Ivy
Show all versions of dellroad-stuff-spring Show documentation
/*
* Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.spring;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation for {@link org.springframework.transaction.annotation.Transactional @Transactional} methods that
* want to have transactions automatically retried when they fail due to a transient exception. A transient exception
* is one that Spring would translate into a
* {@link org.springframework.dao.TransientDataAccessException TransientDataAccessException}.
*
*
*
*
*
*
*
* This automatic retry logic is very handy for solving the problem of transient deadlocks that can occur in complex Java/ORM
* applications. Due to the ORM layer hiding the details of the underlying data access patterns, it's often difficult
* to design Java/ORM applications such that transient deadlocks at the database layer can't occur. Since these
* deadlocks can often be dealt with simply by retrying the transaction, having retry logic automatically applied can
* eliminate this problem.
*
*
* Note, beans involved in transactions should either be stateless, or be prepared to rollback any state changes on transaction
* failure; of course, this is true whether or not transactions are automatically being retried, but adding automatic retry
* can magnify pre-existing bugs of that nature.
*
*
* The {@link RetryTransaction @RetryTransaction} annotation is ignored unless all of the following conditions are satisfied:
*
* -
* The method (and/or the containing type) must be annotated with both
* {@link org.springframework.transaction.annotation.Transactional @Transactional}
* and {@link RetryTransaction @RetryTransaction}
*
* -
* The {@link org.springframework.transaction.annotation.Transactional @Transactional} annotation must have
* {@linkplain org.springframework.transaction.annotation.Transactional#propagation propagation} set to either
* {@link org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRED PROPAGATION_REQUIRED} or
* {@link org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRES_NEW PROPAGATION_REQUIRES_NEW}
* (other propagation values do not involve creating new transactions).
*
* -
* In the case of
* {@link org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRED PROPAGATION_REQUIRED} propagation,
* there must not be a transaction already open in the calling thread (under the same transaction manager). In other words,
* the invoked method must be the one responsible for creating a new transaction.
*
* -
* The method's class must be woven (either at build time or runtime) using the
* AspectJ compiler
* with the {@code RetryTransactionAspect} aspect (included in the
dellroad-stuff
JAR file).
*
* -
* The {@code RetryTransactionAspect} aspect must be configured with a
* {@link org.springframework.dao.support.PersistenceExceptionTranslator PersistenceExceptionTranslator} appropriate for
* the ORM layer being used.
*
*
* This is required because the {@code RetryTransactionAspect} doesn't know a priori which exceptions are
* "retryable" and which exceptions are hard errors. However, this is something that the
* {@link org.springframework.dao.support.PersistenceExceptionTranslator PersistenceExceptionTranslator} knows,
* because part of its job is wrapping lower-layer exceptions in {@link org.springframework.dao} exceptions,
* in particular {@link org.springframework.dao.TransientDataAccessException TransientDataAccessException}.
*
*
* The simplest way to do this is to include the aspect in your Spring application context, for example:
*
*
* <bean class="org.dellroad.stuff.spring.RetryTransactionAspect" factory-method="aspectOf"
* p:persistenceExceptionTranslator-ref="myJpaDialect"/>
*
*
*
* This also gives you the opportunity to change the default values for {@link #maxRetries}, {@link #initialDelay},
* and {@link #maximumDelay}, which are applied when not explicitly overridden in the annotation, for example:
*
*
* <bean class="org.dellroad.stuff.spring.RetryTransactionAspect" factory-method="aspectOf"
* p:persistenceExceptionTranslator-ref="myJpaDialect" p:maxRetriesDefault="2"
* p:initialDelayDefault="25" p:maximumDelayDefault="5000"/>
*
*
*
*
*
* Logging behavior: Normal activity is logged at trace level, retries are logged at debug level, and errors are logged
* at error level.
*
*
* Transactional code can determine the transaction attempt number using the {@link RetryTransactionProvider} interface
* implemented by the aspect. {@link RetryTransactionProvider#getAttemptNumber} method returns the current attempt number
* (1, 2, 3...), or zero if the current thread is not executing within activated retry logic:
*
* import org.dellroad.stuff.spring.RetryTransactionProvider;
* ...
*
* @Autowired
* private RetryTransactionProvider retryTransactionProvider;
* ...
*
* @RetryTransaction
* @Transactional
* public void doSomething() {
* ...
* final int attempt = this.retryTransactionProvider.getAttemptNumber();
* ...
* }
*
*
*
* You can acquire also the singleton {@link RetryTransactionProvider} instance directly like this:
*
* import org.dellroad.stuff.spring.RetryTransactionAspect;
* import org.dellroad.stuff.spring.RetryTransactionProvider;
* ...
*
* @RetryTransaction
* @Transactional
* public void doSomething() {
* ...
* final RetryTransactionProvider rtp = RetryTransactionAspect.aspectOf();
* final int attempt = rtp.getAttemptNumber();
* ...
* }
*
*
*
* You can also invoke the retry logic directly (i.e., without going through a method woven with the aspect); see
* {@link RetryTransactionProvider#retry RetryTransactionProvider.retry()}.
*
* @see RetryTransactionProvider
* @see org.springframework.transaction.annotation.Transactional
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface RetryTransaction {
/**
* Default {@linkplain #maxRetries maximum number of retry attempts}, used when the {@link #maxRetries maxRetries}
* value is not explicitly set in an instance of this annotation.
* This default value can be overridden by configuring the {@code maxRetriesDefault} property on the aspect itself.
*/
int DEFAULT_MAX_RETRIES = 4;
/**
* Default {@linkplain #initialDelay initial delay}, in milliseconds, used when the {@link #initialDelay initialDelay}
* value is not explicitly set in an instance of this annotation.
* This default value can be overridden by configuring the {@code initialDelayDefault} property on the aspect itself.
*/
long DEFAULT_INITIAL_DELAY = 100;
/**
* Default {@linkplain #maximumDelay maximum delay}, in milliseconds, used when the {@link #maximumDelay maximumDelay}
* value is not explicitly set in an instance of this annotation.
* This default value can be overridden by configuring the {@code maximumDelayDefault} property on the aspect itself.
*/
long DEFAULT_MAXIMUM_DELAY = 30 * 1000;
/**
* The maximum number of transaction retry attempts.
*
*
* If the transaction fails, it will be retried at most this many times.
* This limit applies to retries only; it does not apply to the very first attempt, which is always made.
* So a value of zero means at most one attempt.
*
*
* If this property is not set explicitly, the default value of {@code -1} indicates that the aspect-wide default value
* ({@value #DEFAULT_MAX_RETRIES} by default), should be used.
*
* @return maximum number of transaction retry attempts
*/
int maxRetries() default -1;
/**
* The initial delay between retry attempts in milliseconds.
* After the first transaction failure, we will pause for approximately this many milliseconds.
* For additional failures we apply a randomized exponential back-off, up to a maximum of {@link #maximumDelay}.
*
*
* If this property is not set explicitly, the default value of {@code -1} indicates that the aspect-wide default value
* ({@value #DEFAULT_INITIAL_DELAY} milliseconds by default), should be used.
*
* @return initial delay between retry attempts in milliseconds
*/
long initialDelay() default -1;
/**
* The maximum delay between retry attempts in milliseconds.
* After the first transaction failure, we will pause for approximately {@link #initialDelay} milliseconds.
* For additional failures we apply a randomized exponential back-off, up to a maximum of this value.
*
*
* If this property is not set explicitly, the default value of {@code -1} indicates that the aspect-wide default value
* ({@value #DEFAULT_MAXIMUM_DELAY} milliseconds by default), should be used.
*
* @return maximum delay between retry attempts in milliseconds
*/
long maximumDelay() default -1;
}