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

monix.reactive.observers.ConnectableSubscriber.scala Maven / Gradle / Ivy

Go to download

Sub-module of Monix, exposing the Observable pattern for modeling of reactive streams. See: https://monix.io

The newest version!
/*
 * Copyright (c) 2014-2021 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.reactive.observers

import monix.execution.Ack.{Continue, Stop}
import monix.execution.{Ack, CancelableFuture, Scheduler}
import monix.reactive.Observable

import scala.collection.mutable
import scala.concurrent.{Future, Promise}
import scala.util.{Failure, Success}

/** Wraps a [[Subscriber]] into an implementation that abstains from emitting items until the call
  * to `connect()` happens. Prior to `connect()` you can enqueue
  * events for delivery once `connect()` happens, but before any items
  * emitted by `onNext` / `onComplete` and `onError`.
  *
  * Example: {{{
  *   import monix.execution.Ack.Continue
  *   import monix.execution.Scheduler.Implicits.global
  *
  *   val subscriber = new Subscriber[String] {
  *     val scheduler = global
  *     def onNext(a: String) = {
  *       println(s"Received: $$a")
  *       Continue
  *     }
  *     def onError(e: Throwable) =
  *       println(s"Error: $$e")
  *     def onComplete() =
  *       println("Completed!")
  *   }
  *
  *   val out = ConnectableSubscriber(subscriber)
  *
  *   // schedule onNext event, after connect()
  *   out.onNext("c")
  *
  *   // schedule event "a" to be emitted first
  *   out.pushFirst("a")
  *   // schedule event "b" to be emitted second
  *   out.pushFirst("b")
  *
  *   // underlying observer now gets events "a", "b", "c" in order
  *   out.connect()
  * }}}
  *
  * Example of an observer ended in error: {{{
  *   val out2 = ConnectableSubscriber(subscriber)
  *
  *   // schedule onNext event, after connect()
  *   out2.onNext("c")
  *
  *   out2.pushFirst("a") // event "a" to be emitted first
  *   out2.pushFirst("b") // event "b" to be emitted second
  *
  *   // schedule an onError sent downstream, once connect()
  *   // happens, but after "a" and "b"
  *   out2.pushError(new RuntimeException())
  *
  *   // underlying observer receives ...
  *   // onNext("a") -> onNext("b") -> onError(RuntimeException)
  *   out2.connect()
  *
  *   // NOTE: that onNext("c") never happens
  * }}}
  */
final class ConnectableSubscriber[-A] private (underlying: Subscriber[A]) extends Subscriber[A] { self =>

  implicit val scheduler: Scheduler =
    underlying.scheduler

  // MUST BE synchronized by `self`, only available if isConnected == false
  private[this] var queue = mutable.ArrayBuffer.empty[A]
  // MUST BE synchronized by `self`, only available if isConnected == false
  private[this] var scheduledDone = false
  // MUST BE synchronized by `self`, only available if isConnected == false
  private[this] var scheduledError = null: Throwable
  // MUST BE synchronized by `self`
  private[this] var isConnectionStarted = false
  // MUST BE synchronized by `self`, as long as isConnected == false
  private[this] var wasCanceled = false

  // Promise guaranteed to be fulfilled once isConnected is
  // seen as true and used for back-pressure.
  // MUST BE synchronized by `self`, only available if isConnected == false
  private[this] var connectedPromise = Promise[Ack]()
  private[this] var connectedFuture = connectedPromise.future

  // Volatile that is set to true once the buffer is drained.
  // Once visible as true, it implies that the queue is empty
  // and has been drained and thus the onNext/onError/onComplete
  // can take the fast path
  @volatile private[this] var isConnected = false

  // Only accessible in `connect()`
  private[this] var connectionRef: CancelableFuture[Ack] = _

  /** Connects the underling observer to the upstream publisher.
    *
    * This function should be idempotent. Calling it multiple times
    * should have the same effect as calling it once.
    */
  def connect(): CancelableFuture[Ack] =
    self.synchronized {
      if (!isConnected && !isConnectionStarted) {
        isConnectionStarted = true
        val bufferWasDrained = Promise[Ack]()

        val cancelable = Observable
          .fromIterable(queue)
          .unsafeSubscribeFn(new Subscriber[A] {
            implicit val scheduler = underlying.scheduler
            private[this] var ack: Future[Ack] = Continue

            bufferWasDrained.future.onComplete {
              case Success(Continue) =>
                connectedPromise.success(Continue)
                isConnected = true
                // GC relief
                queue = null
                connectedPromise = null
                // This might be a race condition problem, but it only
                // matters for GC relief purposes
                connectionRef = CancelableFuture.successful(Continue)

              case Success(Stop) =>
                wasCanceled = true
                connectedPromise.success(Stop)
                isConnected = true
                // GC relief
                queue = null
                connectedPromise = null
                // This might be a race condition problem, but it only
                // matters for GC relief purposes
                connectionRef = CancelableFuture.successful(Stop)

              case Failure(ex) =>
                wasCanceled = true
                connectedPromise.failure(ex)
                isConnected = true
                // GC relief
                queue = null
                connectedPromise = null
                // This might be a race condition problem, but it only
                // matters for GC relief purposes
                connectionRef = CancelableFuture.failed(ex)
            }

            def onNext(elem: A): Future[Ack] = {
              ack = underlying.onNext(elem).syncOnStopFollow(bufferWasDrained, Stop)
              ack
            }

            def onComplete(): Unit = {
              if (!scheduledDone) {
                ack.syncOnContinue { bufferWasDrained.trySuccess(Continue); () }
              } else if (scheduledError ne null) {
                if (bufferWasDrained.trySuccess(Stop))
                  underlying.onError(scheduledError)
              } else if (bufferWasDrained.trySuccess(Stop))
                underlying.onComplete()
              ()
            }

            def onError(ex: Throwable): Unit = {
              if (scheduledError ne null)
                scheduler.reportFailure(ex)
              else {
                scheduledDone = true
                scheduledError = ex

                if (bufferWasDrained.trySuccess(Stop))
                  underlying.onError(ex)
                else
                  scheduler.reportFailure(ex)
              }
            }
          })

        connectionRef = CancelableFuture(bufferWasDrained.future, cancelable)
      }

      connectionRef
    }

  /** Schedule one element to be pushed to the underlying subscriber
    * when [[connect]] happens.
    *
    * The given elements are appended to a queue that will be
    * drained on [[connect]]. Afterwards no more elements are
    * allowed to be pushed in the queue.
    *
    * These elements are streamed before any elements that will
    * eventually get streamed with [[onNext]], because of
    * the applied back-pressure from `onNext`.
    */
  def pushFirst(elem: A): Unit =
    self.synchronized {
      if (isConnected || isConnectionStarted)
        throw new IllegalStateException("Observer was already connected, so cannot pushFirst")
      else if (!scheduledDone)
        queue += elem
      ()
    }

  /** Schedule elements to be pushed to the underlying subscriber
    * when [[connect]] happens.
    *
    * The given elements are appended to a queue that will be
    * drained on [[connect]]. Afterwards no more elements are
    * allowed to be pushed in the queue.
    *
    * These elements are streamed before any elements that will
    * eventually get streamed with [[onNext]], because of
    * the applied back-pressure from `onNext`.
    */
  def pushFirstAll[U <: A](xs: Iterable[U]): Unit =
    self.synchronized {
      if (isConnected || isConnectionStarted)
        throw new IllegalStateException("Observer was already connected, so cannot pushFirst")
      else if (!scheduledDone)
        queue.appendAll(xs)
      ()
    }

  /** Schedule a complete event when [[connect]] happens,
    * but before any elements scheduled with [[pushFirst]]
    * or [[pushFirstAll]].
    *
    * After `pushComplete` no more [[pushFirst]] or [[onNext]]
    * events are accepted.
    */
  def pushComplete(): Unit =
    self.synchronized {
      if (isConnected || isConnectionStarted)
        throw new IllegalStateException("Observer was already connected, so cannot pushFirst")
      else if (!scheduledDone) {
        scheduledDone = true
      }
    }

  /** Schedule an error event when [[connect]] happens,
    * but before any elements scheduled with [[pushFirst]]
    * or [[pushFirstAll]].
    *
    * After `pushError` no more [[pushFirst]] or [[onNext]]
    * events are accepted.
    */
  def pushError(ex: Throwable): Unit =
    self.synchronized {
      if (isConnected || isConnectionStarted)
        throw new IllegalStateException("Observer was already connected, so cannot pushFirst")
      else if (!scheduledDone) {
        scheduledDone = true
        scheduledError = ex
      }
    }

  /** The [[Subscriber.onNext]] method that pushes events to
    * the underlying subscriber.
    *
    * It will back-pressure by means of its `Future[Ack]` result
    * until [[connect]] happens and the underlying queue of
    * scheduled events have been drained.
    */
  def onNext(elem: A): Future[Ack] = {
    if (!isConnected) {
      // no need for synchronization here, since this reference is initialized
      // before the subscription happens and because it gets written only in
      // onNext / onComplete, which are non-concurrent clauses
      connectedFuture = connectedFuture.flatMap {
        case Continue => underlying.onNext(elem)
        case Stop => Stop
      }
      connectedFuture
    } else if (!wasCanceled) {
      // taking fast path
      underlying.onNext(elem)
    } else {
      // was canceled either during connect, or the upstream publisher
      // sent an onNext event after onComplete / onError
      Stop
    }
  }

  /** The [[Subscriber.onComplete]] method that pushes the
    * complete event to the underlying observer.
    *
    * It will wait for [[connect]] to happen and the queue of
    * scheduled events to be drained.
    */
  def onComplete(): Unit = {
    // we cannot take a fast path here
    connectedFuture.syncTryFlatten
      .syncOnContinue(underlying.onComplete())
    ()
  }

  /** The [[Subscriber.onError]] method that pushes an
    * error event to the underlying observer.
    *
    * It will wait for [[connect]] to happen and the queue of
    * scheduled events to be drained.
    */
  def onError(ex: Throwable): Unit = {
    // we cannot take a fast path here
    connectedFuture.syncTryFlatten
      .syncOnContinue(underlying.onError(ex))
    ()
  }
}

object ConnectableSubscriber {
  /** `ConnectableSubscriber` builder */
  def apply[A](subscriber: Subscriber[A]): ConnectableSubscriber[A] = {
    new ConnectableSubscriber[A](subscriber)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy