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

dev.tauri.choam.data.MsQueue.scala Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2016-2024 Daniel Urban and contributors listed in NOTICE.txt
 *
 * 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 dev.tauri.choam
package data

import MsQueue._

/**
 * Unbounded MPMC queue, based on the lock-free Michael-Scott queue described in
 * https://web.archive.org/web/20220123224641/https://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf.
 *
 * Some optimizations are based on the public domain JSR-166 ConcurrentLinkedQueue
 * (https://web.archive.org/web/20220129102848/http://gee.cs.oswego.edu/dl/concurrency-interest/index.html),
 * or on the implementation in the Reagents paper
 * (https://web.archive.org/web/20220214132428/https://www.ccis.northeastern.edu/home/turon/reagents.pdf).
 */
private final class MsQueue[A] private[this] (
  sentinel: Node[A],
  padded: Boolean,
) extends Queue[A] {

  private[this] val head: Ref[Node[A]] = Ref.unsafePadded(sentinel)
  private[this] val tail: Ref[Node[A]] = Ref.unsafePadded(sentinel)

  private def this(padded: Boolean) =
    this(Node(nullOf[A], if (padded) Ref.unsafePadded(End[A]()) else Ref.unsafeUnpadded(End[A]())), padded = padded)

  override val tryDeque: Axn[Option[A]] = {
    head.modifyWith { node =>
      node.next.unsafeTicketRead.flatMapF { ticket =>
        ticket.unsafePeek match {
          case n @ Node(a, _) =>
            // No need to validate `node.next` here, since
            // it is not the last node (thus it won't change).
            Rxn.postCommit(
              // This is to help the GC; a link from old
              // nodes (e.g., the one we're removing now)
              // to newer nodes (e.g., the new head) can
              // put pressure on the GC. So we set the next
              // pointer to `null` after we're done. (This is
              // the same optimization as in ConcurrentLinkedQueue,
              // except we can use `null` instead of a self-link,
              // because we have a sentinel for the `End`.)
              node.next.update { ov =>
                if (ov eq n) null
                else ov
              }
            ).as((n.copy(data = nullOf[A]), Some(a)))
          case End() =>
            ticket.unsafeValidate.as((node, None))
        }
      }
    }
  }

  override val enqueue: Rxn[A, Unit] = Rxn.unsafe.suspend { (a: A) =>
    findAndEnqueue(newNode(a))
  }

  final override def tryEnqueue: Rxn[A, Boolean] =
    this.enqueue.as(true)

  private[this] def newNode(a: A): Node[A] = {
    val newRef: Ref[Elem[A]] = if (this.padded) {
      Ref.unsafePadded(End[A]()) // TODO: optimize with RIG
    } else {
      Ref.unsafeUnpadded(End[A]()) // TODO: optimize with RIG
    }
    Node(a, newRef)
  }

  private[this] def findAndEnqueue(node: Node[A]): Axn[Unit] = {
    def go(n: Node[A]): Axn[Unit] = {
      n.next.unsafeTicketRead.flatMapF { ticket =>
        ticket.unsafePeek match {
          // TODO: if we allow the tail to lag:
          // case null =>
          //   head.get.flatMapF { h => go(h) }
          case End() =>
            // found true tail; will update, and adjust the tail ref:
            // TODO: we could allow tail to lag by a constant
            ticket.unsafeSet(node) >>> tail.set.provide(node)
          case nv @ Node(_, _) =>
            // not the true tail, continue;
            // no need to validate `n.next`
            // (it is not the last node, thus it won't change)
            go(n = nv)
        }
      }
    }
    tail.get.flatMapF(go)
  }

  /** For testing */
  private[data] def tailLag: Axn[Int] = {
    def go(n: Node[A], acc: Int): Axn[Int] = {
      n.next.unsafeTicketRead.flatMapF { ticket =>
        ticket.unsafePeek match {
          case null =>
            Rxn.pure(-1)
          case End() =>
            Rxn.pure(acc)
          case nv @ Node(_, _) =>
            go(n = nv, acc = acc + 1)
        }
      }
    }
    tail.get.flatMapF { t => go(t, 0) }
  }
}

private object MsQueue {

  private sealed trait Elem[A]

  private final case class Node[A](data: A, next: Ref[Elem[A]]) extends Elem[A]

  private final case class End[A]() extends Elem[A]

  private final object End {

    private[this] final val _end: End[Any] =
      new End[Any]()

    final def apply[A](): End[A] =
      _end.asInstanceOf[End[A]]
  }

  def apply[A]: Axn[MsQueue[A]] =
    padded[A]

  def padded[A]: Axn[MsQueue[A]] =
    applyInternal(padded = true)

  def unpadded[A]: Axn[MsQueue[A]] =
    applyInternal(padded = false)

  private[data] def fromList[F[_], A](as: List[A])(implicit F: Reactive[F]): F[MsQueue[A]] = {
    Queue.fromList[F, MsQueue, A](this.apply[A])(as)
  }

  private[this] def applyInternal[A](padded: Boolean): Axn[MsQueue[A]] =
    Rxn.unsafe.delay { _ => new MsQueue(padded = padded) }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy