001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.fcrepo.common.db;
020
021import static org.slf4j.LoggerFactory.getLogger;
022
023import java.time.temporal.ChronoUnit;
024
025import org.slf4j.Logger;
026import org.springframework.beans.factory.annotation.Autowired;
027import org.springframework.dao.DeadlockLoserDataAccessException;
028import org.springframework.stereotype.Component;
029import org.springframework.transaction.support.TransactionTemplate;
030
031import net.jodah.failsafe.Failsafe;
032import net.jodah.failsafe.RetryPolicy;
033
034/**
035 * Wrapper around Spring's db transaction management
036 *
037 * @author pwinckles
038 */
039@Component
040public class DbTransactionExecutor {
041
042    private static final Logger LOGGER = getLogger(DbTransactionExecutor.class);
043
044    private static final RetryPolicy<Object> DB_RETRY = new RetryPolicy<>()
045            .handleIf(e -> {
046                return e instanceof DeadlockLoserDataAccessException
047                        || (e.getCause() != null && e.getCause() instanceof DeadlockLoserDataAccessException);
048            })
049            .onRetry(event -> {
050                LOGGER.debug("Retrying operation that failed with the following exception", event.getLastFailure());
051            })
052            .withBackoff(10, 100, ChronoUnit.MILLIS, 1.5)
053            .withJitter(0.1)
054            .withMaxRetries(10);
055
056    @Autowired
057    private TransactionTemplate transactionTemplate;
058
059    public DbTransactionExecutor() {
060
061    }
062
063    public DbTransactionExecutor(final TransactionTemplate transactionTemplate) {
064        this.transactionTemplate = transactionTemplate;
065    }
066
067    /**
068     * Executes the runnable within a DB transaction that will retry entire block on MySQL deadlock exceptions.
069     *
070     * @param action the code to execute
071     */
072    public void doInTxWithRetry(final Runnable action) {
073        Failsafe.with(DB_RETRY).run(() -> {
074            doInTx(action);
075        });
076    }
077
078    /**
079     * Executes the runnable within a DB transaction. MySQL deadlock exceptions are NOT retried.
080     *
081     * @param action the code to execute
082     */
083    public void doInTx(final Runnable action) {
084        if (transactionTemplate == null) {
085            // If the transaction template is not set, just execute the code without a tx.
086            // This will never happen when configured by Spring, but is useful when unit testing
087            LOGGER.warn("Executing outside of a DB transaction");
088            action.run();
089        } else {
090            transactionTemplate.executeWithoutResult(status -> {
091                action.run();
092            });
093        }
094    }
095
096}