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

com.outbrain.ob1k.concurrent.scalaapi.ComposableFuture.scala Maven / Gradle / Ivy

package com.outbrain.ob1k.concurrent.scalaapi

import java.util.concurrent.{Callable, TimeUnit, TimeoutException}

import com.google.common.base.{Predicate, Supplier}
import com.outbrain.ob1k.concurrent.handlers._
import com.outbrain.ob1k.concurrent.{ComposableFuture => JavaComposableFuture, ComposableFutures =>
JavaComposableFutures, Consumer, Producer, Try => JavaTry}

import scala.collection.JavaConversions._
import scala.concurrent.duration.Duration
import scala.util.{Failure, Success, Try}


/**
 * A Scala friendly adaptation of the ob1k ComposableFuture API.
 * Inspired by Scala's native Future API.
 *
 * @author slevin
 * @since 26/02/15
 */
object ComposableFuture {

  implicit def toScalaTry[T](javaTry: JavaTry[T]): Try[T] = {
    if (javaTry.isSuccess) Success(javaTry.getValue)
    else Failure(javaTry.getError)
  }

  implicit def toJavaTry[T](scalaTry: Try[T]): JavaTry[T] = {
    scalaTry match {
      case Success(value) => JavaTry.fromValue(value)
      case Failure(error) => JavaTry.fromError(error)
    }
  }

  implicit def toScalaComposableFuture[T](composableFuture: JavaComposableFuture[T]): ComposableFuture[T] =
    ComposableFuture(composableFuture)

  implicit class ComposableFutureExtensions[T](future: ComposableFuture[T]) {

    /**
     * Sets an initial timeout for this future, and retries it with further timeouts in case it fails due to a timeout.
     */
    def retryWithTimeouts(initialTimeout: Duration, moreTimeouts: Duration*): ComposableFuture[T] = {

      def tryHarder(chain: => ComposableFuture[T], timeouts: Duration*) = timeouts.toList match {
        case Nil =>
          chain
        case timeout :: rest =>
          chain.recoverWith({
            case e: Throwable
              if e.isInstanceOf[TimeoutException] ||
                Option(e.getCause).exists(_.isInstanceOf[TimeoutException]) =>
              future.timeoutAfter(timeout)
          })
      }

      tryHarder(future.timeoutAfter(initialTimeout), moreTimeouts: _*)
    }

    /**
     * Retries this future for a given amount of times in case it fails.
     */
    def retry(count: Int): ComposableFuture[T] = JavaComposableFutures.retry(count, new FutureAction[T] {
      override def execute(): JavaComposableFuture[T] = future.future
    })
  }

  private def apply[T](aFuture: JavaComposableFuture[T]): ComposableFuture[T] = new ComposableFuture[T] {
    override val future = aFuture
  }

  def fromValue[T](value: T): ComposableFuture[T] = JavaComposableFutures.fromValueLazy(value)

  def fromError[T](error: Throwable): ComposableFuture[T] = JavaComposableFutures.fromErrorLazy[T](error)

  def fromTry[T](aTry: Try[T]): ComposableFuture[T] = aTry match {
    case Success(s) => JavaComposableFutures.fromValueLazy[T](s)
    case Failure(e) => JavaComposableFutures.fromErrorLazy[T](e)
  }

  /**
   * Repeatedly chains futures until a predicate is satisfied.
   *
   * @param oracle A function that provides the futures to chain
   * @param predicate A predicate to satisfy
   * @tparam T the future type
   * @return A future which is the futures chained until the predicate was true.
   */
  def recursive[T](oracle: => ComposableFuture[T], predicate: T => Boolean): ComposableFuture[T] = {
    JavaComposableFutures.recursive(new Supplier[JavaComposableFuture[T]] {
      override def get(): JavaComposableFuture[T] = oracle.future
    }, new Predicate[T] {
      override def apply(input: T): Boolean = predicate(input)
    })
  }

  def all[T](failOnError: Boolean, futures: ComposableFuture[T]*): ComposableFuture[List[T]] = {
    JavaComposableFutures.all(failOnError, futures.toList.map(_.future)).map(_.toList)
  }

  /**
   * Combines futures' values into a single future which contains the result of the future completed first.
   *
   * @param futures futures to combine
   * @tparam T the common type param of all futures.
   * @return A combined future
   */
  def any[T](futures: ComposableFuture[T]*): ComposableFuture[T] = {
    JavaComposableFutures.any[T](futures.toList.map(_.future))
  }

  def first[K, T](futures: Map[K, ComposableFuture[T]], numOfSuccess: Int): ComposableFuture[Map[K, T]] = {
    JavaComposableFutures.first[K, T](futures.mapValues(cf => cf.future), numOfSuccess).map {
      import scala.collection.JavaConverters._
      _.asScala.toMap
    }
  }

  /**
   * constructs a future that returns at most the first numOfSuccess successful values or whatever values
   * that was returned in the specified duration.
   * the input futures are given as a map so the response can indicate which of the futures actually returned.
   *
   * @param futures the input futures
   * @param numOfSuccess the min number of success values to wait for.
   * @param maxDuration the max time duration to wait for.
   * @tparam K the map key type
   * @tparam T the future type
   * @return a future of a map containing the accumulated results. 
   */
  def first[K, T](futures: Map[K, ComposableFuture[T]],
                  numOfSuccess: Int,
                  maxDuration: Duration): ComposableFuture[Map[K, T]] = {
    JavaComposableFutures.first[K, T](futures.mapValues(cf => cf.future),
                                      numOfSuccess,
                                      maxDuration.toNanos,
                                      TimeUnit.NANOSECONDS).map {
      import scala.collection.JavaConverters._
      _.asScala.toMap
    }
  }

  def submit[T](task: => T, useExecutor: Boolean = true): ComposableFuture[T] = {
    JavaComposableFutures.submitLazy(useExecutor, new Callable[T] {
      override def call(): T = task
    })
  }

  /**
   * constructs a new ComposableFuture using a producer function.
   * producer can supply a value(or an error) to a consumer function(Try[T] => Unit)
   * and should supply it at-most once.
   *
   * @param producer the producer function.
   * @tparam T the type of the future
   * @return the constructed future.
   */
  def build[T](producer: (Try[T] => Unit) => Unit): ComposableFuture[T] = {
    JavaComposableFutures.buildLazy[T](new Producer[T] {
      override def produce(consumer: Consumer[T]): Unit = producer(consumer.consume(_))
    })
  }

  /**
   * Creates a future that holds the result of a delayed execution of a task.
   *
   * @param task The task to be executed by the future.
   * @param delay The delay with which to execute the task.
   * @tparam T the future type
   * @return A future that holds the result of task's execution.
   */
  def schedule[T](task: => T, delay: Duration): ComposableFuture[T] = {
    JavaComposableFutures.scheduleLazy(new Callable[T] {
      override def call(): T = task
    }, delay.toNanos, TimeUnit.NANOSECONDS)
  }

  /**
   * Creates a future that holds the result of a delayed execution of a computation.
   *
   * @param computation The computation to be executed by the future.
   * @param delay The delay with which to execute the computation.
   * @tparam T the future type
   * @return A future that holds the result of computation's execution.
   */
  def scheduleWith[T](computation: => ComposableFuture[T], delay: Duration): ComposableFuture[T] = {
    JavaComposableFutures.scheduleFuture(new Callable[JavaComposableFuture[T]] {
      override def call(): JavaComposableFuture[T] = computation.future
    }, delay.toNanos, TimeUnit.NANOSECONDS)
  }
}

trait ComposableFuture[T] {

  import com.outbrain.ob1k.concurrent.scalaapi.ComposableFuture.{toScalaComposableFuture, toScalaTry}

  protected val future: JavaComposableFuture[T]

  def map[V](f: T => V): ComposableFuture[V] = future.continueOnSuccess[V](new SuccessHandler[T, V] {
    override def handle(result: T): V = f(result)
  })

  def flatMap[V](f: T => ComposableFuture[V]): ComposableFuture[V] = {
    future.continueOnSuccess(new FutureSuccessHandler[T, V] {
      override def handle(result: T): JavaComposableFuture[V] = {
        Try(f(result)) match {
          case Success(ft) => ft.future
          case Failure(e) => ComposableFuture.fromError(e).future
        }
      }
    })
  }

  def consume(processResult: Try[T] => Unit): Unit = future.consume(new Consumer[T] {
    override def consume(result: JavaTry[T]): Unit = {
      processResult(result)
    }
  })


  def foreach(f: T => Unit): Unit = consume {_ foreach f}

  /**
   * Sets a timeout for getting a result from this future.
   * If the timeout value is Duration.Inf this method does nothing.
   */
  def timeoutAfter(duration: Duration): ComposableFuture[T] = duration match {
    case Duration.Inf => this
    case timeout => future.withTimeout(timeout.toNanos, TimeUnit.NANOSECONDS)
  }

  def recover(pf: PartialFunction[Throwable, T]): ComposableFuture[T] = future.continueOnError(new ErrorHandler[T] {
    override def handle(error: Throwable): T = {
      Try(pf(error)).orElse(Failure(error)).get
    }
  })

  def recoverWith(pf: PartialFunction[Throwable, ComposableFuture[T]]): ComposableFuture[T] = {
    future.continueOnError(new FutureErrorHandler[T] {
      override def handle(result: Throwable): JavaComposableFuture[T] = {
        Try(pf(result)).getOrElse(ComposableFuture.fromError(result)).future
      }
    })
  }

  def materialize(): ComposableFuture[T] = future.materialize()

  def get(atMost: Duration = Duration.Inf): Try[T] = atMost match {
    case Duration.Inf => Try(future.get())
    case duration => Try(future.withTimeout(atMost.toNanos, TimeUnit.NANOSECONDS).get())
  }

  def asJavaComposableFuture(): JavaComposableFuture[T] = future
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy