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

com.twitter.collection.GenerationalQueue.scala Maven / Gradle / Ivy

The newest version!
package com.twitter.collection

import scala.collection._

import com.twitter.util.{Duration, Time}

trait GenerationalQueue[A] {
  def touch(a: A)
  def add(a: A)
  def remove(a: A)
  def collect(d: Duration): Option[A]
  def collectAll(d: Duration): Iterable[A]
}

/**
 * Generational Queue keep track of elements based on their last activity.
 * You can refresh activity of an element by calling touch(a: A) on it.
 * There is 2 ways of retrieving old elements:
 * - collect(age: Duration) collect the oldest element (age of elem must be > age)
 * - collectAll(age: Duration) collect all the elements which age > age in parameter
 */
class ExactGenerationalQueue[A] extends GenerationalQueue[A] {
  private[this] val container = mutable.HashMap.empty[A, Time]
  private[this] implicit val ordering = Ordering.by[(A, Time), Time]{ case (_, ts) => ts }

  /**
   * touch insert the element if it is not yet present
   */
  def touch(a: A) = synchronized { container.update(a, Time.now) }

  def add(a: A) = synchronized { container += ((a, Time.now)) }

  def remove(a: A) = synchronized { container.remove(a) }

  def collect(age: Duration): Option[A] = synchronized {
    if (container.isEmpty)
      None
    else
      container.min match {
        case (a, t) if (t.untilNow > age) => Some(a)
        case _ => None
      }
  }

  def collectAll(age: Duration): Iterable[A] = synchronized {
    (container filter { case (_, t) => t.untilNow > age }).keys
  }
}


/**
 * Improved GenerationalQueue: using a list of buckets responsible for containing elements belonging
 * to a slice of time.
 * For instance: 3 Buckets, First contains elements from 0 to 10, second elements from 11 to 20
 * and third elements from 21 to 30
 * We expand the list when we need a new bucket, and compact the list to stash all old buckets
 * into one.
 * There is a slightly difference with the other implementation, when we collect elements we only
 * choose randomly an element in the oldest bucket, as we don't have activity date in this bucket
 * we consider the worst case and then we can miss some expired elements by never find elements
 * that aren't expired.
 */
class BucketGenerationalQueue[A](timeout: Duration) extends GenerationalQueue[A]
{
  object TimeBucket {
    def empty[B] = new TimeBucket[B](Time.now, timeSlice)
  }
  class TimeBucket[B](var origin: Time, var span: Duration) extends mutable.HashSet[B] {

    // return the age of the potential youngest element of the bucket (may be negative if the
    // bucket is not yet expired)
    def age(now: Time = Time.now): Duration = (origin + span).until(now)

    def ++=(other: TimeBucket[B]) = {
      origin = List(origin, other.origin).min
      span = List(origin + span, other.origin + other.span).max - origin

      super.++=(other)
      this
    }

    override def toString() = "TimeBucket(origin=%d, size=%d, age=%s, count=%d)".format(
      origin.inMilliseconds, span.inMilliseconds, age().toString, super.size
    )
  }

  private[this] val timeSlice = timeout / 3
  private[this] var buckets = List[TimeBucket[A]]()

  private[this] def maybeGrowChain() = {
    // NB: age of youngest element is negative when bucket isn't expired
    val growChain = buckets.headOption.map((bucket) => {
      bucket.age() > Duration.Zero
    }).getOrElse(true)

    if (growChain)
      buckets = TimeBucket.empty[A] :: buckets
    growChain
  }

  private[this] def compactChain(): List[TimeBucket[A]] = {
    val now = Time.now
    // partition equivalent to takeWhile/dropWhile because buckets are ordered
    val (news, olds) = buckets.partition(_.age(now) < timeout)
    if (olds.isEmpty)
      news
    else {
      val tailBucket = olds.head
      olds drop 1 foreach { tailBucket ++= _ }
      if (tailBucket.isEmpty)
        news
      else
        news ::: List(tailBucket)
    }
  }

  private[this] def updateBuckets() {
    if (maybeGrowChain())
      buckets = compactChain()
  }

  def touch(a: A) = synchronized {
    buckets drop 1 foreach { _.remove(a) }
    add(a)
  }

  def add(a: A) = synchronized {
    updateBuckets()
    buckets.head.add(a)
  }

  def remove(a: A) = synchronized {
    buckets foreach { _.remove(a) }
    buckets = compactChain()
  }

  def collect(d: Duration): Option[A] = synchronized {
    if (buckets.isEmpty)
      return None

    if (buckets.last.isEmpty)
      buckets = compactChain()

    if (buckets.isEmpty)
      return None

    val oldestBucket = buckets.last
    if (d < oldestBucket.age())
      oldestBucket.headOption
    else
      None
  }

  def collectAll(d: Duration): Iterable[A] = synchronized {
    (buckets dropWhile(_.age() < d)).flatten
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy