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

scalaz.stream.nondeterminism.nondeterminism.scala Maven / Gradle / Ivy

The newest version!
package scalaz.stream


import Cause._
import scalaz.{Right3, Middle3, Left3, \/-, -\/, Either3, \/}
import scalaz.concurrent.{Actor, Strategy, Task}
import scalaz.stream.Process._

package object nondeterminism {


  /**
   * Nondeterministic join of streams. Up to `maxOpen` processes may be
   * open at a time, and `maxQueued` bounds the amount of prefetching.
   *
   * If any process terminates abnormally, the whole join terminates with
   * that reason and all open processes are killed.
   * If the resulting process is killed or fails, or if downstream terminates
   * early, all the merged processes are killed.
   *
   * @param maxOpen     maximum simultaneous processes that concurrently merge. If eq 0 no limit.
   * @param maxQueued   maximum number of queued `A` before merged processes will back-off. If eq 0 no limit.
   * @param source
   * @param S
   * @tparam A
   * @return
   */
  def njoin[A](maxOpen: Int, maxQueued: Int)(source: Process[Task, Process[Task, A]])(implicit S: Strategy): Process[Task, A] = {
    sealed trait M
    case object Start extends M
    case class Delay(cont: Cont[Task, Process[Task, A]]) extends M
    case class Offer(p: Process[Task, A], cont: Cont[Task,Process[Task,A]]) extends M
    case class FinishedSource(rsn: Cause) extends M
    case class Finished(result: Throwable \/ Unit) extends M
    case class FinishedDown(cb: (Throwable \/ Unit) => Unit) extends M


    suspend {
      val q = if (maxQueued > 0)
        async.boundedQueue[A](maxQueued)(S)
      else
        async.unboundedQueue[A](S)

      val done = async.signalOf(false)(S)

      //keep state of master source
      var state: Either3[Cause, EarlyCause => Unit,  Cont[Task,Process[Task,A]]] =
        Either3.left3(End)

      //keep no of open processes
      var opened: Int = 0

      //if this mergeN is closed this is set to reason that caused mergeN to close
      var closed: Option[Cause] = None

      //keeps track of finalized streams and will issue callback
      //when all streams have finished.
      var completer: Option[(Throwable \/ Unit) => Unit] = None

      var actor: Actor[M] = null

      //evaluates next step of the source of processes
      def nextStep(from: Process[Task, Process[Task, A]]) = {
        Either3.middle3({
          from stepAsync {
            case -\/(rsn) =>
              actor ! FinishedSource(rsn)

            // contrary to popular belief (well, basically just *here*), stepAsync does not guarantee non-emptiness
            case \/-((Seq(), cont)) =>
              actor ! Delay(cont)

            case \/-((processes, cont)) =>
              val next = (c:Cause) => Trampoline.done(
                c.fold(emitAll(processes.tail) +: cont)(
                  early => Halt(early) +: cont
                )
              )
              actor ! Offer(processes.head, Cont(Vector(next)))
          }
        })
      }

      // fails the signal should cause all streams to terminate

      def fail(cause: Cause): Unit = {
        closed = Some(cause)
        S(done.kill runAsync { _=> () })
        state = state match {
          case Middle3(interrupt) => S(interrupt(Kill)) ; Either3.middle3((_:Cause) => ())
          case Right3(cont) => nextStep(Halt(Kill) +: cont);  Either3.middle3((_:Cause) => ())
          case Left3(_) => state
        }
      }

      // starts the source evaluation
      def start: Task[Unit] =
        Task delay { actor ! Start }

      def sourceDone = state.leftOr(false)(_=>true)

      def allDone : Boolean = opened <= 0 && sourceDone

      def completeIfDone: Unit = {
        if (allDone) {
          S(q.failWithCause(closed.getOrElse(End)) runAsync { _ => {} })
          completer.foreach { cb =>  S(cb(\/-(()))) }
          completer = None
          closed = closed orElse Some(End)
        }
      }

      actor = Actor[M]({m =>
        closed.fold(m match {
          case Delay(cont) =>
            state = nextStep(cont.continue)

          // next merged process is available
          // run it with chance to interrupt
          // and give chance to start next process if not bounded
          case Offer(p, cont) =>
            opened = opened + 1
            if (maxOpen <= 0 || opened < maxOpen) state = nextStep(cont.continue)
            else state = Either3.right3(cont)

            //runs the process with a chance to interrupt it using signal `done`
            //interrupt is done via killing the done signal.
            //enqueue action needs to be interruptible, otherwise the process will never halt
            //if the outer process is terminated while the queue is full and an enqueue is pending
            //note that here we convert `Kill` to exception to distinguish form normal and
            //killed behaviour of upstream termination
            done.discrete.wye(p.flatMap(a => eval_(q.enqueueOne(a))))(wye.interrupt)
            .onHalt{
              case Kill => Halt(Error(Terminated(Kill)))
              case cause => Halt(cause)
            }
            .run.runAsync { res =>
              S(actor ! Finished(res))
            }

          //finished the `upstream` normally but still have some open processes to merge
          //update state and wait for processes to merge
          case FinishedSource(End) if opened > 0 =>
            state = Either3.left3(End)

          // finished upstream and no processes are running, terminate downstream
          // or the cause is early cause and shall kill all the remaining processes
          case FinishedSource(rsn) =>
            state = Either3.left3(rsn)
            fail(rsn)
            completeIfDone

          // merged process terminated with failure
          // kill all the open processes including the `source`
          case Finished(-\/(rsn)) =>
            val cause = rsn match {
              case Terminated(Kill) => Kill
              case _ => Error(rsn)
            }
            opened = opened - 1
            fail(cause)
            completeIfDone

          // One of the processes terminated w/o failure
          // Finish merge if the opened == 0 and upstream terminated
          // or give an opportunity to start next merged process if the maxOpen was hit
          case Finished(\/-(_)) =>
            opened = opened - 1
            state = state match {
              case Right3(cont)
                if maxOpen <= 0 || opened < maxOpen => nextStep(cont.continue)
              case Left3(End) if opened == 0 => fail(End) ; state
              case _                     => state
            }
            completeIfDone

          // `Downstream` of the merge terminated
          // Kill all upstream processes and eventually the source.
          case FinishedDown(cb) =>
            fail(Kill)
            if (allDone) S(cb(\/-(())))
            else completer = Some(cb)

          // Start evaluation of the source.
          case Start =>
            // Assignment to `state` must be performed inside this actor to prevent races.
            state = nextStep(source)

        })(rsn => m match {
          //join is closed, next p is ignored and source is killed
          case Offer(_, cont) => nextStep(Halt(Kill) +: cont)
          case FinishedSource(cause) => state = Either3.left3(cause) ; completeIfDone
          case Finished(_) => opened = opened - 1; completeIfDone

          case FinishedDown(cb) => {
            if (allDone)
              S(cb(\/-(())))
            else {
              // this dance is symetrical with what we do in wye.apply handling multiple DownDone messages
              completer = completer map { cb0 =>
                { v: (Throwable \/ Unit) => cb0(v); cb(v) }
              } orElse Some(cb)     // this case shouldn't ever happen
            }
          }

          case Start => ()
          case Delay(cont) => nextStep(cont.continue)
        })

      })


      (eval_(start) ++ q.dequeue) onComplete eval_(Task.async[Unit] { cb => actor ! FinishedDown(cb) })
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy