001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * 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.apache.oozie.util.db;
020
021import java.util.concurrent.Callable;
022
023import com.google.common.annotations.VisibleForTesting;
024import org.apache.oozie.util.XLog;
025
026import com.google.common.base.Preconditions;
027import com.google.common.base.Predicate;
028
029public class OperationRetryHandler {
030    private static XLog LOG = XLog.getLog(OperationRetryHandler.class);
031    @VisibleForTesting
032    static final RetryAttemptState RETRY_ATTEMPT_STATE = new RetryAttemptState();
033
034    private final int maxRetryCount;
035    private final long initialWaitTime;
036    private final long maxWaitTime;
037    private final Predicate<Throwable> retryPredicate;
038    private final boolean shouldRetry;
039
040    public OperationRetryHandler(final int maxRetryCount, final long initialWaitTime, final long maxWaitTime,
041                                 final Predicate<Throwable> retryPredicate) {
042        Preconditions.checkArgument(maxRetryCount >= 0, "Retry count must not be less than zero");
043        Preconditions.checkArgument(initialWaitTime > 0, "Initial wait time must be greater than zero");
044        Preconditions.checkArgument(maxWaitTime >= 0, "Maximum wait time must not be less than zero");
045
046        this.maxRetryCount = maxRetryCount;
047        this.initialWaitTime = initialWaitTime;
048        this.maxWaitTime = maxWaitTime;
049        this.retryPredicate = Preconditions.checkNotNull(retryPredicate, "Retry predicate must not be null");
050        this.shouldRetry = !(maxRetryCount == 0 || maxWaitTime == 0);
051
052        LOG.trace("Retry handler parameters are set." +
053                "[maxRetryCount={0};initialWaitTime={1};maxWaitTime={2};retryPredicate.class={3};shouldRetry={4}]",
054                this.maxRetryCount, this.initialWaitTime, this.maxWaitTime, this.retryPredicate.getClass().getName(), shouldRetry);
055    }
056
057    public <V> V executeWithRetry(final Callable<V> operation) throws Exception {
058        int retries = 0;
059        long waitTime = initialWaitTime;
060        Exception lastException = null;
061
062        if (!shouldRetry) {
063            try {
064                LOG.trace("Configured not to retry, calling operation once.");
065
066                final V result = operation.call();
067
068                LOG.trace("Operation called once successfully.");
069
070                return result;
071            }
072            catch (final Exception e) {
073                LOG.error("An error occurred while calling the operation once. [e.message={0}]", e.getMessage());
074                throw e;
075            }
076        }
077
078        try {
079            RETRY_ATTEMPT_STATE.signalStart();
080
081            while (retries < maxRetryCount) {
082                try {
083                    LOG.trace("Calling operation. [retries={0}]", retries);
084
085                    retries++;
086                    final V result = operation.call();
087
088                    LOG.trace("Operation called successfully.");
089
090                    return result;
091                } catch (final Exception e) {
092                    LOG.warn("Database error", e);
093
094                    // if retries have been done by an inner retry handler,
095                    // then we won't make any effort to do it again
096                    if (RETRY_ATTEMPT_STATE.isExhausted()) {
097                        LOG.error("Retry attempts have been exhausted. [e.message={0}]", e.getMessage());
098                        throw e;
099                    }
100
101                    if (retryPredicate.apply(e)) {
102                        LOG.trace("Exception is not on blacklist, handling retry. [retries={0};e.class={1}]",
103                                retries, e.getClass().getName());
104                        waitTime = handleRetry(waitTime, retries);
105                        lastException = e;
106                    }
107                    else {
108                        LOG.warn("Exception is on blacklist, not handling retry. [retries={0};e.class={1}]",
109                                retries, e.getClass().getName());
110                        throw e;
111                    }
112                }
113            }
114
115            LOG.error("Number of maximum retry attempts exhausted");
116            RETRY_ATTEMPT_STATE.signalExhausted(); // signal to possible outer retry handlers
117            throw lastException;
118        } finally {
119            RETRY_ATTEMPT_STATE.signalEnd();
120        }
121    }
122
123    private long handleRetry(long sleepBeforeRetryMs, final int retries) throws InterruptedException {
124        LOG.warn("Operation failed, sleeping {0} milliseconds before retry #{1}", sleepBeforeRetryMs, retries);
125        Thread.sleep(sleepBeforeRetryMs);
126        sleepBeforeRetryMs *=  2;
127
128        return sleepBeforeRetryMs > maxWaitTime ? maxWaitTime : sleepBeforeRetryMs;
129    }
130}