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

com.twitter.io.Pipe.scala Maven / Gradle / Ivy

package com.twitter.io

import com.twitter.util.{Future, Promise, Return, Throw, Time, Timer}

/**
 * A synchronous in-memory pipe that connects [[Reader]] and [[Writer]] in the sense
 * that a reader's input is the output of a writer.
 *
 * A pipe is structured as a smash of both interfaces, a [[Reader]] and a [[Writer]] such that can
 * be passed directly to a consumer or a producer.
 *
 * {{{
 *   def consumer(r: Reader[Buf]): Future[Unit] = ???
 *   def producer(w: Writer[Buf]): Future[Unit] = ???
 *
 *   val p = new Pipe[Buf]
 *
 *   consumer(p)
 *   producer(p)
 * }}}
 *
 * Reads and writes on the pipe are matched one to one and only one outstanding `read`
 * or `write` is permitted in the current implementation (multiple pending writes or reads
 * resolve into `IllegalStateException` while leaving the pipe healthy). That is, the `write`
 * (its returned [[com.twitter.util.Future]]) is resolved when the `read` consumes the written data.
 *
 * Here is, for example, a very typical write-loop that writes into a pipe-backed [[Writer]]:
 *
 * {{{
 *   def writeLoop(w: Writer[Buf], data: List[Buf]): Future[Unit] = data match {
 *     case h :: t => p.write(h).before(writeLoop(w, t))
 *     case Nil => w.close()
 *   }
 * }}}
 *
 * Reading from a pipe-backed [[Reader]] is no different from working with any other reader:
 *
 *{{{
 *   def readLoop(r: Reader[Buf], process: Buf => Future[Unit]): Future[Unit] = r.read().flatMap {
 *     case Some(chunk) => process(chunk).before(readLoop(r, process))
 *     case None => Future.Done
 *   }
 * }}}
 *
 * == Thread Safety ==
 *
 * It is safe to call `read`, `write`, `fail`, `discard`, and `close` concurrently. The individual
 * calls are synchronized on the given [[Pipe]].
 *
 * == Closing or Failing Pipes ==
 *
 * Besides expecting a write or a read, a pipe can be closed or failed. A writer can do both `close`
 * and `fail` the pipe, while reader can only fail the pipe via `discard`.
 *
 * The following behavior is expected with regards to reading from or writing into a closed or a
 * failed pipe:
 *
 *  - Writing into a closed pipe yields `IllegalStateException`
 *  - Reading from a closed pipe yields EOF ([[com.twitter.util.Future.None]])
 *  - Reading from or writing into a failed pipe yields a failure it was failed with
 *
 * It's also worth discussing how pipes are being closed. As closure is always initiated by a
 * producer (writer), there is a machinery allowing it to be notified when said closure is observed
 * by a consumer (reader).
 *
 * The following rules should help reasoning about closure signals in pipes:
 *
 * - Closing a pipe with a pending read resolves said read into EOF and returns a
 *   [[com.twitter.util.Future.Unit]]
 * - Closing a pipe with a pending write by default fails said write with `IllegalStateException` and
 *   returns a future that will be satisfied when a consumer observes the closure (EOF) via read. If
 *   a timer is provided, the pipe will wait until the provided deadline for a successful read before
 *   failing the write.
 * - Closing an idle pipe returns a future that will be satisfied when a consumer observes the
 *   closure (EOF) via read when a timer is provided, otherwise the pipe will be closed immedidately.
 */
final class Pipe[A](timer: Timer) extends Reader[A] with Writer[A] {

  // Implementation Notes:
  //
  // - The the only mutable state of this pipe is `state` and the access to it is synchronized
  //   on `this`.
  //
  // - We do not run promises under a lock (`synchronized`) as it could lead to a deadlock if
  //   there are interleaved queue operations in the waiter closure.
  //
  // - Although promises are run without a lock, synchronized `state` transitions guarantee
  //   a promise that needs to be run is now owned exclusively by a caller, hence no concurrent
  //   updates will be racing with it.

  import Pipe._

  /**
   * For Java compatability
   */
  def this() = this(Timer.Nil)

  // thread-safety provided by synchronization on `this`
  private[this] var state: State[A] = State.Idle

  // satisfied when a `read` observes the EOF (after calling close())
  private[this] val closep: Promise[StreamTermination] = new Promise[StreamTermination]()

  def read(): Future[Option[A]] = {
    val (waiter, result) = synchronized {
      state match {
        case State.Failed(exc) =>
          (null, Future.exception(exc))

        case State.Closed =>
          (null, Future.None)

        case State.Idle =>
          val p = new Promise[Option[A]]
          state = State.Reading(p)
          (null, p)

        case State.Writing(buf, p) =>
          state = State.Idle
          (p, Future.value(Some(buf)))

        case State.Reading(_) =>
          (null, Future.exception(new IllegalStateException("read() while read is pending")))

        case State.Closing(buf, p) =>
          val result = buf match {
            case None =>
              state = State.Closed
              Future.None
            case _ =>
              state = State.Closing(None, null)
              Future.value(buf)
          }
          (p, result)
      }
    }

    if (waiter != null) waiter.setDone()
    if (result eq Future.None) closep.updateIfEmpty(StreamTermination.FullyRead.Return)
    result
  }

  def discard(): Unit = {
    fail(new ReaderDiscardedException(), discard = true)
  }

  def write(buf: A): Future[Unit] = {
    val (waiter, value, result) = synchronized {
      state match {
        case State.Failed(exc) =>
          (null, null, Future.exception(exc))

        case State.Closed | State.Closing(_, _) =>
          (null, null, Future.exception(new IllegalStateException("write() while closed")))

        case State.Idle =>
          val p = new Promise[Unit]
          state = State.Writing(buf, p)
          (null, null, p)

        case State.Reading(p: Promise[Option[A]]) =>
          // pending reader has enough space for the full write
          state = State.Idle
          // The Scala 3 compiler differentiates between the class type, `A` and the the defined
          // type of Promise in `Reading`, due to `State` being covariant in it's type parameter
          // but invariant in `Reading`. Let's explictly cast this type as we know it will be of
          // `Promise[Option[A]]`. See CSL-11210 or https://github.com/lampepfl/dotty/issues/13126
          (p.asInstanceOf[Promise[Option[A]]], Some(buf), Future.Done)

        case State.Writing(_, _) =>
          (
            null,
            null,
            Future.exception(new IllegalStateException("write() while write is pending"))
          )
      }
    }

    // The waiter and the value are mutually inclusive so just checking against waiter is adequate.
    if (waiter != null) waiter.setValue(value)
    result
  }

  def fail(cause: Throwable): Unit = fail(cause, discard = false)

  private[this] def fail(cause: Throwable, discard: Boolean): Unit = {
    val (closer, reader, writer) = synchronized {
      state match {
        case State.Closed | State.Failed(_) =>
          // do not update state to failing
          (null, null, null)
        case State.Idle =>
          state = State.Failed(cause)
          (closep, null, null)
        case State.Closing(_, p) =>
          state = State.Failed(cause)
          (closep, null, p)
        case State.Reading(p) =>
          state = State.Failed(cause)
          (closep, p, null)
        case State.Writing(_, p) =>
          state = State.Failed(cause)
          (closep, null, p)
      }
    }

    if (reader != null) reader.setException(cause)
    else if (writer != null) writer.setException(cause)

    if (closer != null) {
      if (discard) closer.updateIfEmpty(StreamTermination.Discarded.Return)
      else closer.updateIfEmpty(Throw(cause))
    }
  }

  private def closeLater(deadline: Time): Future[Unit] = {
    timer.doLater(Time.now.until(deadline)) {
      Pipe.this.synchronized {
        state match {
          case State.Failed(_) | State.Closed =>
          case _ => state = State.Closed
        }
      }
    }
  }

  def close(deadline: Time): Future[Unit] = {
    val (reader, writer) = synchronized {
      state match {
        case State.Failed(_) | State.Closed | State.Closing(_, _) =>
          (null, null)

        case State.Idle =>
          state = State.Closing(None, null)
          (null, null)

        case State.Reading(p) =>
          state = State.Closed
          (p, null)

        case State.Writing(buf, p) =>
          state = State.Closing(Some(buf), p)
          (null, p)
      }
    }

    if (reader != null) {
      reader.update(Return.None)
      closep.update(StreamTermination.FullyRead.Return)
    } else if (writer != null) {
      closeLater(deadline).respond { _ =>
        val exn = new IllegalStateException("close() while write is pending")
        writer.updateIfEmpty(Throw(exn))
        closep.updateIfEmpty(Throw(exn))
      }
    }

    onClose.unit
  }

  def onClose: Future[StreamTermination] = closep

  override def toString: String = synchronized(s"Pipe(state=$state)")
}

object Pipe {

  private sealed trait State[+A]

  private object State {

    /** Indicates no reads or writes are pending, and is not closed. */
    case object Idle extends State[Nothing]

    /**
     * Indicates a read is pending and is awaiting a `write`.
     *
     * @param p when satisfied it indicates that this read has completed.
     */
    final case class Reading[A](p: Promise[Option[A]]) extends State[A]

    /**
     * Indicates a write of `value` is pending to be `read`.
     *
     * @param value the value to write.
     * @param p when satisfied it indicates that this write has been fully read.
     */
    final case class Writing[A](value: A, p: Promise[Unit]) extends State[A]

    /** Indicates the pipe was failed. */
    final case class Failed(exc: Throwable) extends State[Nothing]

    /**
     * Indicates a close occurred while a write was pending.
     *
     * @param value the pending write
     * @param p when satisfied it indicates that this write has been fully read or the
     *          close deadline has expired before it has been fully read.
     * */
    final case class Closing[A](value: Option[A], p: Promise[Unit]) extends State[A]

    /** Indicates the reader has seen the EOF. No more reads or writes are allowed. */
    case object Closed extends State[Nothing]
  }

  /**
   * Copy elements from a Reader to a Writer. The Reader will be discarded if
   * `copy` is cancelled (discarding the writer). The Writer is unmanaged, the caller
   * is responsible for finalization and error handling, e.g.:
   *
   * {{{
   * Pipe.copy(r, w, n) ensure w.close()
   * }}}
   */
  def copy[A](r: Reader[A], w: Writer[A]): Future[Unit] = {
    def loop(): Future[Unit] =
      r.read().flatMap {
        case None => Future.Done
        case Some(elem) => w.write(elem) before loop()
      }

    w.onClose.respond {
      case Return(StreamTermination.Discarded) => r.discard()
      case _ => ()
    }
    val p = new Promise[Unit]
    // We have to do this because discarding the writer doesn't interrupt read
    // operations, it only fails the next write operation.
    loop().proxyTo(p)
    p.setInterruptHandler { case _ => r.discard() }
    p
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy