All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gfccollective.concurrent.ScalaFutures.scala Maven / Gradle / Ivy

The newest version!
package org.gfccollective.concurrent

import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.{AtomicReference, AtomicInteger}
import scala.annotation.tailrec
import scala.collection.generic.CanBuildFrom
import scala.concurrent.duration._
import scala.concurrent.{Future, ExecutionContext, Promise}
import scala.util.control.NonFatal
import scala.util.{Random, Success, Failure, Try}

/**
 * Little helpers for scala futures
 *
 * @author Gregor Heine
 * @since 11/Jul/2014 13:25
 */
object ScalaFutures {
  private val FiniteDurationMax = FiniteDuration(Long.MaxValue, TimeUnit.NANOSECONDS)

  implicit class FutureOps[A](val f: Future[A]) extends AnyVal {
    /**
     * Create a new Future that times out with a [[java.util.concurrent.TimeoutException]] after the given FiniteDuration
     */
    def withTimeout(after: FiniteDuration)(implicit ec: ExecutionContext): Future[A] = withTimeout(after, None)

    /**
     * Create a new Future that times out with a [[java.util.concurrent.TimeoutException]] after the given FiniteDuration.
     * If errorMessage is provided it is used when building the TimeoutException.
     */
    def withTimeout(after: FiniteDuration, errorMessage: Option[String])(implicit ec: ExecutionContext): Future[A] =
      Future.firstCompletedOf(Seq(f, Timeouts.timeout(after, errorMessage)))
  }

  implicit class AsFuture[A](val a: A) extends AnyVal {
    @inline def asFuture: Future[A] = Future.successful(a)
  }

  implicit class FutureTryOps[A](val f: Future[Try[A]]) extends AnyVal {
    def tryFlatten(implicit ec: ExecutionContext): Future[A] = f.flatMap(fromTry)
  }

  // Obsolete in scala 2.12 thanks to the addition of Future.flatten
  implicit class FutureFutureOps[A](val f: Future[Future[A]]) extends AnyVal {
    def flatten(implicit ec: ExecutionContext): Future[A] = f.flatMap(identity)
  }

  /**
   * Asynchronously tests whether a predicate holds for some of the elements of a collection of futures
   */
  def exists[T](futures: Iterable[Future[T]])
               (predicate: T => Boolean)
               (implicit executor: ExecutionContext): Future[Boolean] = {
    if (futures.isEmpty) Future.successful(false)
    else Future.find(futures.toSeq)(predicate).map(_.isDefined)
  }

  /**
   * Asynchronously tests whether a predicate holds for all elements of a collection of futures
   */
  def forall[T](futures: Iterable[Future[T]])
               (predicate: T => Boolean)
               (implicit executor: ExecutionContext): Future[Boolean] = {
    if (futures.isEmpty) Future.successful(true)
    else Future.find(futures.toSeq)(!predicate(_)).map(_.isEmpty)
  }

  /**
   * Future of an empty Option
   */
  val FutureNone: Future[Option[Nothing]] = Future.successful(None)

  /**
   * Convert a Try into a Future
   */
  def fromTry[T](t: Try[T]): Future[T] = t match {
    case Success(s) => Future.successful(s)
    case Failure(e) => Future.failed(e)
  }

  /**
   * Turn Exceptions thrown by f into a failed Future
   */
  def safely[T](f: => Future[T]): Future[T] = Try(f) match {
    case Success(s) => s
    case Failure(e) => Future.failed(e)
  }

  /**
   * Improved version of [[scala.concurrent.Future.fold]], that fails the resulting Future as soon as one of the input Futures fails.
   */
  def foldFast[T, R >: T](futures: TraversableOnce[Future[T]])(zero: R)(foldFun: (R, T) => R)(implicit executor: ExecutionContext): Future[R] = {
    if (futures.isEmpty) Future.successful(zero)
    else {
      val atomic = new AtomicReference[R](zero)
      val promise = Promise[R]()
      val counter = new AtomicInteger()

      @tailrec def update(f: R => R): R = {
        val oldValue = atomic.get()
        val newValue = f(oldValue)
        if (atomic.compareAndSet(oldValue, newValue)) newValue else update(f)
      }

      futures.foreach { _.onComplete {
        // succeed slow: only succeed when all futures have succeeded
        case Success(v) =>
          update(foldFun(_, v))
          if (counter.incrementAndGet() == futures.size) {
            promise.trySuccess(atomic.get)
          }
        // fail fast: fail as soon as the first future has failed
        case Failure(t) =>
          promise.tryFailure(t)
      }}

      promise.future
    }
  }

  /**
    *Version of [[scala.concurrent.Future.traverse]], that performs a sequential rather than a parallel map
    */
  import scala.language.higherKinds
  def traverseSequential[A, B, M[X] <: TraversableOnce[X]](in: M[A])(fn: A => Future[B])(implicit cbf: CanBuildFrom[M[A], B, M[B]], executor: ExecutionContext): Future[M[B]] =
    in.foldLeft(Future.successful(cbf(in))) { (fr, a) =>
      for { r <- fr
            b <- fn(a)
      } yield (r += b)
    }.map(_.result())

  /**
   * Retries a Future until it succeeds or a maximum number of retries has been reached.
   *
   * @param maxRetryTimes The maximum number of retries, defaults to Long.MaxValue. The future f is triggered at most maxRetryTimes + 1 times.
   *                      In other words, iff maxRetryTimes == 0, f will be called exactly once, iff maxRetryTimes == 1, it will be called at
   *                      most twice, etc.
   * @param f A function that returns a new Future
   * @param ec The ExecutionContext on which to retry the Future if it failed.
   * @param log An optional log function to report failed iterations to. By default prints the thrown Exception to the console.
   * @return A successful Future if the Future succeeded within maxRetryTimes or a failed Future otherwise.
   */
  def retry[T](maxRetryTimes: Long = Long.MaxValue)
              (f: => Future[T])
              (implicit ec: ExecutionContext,
                        log: Throwable => Unit = Implicits.NoLog): Future[T] = {
    safely(f).recoverWith {
      case NonFatal(e) if maxRetryTimes > 0 =>
        log(e)
        retry(maxRetryTimes - 1)(f)
    }
  }


  /**
   * Retries a Future until it succeeds or a maximum number of retries has been reached, or a retry timeout
   * has been reached. Each retry iteration is being exponentially delayed. The delay grows from a given start value
   * and by a given factor until it reaches a given maximum delay value. If maxRetryTimeout is reached, the last
   * Future is scheduled at the point of the timeout. E.g. if the initial delay is 1 second, the retry timeout 10 seconds
   * and all other parameters at their default, the future will be retried after 1, 3 (=1+2), 7 (=1+2+4) and 10 seconds before it fails.
   * The actual delay between iterations is subject to jitter randomization. For more background on the subject of jitter see
   * http://www.awsarchitectureblog.com/2015/03/backoff.html
   * Optionally, jitter can be disabled, in which case the delay interval follows the strict exponential propagation as outlined above.
   *
   * @param maxRetryTimes The maximum number of retries, defaults to Long.MaxValue. The future f is triggered at most maxRetryTimes + 1 times.
   *                      In other words, iff maxRetryTimes == 0, f will be called exactly once, iff maxRetryTimes == 1, it will be called at
   *                      most twice, etc.
   * @param maxRetryTimeout The retry Deadline until which to retry the Future, defaults to 1 day from now
   * @param initialDelay The initial delay value, defaults to 1 nanosecond
   * @param maxDelay The maximum delay value, defaults to 1 day
   * @param exponentFactor The factor by which the delay increases between retry iterations
   * @param jitter Enable jitter to randomize the delay, defaults to true.
   * @param f A function that returns a new Future
   * @param ec The ExecutionContext on which to retry the Future if it failed.
   * @param log An optional log function to report failed iterations to. It defaults to no-op.
   * @return A successful Future if the Future succeeded within maxRetryTimes or a failed Future otherwise.
   */
  def retryWithExponentialDelay[T](maxRetryTimes: Long = Long.MaxValue,
                                   maxRetryTimeout: Deadline = 1.day.fromNow,
                                   initialDelay: Duration = 1.millisecond,
                                   maxDelay: FiniteDuration = 1.day,
                                   exponentFactor: Double = 2d,
                                   jitter: Boolean = true)
                                  (f: => Future[T])
                                  (implicit ec: ExecutionContext,
                                   log: Throwable => Unit = Implicits.NoLog): Future[T] = {
    require(exponentFactor >= 1)
    safely(f).recoverWith {
      case NonFatal(e) if (maxRetryTimes > 0 && maxRetryTimeout.timeLeft.toMillis > 1) =>
        log(e)
        val p = Promise[T]
        val delayLimit = maxDelay.min(maxRetryTimeout.timeLeft)
        val jitteredDelay = {
          if (jitter) {
            initialDelay * Random.nextDouble
          } else {
            initialDelay
          }
        }
        val delay: FiniteDuration = jitteredDelay.min(delayLimit) match {
          case fd: FiniteDuration => fd
          case _ => FiniteDurationMax
        }
        Timeouts.scheduledExecutor.schedule(delay) {
          p.completeWith(retryWithExponentialDelay(maxRetryTimes - 1,
                                                   maxRetryTimeout,
                                                   initialDelay.min(delayLimit) * exponentFactor,
                                                   maxDelay,
                                                   exponentFactor,
                                                   jitter)(
                                                   f)
          )
        }

        p.future
    }
  }

  object Implicits {
    implicit val sameThreadExecutionContext = SameThreadExecutionContext
    implicit val NoLog: Throwable => Unit = _ => {}
    implicit val ConsoleLog: Throwable => Unit = _.printStackTrace
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy