Automatic retry of transactions/requests in Dropwizard/JPA/Hibernate

爱⌒轻易说出口 提交于 2021-02-10 05:14:51

问题


I am currently implementing a REST API web service using the Dropwizard framework together with dropwizard-hibernate respectively JPA/Hibernate (using a PostgreSQL database). I have a method inside a resource which I annotated with @UnitOfWork to get one transaction for the whole request. The resource method calls a method of one of my DAOs which extends AbstractDAO<MyEntity> and is used to communicate retrieval or modification of my entities (of type MyEntity) with the database.

This DAO method does the following: First it selects an entity instance and therefore a row from the database. Afterwards, the entity instance is inspected and based on its properties, some of its properties can be altered. In this case, the row in the database should be updated. I didn't specify anything else regarding caching, locking or transactions anywhere, so I assume the default is some kind of optimistic locking mechanism enforced by Hibernate. Therefore (I think), when deleting the entity instance in another thread after selecting it from the database in the current one, a StaleStateException is thrown when trying to commit the transaction because the entity instance which should be updated has been deleted before by the other thread.

When using the @UnitOfWork annotation, my understanding is that I'm not able to catch this exception, neither in the DAO method nor in the resource method. I could now implement an ExceptionMapper<StaleStateException> for Jersey to deliver a HTTP 503 response with a Retry-After header or something like that to the client to tell it to retry its request. But I'd rather first like to retry to request/transaction (which is basically the same here because of the @UnitOfWork annotation) while still on the server.

Is there any example implementation for a server-sided transaction retry mechanism when using Dropwizard? Like retrying a configurable amount of times (e.g. 3) and then failing with an exception/HTTP 503 response. How would you implement this? First thing that came to my mind is another annotation like @Retry(exception = StaleStateException.class, count = 3) which I could add to my resource. Any suggestions on this? Or is there an alternative solution to my problem considering different locking/transaction-related things?


回答1:


Alternative approach to this is to use an injection framework - in my case guice - and use method interceptors for this. This is a more generic solution.

DW integreates with guice very smoothly through https://github.com/xvik/dropwizard-guicey

I have a generic implementation that can retry any exception. It works, as yours, on an annotation, as follows:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {

}

The interceptor then does (with docs):

 /**
 * Abstract interceptor to catch exceptions and retry the method automatically.
 * Things to note:
 * 
 * 1. Method must be idempotent (you can invoke it x times without alterint the result) 
 * 2. Method MUST re-open a connection to the DB if that is what is retried. Connections are in an undefined state after a rollback/deadlock. 
 *    You can try and reuse them, however the result will likely not be what you expected 
 * 3. Implement the retry logic inteligently. You may need to unpack the exception to get to the original.
 * 
 * @author artur
 *
 */
public abstract class RetryInterceptor implements MethodInterceptor {

    private static final Logger log = Logger.getLogger(RetryInterceptor.class);

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        if(invocation.getMethod().isAnnotationPresent(Retry.class)) {
            int retryCount = 0;
            boolean retry = true;
            while(retry && retryCount < maxRetries()) {
                try {
                    return invocation.proceed();
                } catch(Exception e) {
                    log.warn("Exception occured while trying to executed method", e);
                    if(!retry(e)) {
                        retry = false;
                    } {
                        retryCount++;
                    }
                }
            }
        }
        throw new IllegalStateException("All retries if invocation failed");
    }

    protected boolean retry(Exception e) {
        return false;
    }

    protected int maxRetries() {
        return 0;
    }

}

A few things to note about this approach.

  • The retried method must be designed to be invoked multiple times without any result altering (e.g. if the method stores temporary results in forms of increments, then executing twice might increment twice)

  • Database exceptions are generally not save for retry. They must open a new connection (in particular when retrying deadlocks which is my case)

Other than that this base implementation simply catches anything and then delegates the retry count and detection to the implementing class. For example, my specific deadlock retry interceptor:

public class DeadlockRetryInterceptor extends RetryInterceptor {

    private static final Logger log = Logger.getLogger(MsRetryInterceptor.class);

    @Override
    protected int maxRetries() {
        return 6;
    }

    @Override
    protected boolean retry(Exception e) {
        SQLException ex = unpack(e);
        if(ex == null) {
            return false;
        }
        int errorCode = ex.getErrorCode();
        log.info("Found exception: " + ex.getClass().getSimpleName() + " With error code: " + errorCode, ex);
        return errorCode == 1205;
    }

    private SQLException unpack(final Throwable t) {
        if(t == null) {
            return null;
        }

        if(t instanceof SQLException) {
            return (SQLException) t;
        }

        return unpack(t.getCause());
    }
}

And finally, i can bind this to guice by doing:

bindInterceptor(Matchers.any(), Matchers.annotatedWith(Retry.class), new MsRetryInterceptor());

Which checks any class, and any method annotated with retry.

An example method for retry would be:

    @Override
    @Retry
    public List<MyObject> getSomething(int count, String property) {
        try(Connection con = datasource.getConnection();
                Context c = metrics.timer(TIMER_NAME).time()) 
        {
            // do some work
            // return some stuff
        } catch (SQLException e) {
            // catches exception and throws it out
            throw new RuntimeException("Some more specific thing",e);
        }
    }

The reason I need an unpack is that old legacy cases, like this DAO impl, already catch their own exceptions.

Note also how the method (a get) retrieves a new connection when invoked twice from my datasource pool, and how no modifications are done inside it (hence: safe to retry)

I hope that helps.

You can do similar things by implementing ApplicationListeners or RequestFilters or similar, however I think this is a more generic approach that could retry any kind of failure on any method that is guice bound.

Also note that guice can only intercept methods when it constructs the class (inject annotated constructor etc.)

Hope that helps,

Artur




回答2:


I found a pull request in the Dropwizard repository that helped me. It basically enables the possibility of using the @UnitOfWork annotation on other than resource methods.

Using this, I was able to detach the session opening/closing and transaction creation/committing lifecycle from the resource method by moving the @UnitOfWork annotation from the resource method to the DAO method which is responsible for the data manipulation which causes the StaleStateException. Then I was able to build a retry mechanism around this DAO method.

Examplary explanation:

// class MyEntityDAO extends AbstractDAO<MyEntity>
@UnitOfWork
void tryManipulateData() {
    // Due to optimistic locking, this operations cause a StaleStateException when
    // committed "by the @UnitOfWork annotation" after returning from this method.
}

// Retry mechanism, implemented wheresoever.
void manipulateData() {
    while (true) {
        try {
            retryManipulateData();
        } catch (StaleStateException e) {
            continue; // Retry.
        }
        return;
    }
}

// class MyEntityResource
@POST
// ...
// @UnitOfWork can also be used here if nested transactions are desired.
public Response someResourceMethod() {
    // Call manipulateData() somehow.
}

Of course one could also attach the @UnitOfWork annotation rather on a method inside a service class which makes use of the DAOs instead of directly applying it to a DAO method. In whatever class the annotation is used, remember to create a proxy of the instances with the UnitOfWorkAwareProxyFactory as described in the pull request.



来源:https://stackoverflow.com/questions/39949017/automatic-retry-of-transactions-requests-in-dropwizard-jpa-hibernate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!