/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.retry;

import alluxio.conf.Configuration;
import alluxio.conf.PropertyKey;
import alluxio.exception.runtime.AlluxioRuntimeException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.Callable;

/**
 * Utilities for performing retries.
 */
public final class RetryUtils {
  private static final Logger LOG = LoggerFactory.getLogger(RetryUtils.class);

  /**
   * Retries the given method until it doesn't throw an IO exception or the retry policy expires. If
   * the retry policy expires, the last exception generated will be rethrown. If no retry succeeds
   * then a default IO Exception will be thrown.
   *
   * @param action a description of the action that fits the phrase "Failed to ${action}"
   * @param f the function to retry
   * @param policy the retry policy to use
   */
  public static void retry(String action, RunnableThrowsIOException f, RetryPolicy policy)
      throws IOException {
    IOException e = null;
    while (policy.attempt()) {
      try {
        f.run();
        return;
      } catch (IOException ioe) {
        e = ioe;
        LOG.debug("Failed to {} (attempt {}): {}", action, policy.getAttemptCount(), e.toString());
      }
    }
    if (e != null) {
      throw e;
    }
    throw new IOException(String.format("Failed to run action %s after %d attempts",
        action, policy.getAttemptCount()));
  }

  /**
   * Notice we assume the exception with the method is retryable, so we are not wasting time here.
   * Retries the given retryable method until it succeed or the retry policy expires. If the retry
   * policy expires, the last exception generated will be rethrown as a AlluxioRuntimeException.
   *
   * @param description a description of the function
   * @param f the function to retry which returns value
   * @param policy the retry policy to use
   * @param <V> result type returned by callable
   * @return callable result
   */
  public static <V> V retryCallable(String description, Callable<V> f, RetryPolicy policy) {
    Exception cause = null;
    while (policy.attempt()) {
      try {
        return f.call();
      } catch (Exception e) {
        LOG.warn("Failed to {} (attempt {}): {}", description, policy.getAttemptCount(),
            e.toString());
        if (e instanceof AlluxioRuntimeException && !((AlluxioRuntimeException) e).isRetryable()) {
          throw AlluxioRuntimeException.from(e);
        }
        cause = e;
      }
    }
    throw AlluxioRuntimeException.from(cause);
  }

  /**
   * Gives a default ClientRetry based on config.
   * @return default Client Retry policy based on config
   */
  public static RetryPolicy defaultClientRetry() {
    return ExponentialTimeBoundedRetry.builder()
        .withMaxDuration(Configuration.getDuration(PropertyKey.USER_RPC_RETRY_MAX_DURATION))
        .withInitialSleep(Configuration.getDuration(PropertyKey.USER_RPC_RETRY_BASE_SLEEP_MS))
        .withMaxSleep(Configuration.getDuration(PropertyKey.USER_RPC_RETRY_MAX_SLEEP_MS))
        .build();
  }

  /**
   * @return the default worker to master client retry
   */
  public static RetryPolicy defaultWorkerMasterClientRetry() {
    return ExponentialTimeBoundedRetry.builder()
        .withMaxDuration(Configuration.getDuration(PropertyKey.WORKER_MASTER_CONNECT_RETRY_TIMEOUT))
        .withInitialSleep(Duration.ofMillis(100))
        .withMaxSleep(Duration.ofSeconds(5))
        .build();
  }

  /**
   * Interface for methods which return nothing and may throw IOException.
   */
  @FunctionalInterface
  public interface RunnableThrowsIOException {
    /**
     * Runs the runnable.
     */
    void run() throws IOException;
  }

  private RetryUtils() {} // prevent instantiation
}
