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

korolev.effect.Queue.scala Maven / Gradle / Ivy

/*
 * Copyright 2017-2020 Aleksey Fomkin
 *
 * 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 korolev.effect

import korolev.effect.Effect.Promise
import korolev.effect.syntax._

import java.util.concurrent.atomic.AtomicReference
import scala.annotation.tailrec
import scala.collection.immutable.{Queue => IQueue}

/**
 * Nonblocking, concurrent, asynchronous queue.
 */
class Queue[F[_]: Effect, T](maxSize: Int) {

  import Queue._

  private val state = new AtomicReference(Queue.State[T]())

  private final class QueueStream extends Stream[F, T] {

    def pull(): F[Option[T]] = Effect[F].promise { cb =>
      @tailrec
      def aux(): Unit = {
        val ref = state.get()
        ref.error match {
          case Some(error) => cb(Left(error))
          case None if ref.closed => cb(noneToken)
          case None if ref.queue.nonEmpty =>
            val (value, updatedQueue) = ref.queue.dequeue
            val canOfferCallback = ref.canOfferCallbacks.headOption
            val newValue = ref.copy(
              queue = updatedQueue,
              canOfferCallbacks = ref.canOfferCallbacks.drop(1)
            )
            if (state.compareAndSet(ref, newValue)) {
              cb(Right(Option(value)))
              canOfferCallback.foreach(_(unitToken))
            } else {
              aux()
            }
          case None if ref.stopped => cb(noneToken)
          case None =>
            val updatedPc = cb :: ref.pullCallbacks
            val newValue = ref.copy(pullCallbacks = updatedPc)
            if (!state.compareAndSet(ref, newValue)) {
              aux()
            }
        }
      }
      aux()
    }

    def cancel(): F[Unit] = Effect[F].delay {
      unsafeClose()
      @tailrec def aux(): Unit = {
        val ref = state.get
        val newValue = ref.copy(cancelCallbacks = Nil)
        if (state.compareAndSet(ref, newValue)) {
          ref.cancelCallbacks.foreach(cb => cb(unitToken))
        } else {
          aux()
        }
      }
      aux()
    }
  }

  /**
   * Strict version of [[offer]]. Still thread safe.
   */
  def offerUnsafe(item: T): Boolean = {
    @tailrec
    def aux(): Boolean = {
      val ref = state.get()
      if (!ref.stopped && !ref.closed) {
        // Pull callbacks can be nonempty
        // if queue was empty when pull was ran.
        if (ref.pullCallbacks.nonEmpty) {
          val newValue = ref.copy(pullCallbacks = Nil)
          if (state.compareAndSet(ref, newValue)) {
            val token = Right(Some(item))
            ref.pullCallbacks.foreach(cb => cb(token))
            true
          } else {
            aux()
          }
        } else if (ref.queue.size < maxSize) {
          val updatedQueue = ref.queue.enqueue(item)
          val newValue = ref.copy(queue = updatedQueue)
          if (state.compareAndSet(ref, newValue)) {
            true
          } else {
            aux()
          }
        } else {
          false
        }
      } else {
        false
      }
    }
    aux()
  }

  def unsafeStop(): Unit = {
    @tailrec def aux(): Unit = {
      val ref = state.get
      if (state.compareAndSet(ref, ref.copy(stopped = true))) {
        if (ref.queue.isEmpty) {
          ref.canOfferCallbacks.foreach(cb => cb(unitToken))
          ref.pullCallbacks.foreach(cb => cb(noneToken))
        }
      } else  {
        aux()
      }
    }
    aux()
  }

  def unsafeClose(): Unit = {
    @tailrec
    def aux(): Unit = {
      val ref = state.get
      if (!ref.closed) {
        val newValue = ref.copy(canOfferCallbacks = Nil, pullCallbacks = Nil, closed = true)
        if (state.compareAndSet(ref, newValue)) {
          ref.canOfferCallbacks.foreach(cb => cb(unitToken))
          ref.pullCallbacks.foreach(cb => cb(noneToken))
        } else {
          aux()
        }
      }
    }
    aux()
  }

  /**
   * Signals that queue size became less than [[maxSize]].
   * @example {{{
   * def aux(): F[Unit] = queue.offer(o).flatMap {
   *   case false => queue.canOffer *> aux()
   *   case true => Effect[F].unit
   * }
   * aux()
   * }}}
   */
  def canOffer: F[Unit] = Effect[F].promise { cb =>
    @tailrec
    def aux(): Unit = {
      val ref = state.get
      if (ref.closed || ref.stopped || ref.queue.size < maxSize) {
        cb(unitToken)
      } else {
        val newValue = ref.copy(canOfferCallbacks = cb :: ref.canOfferCallbacks)
        if (!state.compareAndSet(ref, newValue)) {
          aux()
        }
      }
    }
    aux()
  }

  def failUnsafe(e: Throwable): Unit = {
    @tailrec
    def aux(): Unit = {
      val ref = state.get
      val newValue = ref.copy(canOfferCallbacks = Nil, pullCallbacks = Nil, error = Some(e))
      if (state.compareAndSet(ref, newValue)) {
        ref.canOfferCallbacks.foreach(cb => cb(unitToken))
        ref.pullCallbacks.foreach(cb => cb(Left(e)))
      } else {
        aux()
      }
    }
    aux()
  }

  /**
   * Offers `item` to the queue.
   * @return true is ok and false if [[maxSize]] reached or queue is stopped.
   */
  def offer(item: T): F[Boolean] =
    Effect[F].delay(offerUnsafe(item))

  /**
   * Enqueue item. If [[maxSize]] reached waits until queue will decrease.
   */
  def enqueue(item: T): F[Unit] = {
    def aux(): F[Unit] = {
      val ref = state.get
      offer(item).flatMap {
        case true => Effect[F].unit
        case false if ref.stopped || ref.closed => Effect[F].unit
        case false => canOffer *> aux()
      }
    }
    aux()
  }

  /**
   * Disallow to offer new items.
   * Stream ends with last item.
   */
  def stop(): F[Unit] =
    Effect[F].delay(unsafeStop())

  /**
   * Immediately stop offering and pulling items from the queue.
   * @return
   */
  def close(): F[Unit] =
    Effect[F].delay(unsafeClose())

  def fail(e: Throwable): F[Unit] =
    Effect[F].delay(failUnsafe(e))

  /**
   * Resolves only if `stream.cancel` ran.
   * @return
   */
  def cancelSignal: F[Unit] = Effect[F].promise { cb =>
    @tailrec def aux(): Unit = {
      val ref = state.get
      val newValue = ref.copy(cancelCallbacks = cb :: ref.cancelCallbacks)
      if (!state.compareAndSet(ref, newValue)) {
        aux()
      }
    }
    aux()
  }

  /**
   * Returns the size of the queue. This property takes into account canOfferCallbacks and pullCallbacks to
   * determine the real size of the queue after all of them are resolved.
   * @return
   */
  def size(): F[Int] =
    Effect[F].delay {
      val s = state.get()
      s.queue.size + s.canOfferCallbacks.size - s.pullCallbacks.length
    }

  val stream: Stream[F, T] = new QueueStream()
}

object Queue {

  private case class State[T](stopped: Boolean = false,
                              closed: Boolean = false,
                              error: Option[Throwable] = None,
                              pullCallbacks: List[Promise[Option[T]]] = Nil,
                              canOfferCallbacks: List[Promise[Unit]] = Nil,
                              cancelCallbacks: List[Promise[Unit]] = Nil,
                              queue: IQueue[T] = IQueue.empty)

  private final val unitToken = Right(())
  private final val noneToken = Right(None)

  def apply[F[_]: Effect, T](maxSize: Int = Int.MaxValue): Queue[F, T] =
    new Queue[F, T](maxSize)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy