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

monix.execution.CancelablePromise.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014-2019 by The Monix Project Developers.
 * See the project homepage at: https://monix.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package monix.execution

import monix.execution.atomic.PaddingStrategy.NoPadding
import monix.execution.internal.Platform
import monix.execution.atomic.{AtomicAny, PaddingStrategy}

import scala.annotation.tailrec
import scala.collection.immutable.LongMap
import scala.collection.mutable.ListBuffer
import scala.concurrent.Promise
import scala.util.{Failure, Success, Try}
import scala.util.control.NonFatal

/**
  * `CancelablePromise` is a [[scala.concurrent.Promise]] implementation that
  * allows listeners to unsubscribe from receiving future results.
  *
  * It does so by:
  *
  *  - adding a low-level [[subscribe]] method, that allows for callbacks
  *    to be subscribed
  *  - returning [[CancelableFuture]] in its [[future]] method implementation,
  *    allowing created future objects to unsubscribe (being the high-level
  *    [[subscribe]] that should be preferred for most usage)
  *
  * Being able to unsubscribe listeners helps with avoiding memory leaks
  * in case of listeners or futures that are being timed-out due to
  * promises that take a long time to complete.
  *
  * @see [[subscribe]]
  * @see [[future]]
  */
sealed abstract class CancelablePromise[A] extends Promise[A] {
  /**
    * Returns a future that can unsubscribe from this promise's
    * notifications via cancelation.
    *
    * {{{
    *   val promise = CancelablePromise[Int]()
    *
    *   val future1 = promise.future
    *   val future2 = promise.future
    *
    *   for (r <- future1) println(s"Future1 completed with: $$r")
    *   for (r <- future2) println(s"Future2 completed with: $$r")
    *
    *   // Unsubscribing from the future notification, but only for future1
    *   future1.cancel()
    *
    *   // Completing our promise
    *   promise.success(99)
    *
    *   //=> Future2 completed with: 99
    * }}}
    *
    * Note that in the above example `future1` becomes non-terminating
    * after cancellation. By unsubscribing its listener, it will never
    * complete.
    *
    * This helps with avoiding memory leaks for futures that are being
    * timed-out due to promises that take a long time to complete.
    */
  def future: CancelableFuture[A]

  /** Low-level subscription method that registers a callback to be called
    * when this promise will complete.
    *
    * {{{
    *   val promise = CancelablePromise[Int]()
    *
    *   def subscribe(n: Int): Cancelable =
    *     promise.subscribe {
    *       case Success(str) =>
    *         println(s"Callback ($$n) completed with: $$str")
    *       case Failure(e) =>
    *         println(s"Callback ($$n) completed with: $$e")
    *     }
    *
    *   val token1 = subscribe(1)
    *   val token2 = subscribe(2)
    *
    *   // Unsubscribing from the future notification
    *   token1.cancel()
    *
    *   // Completing our promise
    *   promise.success(99)
    *
    *   //=> Callback (2) completed with: 99
    * }}}
    *
    * '''UNSAFE PROTOCOL:''' the implementation does not protect
    * against stack-overflow exceptions. There's no point in doing it
    * for such low level methods, because this is useful as middleware
    * and different implementations will have different ways to deal
    * with stack safety (e.g. `monix.eval.Task`).
    *
    * @param cb is a callback that will be called when the promise
    *        completes with a result, assuming that the returned
    *        cancelable token isn't canceled
    *
    * @return a cancelable token that can be used to unsubscribe the
    *         given callback, in order to prevent memory leaks, at
    *         which point the callback will never be called (if it
    *         wasn't called already)
    */
  def subscribe(cb: Try[A] => Unit): Cancelable
}

object CancelablePromise {
  /**
    * Builds an empty [[CancelablePromise]] object.
    *
    * @tparam A the type of the value in the promise
    *
    * @param ps is a configurable
    *        [[monix.execution.atomic.PaddingStrategy PaddingStrategy]]
    *        to avoid the false sharing problem
    *
    * @return the newly created promise object
    */
  def apply[A](ps: PaddingStrategy = NoPadding): CancelablePromise[A] =
    new Async[A](ps)

  /** Creates an already completed [[CancelablePromise]] with the specified
    * result or exception.
    *
    * @param value is the [[scala.util.Try]] result to signal to subscribers
    * @return the newly created promise object
    */
  def fromTry[A](value: Try[A]): CancelablePromise[A] =
    new Completed[A](value)

  /** Creates a [[CancelablePromise]] that's already completed with the
    * given successful result.
    *
    * @param value is the successful result to signal to subscribers
    * @return the newly created promise object
    */
  def successful[A](value: A): CancelablePromise[A] =
    fromTry(Success(value))

  /** Creates a [[CancelablePromise]] that's already completed in error.
    *
    * @param e is the error to signal to subscribers
    * @return the newly created promise object
    */
  def failed[A](e: Throwable): CancelablePromise[A] =
    fromTry(Failure(e))

  /**
    * Implements already completed `CancelablePromise` references.
    */
  private final class Completed[A](value: Try[A]) extends CancelablePromise[A] {
    def future: CancelableFuture[A] =
      CancelableFuture.fromTry(value)

    def subscribe(cb: Try[A] => Unit): Cancelable = {
      cb(value)
      Cancelable.empty
    }

    def isCompleted: Boolean = true
    def tryComplete(result: Try[A]): Boolean = false
  }

  /**
    * Actual implementation for `CancelablePromise`.
    */
  private final class Async[A](ps: PaddingStrategy) extends CancelablePromise[A] { self =>
    // States:
    //  - Try[A]: completed with a result
    //  - MapQueue: listeners queue
    private[this] val state = AtomicAny.withPadding[AnyRef](emptyMapQueue, ps)

    override def subscribe(cb: Try[A] => Unit): Cancelable =
      unsafeSubscribe(cb)

    override def isCompleted: Boolean =
      state.get.isInstanceOf[Try[_]]

    override def future: CancelableFuture[A] =
      state.get match {
        case ref: Try[A] @unchecked =>
          CancelableFuture.fromTry(ref)
        case queue: MapQueue[AnyRef] @unchecked =>
          // Optimization over straight usage of `unsafeSubscribe`,
          // to avoid an extra `ref.get`
          val p = Promise[A]()
          val (id, update) = queue.enqueue(p)
          val cancelable =
            if (!state.compareAndSet(queue, update)) unsafeSubscribe(p)
            else new IdCancelable(id)
          CancelableFuture(p.future, cancelable)
      }

    @tailrec
    override def tryComplete(result: Try[A]): Boolean = {
      state.get match {
        case queue: MapQueue[AnyRef] @unchecked =>
          if (!state.compareAndSet(queue, result)) {
            // Failed, retry...
            tryComplete(result)
          } else if (queue eq emptyMapQueue) true
          else {
            var errors: ListBuffer[Throwable] = null
            val cursor = queue.iterator

            while (cursor.hasNext) {
              val cb = cursor.next()
              try {
                call(cb, result)
              } catch {
                // Aggregating errors, because we want to throw them all
                // as a composite and to prevent listeners from spoiling
                // the fun for other listeners by throwing errors
                case e if NonFatal(e) =>
                  if (errors eq null) errors = ListBuffer.empty
                  errors += e
              }
            }
            if (errors ne null) {
              // Throws all errors as a composite
              val x :: xs = errors.toList
              throw Platform.composeErrors(x, xs: _*)
            }
            true
          }
        case _ =>
          false
      }
    }

    private def call(cb: AnyRef, result: Try[A]): Unit =
      cb match {
        case f: (Try[A] => Unit) @unchecked =>
          f(result)
        case p: Promise[A] =>
          p.complete(result)
      }

    @tailrec def unsafeSubscribe(cb: AnyRef): Cancelable =
      state.get match {
        case ref: Try[A] @unchecked =>
          call(cb, ref)
          Cancelable.empty

        case queue: MapQueue[Any] @unchecked =>
          val (id, update) = queue.enqueue(cb)
          if (!state.compareAndSet(queue, update)) unsafeSubscribe(cb)
          else new IdCancelable(id)
      }

    private final class IdCancelable(id: Long) extends Cancelable {
      @tailrec def cancel(): Unit =
        state.get match {
          case queue: MapQueue[_] =>
            if (!state.compareAndSet(queue, queue.dequeue(id)))
              cancel()
          case _ =>
            ()
        }
    }
  }

  private final case class MapQueue[+A](map: LongMap[A], nextId: Long) extends Iterable[A] {

    def enqueue[AA >: A](elem: AA): (Long, MapQueue[AA]) =
      (nextId, MapQueue(map.updated(nextId, elem), nextId + 1))

    def dequeue(id: Long): MapQueue[A] =
      MapQueue(map - id, nextId)

    def iterator: Iterator[A] =
      map.valuesIterator
  }

  private[this] val emptyMapQueue: MapQueue[Nothing] =
    MapQueue(LongMap.empty, 0)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy