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}