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

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

There is a newer version: 0.4.7
Show 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 RemoveQueue.{ Elem, Node, End, tombstone, isTombstone }

/**
 * Like `MsQueue`, but also has support for interior node deletion
 * (`remove`), based on the public domain JSR-166 ConcurrentLinkedQueue
 * (https://web.archive.org/web/20220129102848/http://gee.cs.oswego.edu/dl/concurrency-interest/index.html).
 *
 * TODO: also unlink removed nodes (instead of just tombing them).
 */
private final class RemoveQueue[A] private[this] (sentinel: Node[A])
  extends Queue.WithRemove[A] {

  // TODO: do the optimization with ticketRead (like in `MsQueue`)

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

  def this() =
    this(Node(nullOf[Ref[A]], Ref.unsafeUnpadded(End[A]())))

  override val tryDeque: Axn[Option[A]] = {
    head.modifyWith { node =>
      skipTombs(from = node.next).flatMap {
        case None =>
          // empty queue:
          Rxn.ret((node, None))
        case Some((a, n)) =>
          // deque first node (and drop tombs before it):
          Rxn.ret((n.copy(data = nullOf[Ref[A]]), Some(a)))
      }
    }
  }

  private[this] def skipTombs(from: Ref[Elem[A]]): Axn[Option[(A, Node[A])]] = {
    from.get.flatMapF {
      case n @ Node(dataRef, nextRef) =>
        dataRef.get.flatMapF { a =>
          if (isTombstone(a)) {
            skipTombs(nextRef)
          } else {
            Rxn.pure(Some((a, n)))
          }
        }
      case End() =>
        Rxn.pure(None)
    }
  }

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

  override val enqueue: Rxn[A, Unit] = Rxn.computed { (a: A) =>
    Ref.unpadded[Elem[A]](End[A]()).flatMap { nextRef =>
      Ref.unpadded(a).flatMap { dataRef =>
        findAndEnqueue(Node(dataRef, nextRef))
      }
    }
  }

  override val enqueueWithRemover: Rxn[A, Axn[Unit]] = Rxn.computed { (a: A) =>
    Ref.unpadded[Elem[A]](End[A]()).flatMap { nextRef =>
      Ref.unpadded(a).flatMap { dataRef =>
        val newNode = Node(dataRef, nextRef)
        findAndEnqueue(newNode).as(newNode.remover)
      }
    }
  }

  // TODO: we could allow tail to lag by a constant
  private[this] def findAndEnqueue(node: Node[A]): Axn[Unit] = {
    def go(n: Node[A]): Axn[Unit] = {
      n.next.get.flatMapF {
        case End() =>
          // found true tail; will update, and adjust the tail ref:
          n.next.set.provide(node) >>> tail.set.provide(node)
        case nv @ Node(_, _) =>
          // not the true tail; try to catch up, and continue:
          go(n = nv)
      }
    }
    tail.get.flatMapF(go)
  }

  /**
   * Removes a single instance of the input
   *
   * Note: an item is only removed if it is identical to
   * (i.e., the same object as) the input. That is, items
   * are compared by reference equality. (This is why this
   * operation is not part of the public API.)
   */
  private[data] val remove: Rxn[A, Boolean] = Rxn.computed { (a: A) =>
    head.get.flatMapF { h =>
      findAndTomb(a, h.next)
    }
  }

  private[this] def findAndTomb(item: A, from: Ref[Elem[A]]): Axn[Boolean] = {
    from.get.flatMapF {
      case Node(dataRef, nextRef) =>
        dataRef.get.flatMapF { a =>
          if (equ(a, item)) {
            // found it
            dataRef.set.provide(tombstone[A]).as(true)
          } else {
            // continue search:
            findAndTomb(item, nextRef)
          }
        }
      case End() =>
        Rxn.pure(false)
    }
  }
}

private object RemoveQueue {

  def apply[A]: Axn[RemoveQueue[A]] =
    Rxn.unsafe.delay { _ => new RemoveQueue }

  private sealed trait Elem[A]

  /**
   * Sentinel node (head and tail): `data` is `null` (not a `Ref`).
   * Deleted (tombstone) node: `data` is a `Ref` which contains `Tombstone`.
   */
  private final case class Node[A](data: Ref[A], next: Ref[Elem[A]])
    extends Elem[A] {

    // We don't return a `Boolean` from
    // the `remover`, because since we're
    // deleting directly from the `Node`,
    // we can't be sure that the queue
    // even contains the thing we're removing.
    final def remover: Axn[Unit] = {
      this.data.set.provide(tombstone[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]]
  }

  // Note: it's important, that user code
  // can never access a `Tombstone`, as we
  // need to be able to reliably distinguish
  // user data from a tombstone (this is why
  // we can't simply use `null`).
  private[this] final object Tombstone {
    final def as[A]: A =
      this.asInstanceOf[A]
  }

  private def tombstone[A]: A =
    Tombstone.as[A]

  private def isTombstone[A](a: A): Boolean =
    equ(a, tombstone[A])
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy