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

org.http4s.blaze.util.TickWheelExecutor.scala Maven / Gradle / Ivy

package org.http4s.blaze.util

import scala.annotation.tailrec
import scala.util.control.NonFatal
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

import com.typesafe.scalalogging.slf4j.Logging


/**
 * @author Bryce Anderson
 *         Created on 2/2/14
 *
 */


/** Low resolution execution scheduler
  *
  * @note The ideas for [[org.http4s.blaze.util.TickWheelExecutor]] is based off of loosely came from the
  * Akka scheduler, which was based on the Netty HashedWheelTimer which was in term
  * based on concepts in George Varghese
  * and Tony Lauck's paper 'Hashed
  * and Hierarchical Timing Wheels: data structures to efficiently implement a
  * timer facility'
  *
  * @constructor primary constructor which immediately spins up a thread and begins ticking
  *
  * @param wheelSize number of spokes on the wheel. Each tick, the wheel will advance a spoke
  * @param resolution duration between ticks
  */
class TickWheelExecutor(wheelSize: Int = 512, resolution: Duration = 100.milli) extends Logging {

  @volatile private var currentTick = 0
  @volatile private var alive = true

  private val tickMilli = resolution.toMillis
  private val _tickInv = 1.0/tickMilli.toDouble

  private val clockFace: Array[Bucket] = {
    val arr = new Array[Bucket](wheelSize)
    0 until wheelSize foreach { i => arr(i) = new Bucket }
    arr
  }

  /////////////////////////////////////////////////////
  // new Thread that actually runs the execution.

  private val thread = new Thread {
    override def run() {
      cycle()
    }
  }

  thread.start()

  /////////////////////////////////////////////////////

  def shutdown(): Unit = {
    alive = false
  }

  // Execute directly on this worker thread. ONLY for QUICK tasks...
  def schedule(r: Runnable, timeout: Duration): Cancellable = {
    schedule(r, Execution.directec, timeout)
  }

  def schedule(r: Runnable, ec: ExecutionContext, timeout: Duration): Cancellable = {
    if (!timeout.isFinite()) sys.error(s"Cannot submit infinite duration delays!")
    else if (alive) {
      val exp = timeout.toMillis
      if (exp > 0) getBucket(exp).add(r, ec, exp + System.currentTimeMillis())
      else {  // we can submit the task right now! Not sure why you would want to do this...
        try ec.execute(r)
        catch { case NonFatal(t) => onNonFatal(t) }
        Cancellable.finished
      }
    }
    else sys.error("TickWheelExecutor is shutdown")
  }

  @tailrec
  private def cycle(): Unit = {
    val i = currentTick
    val time = System.currentTimeMillis()

    clockFace(i).prune(time) // Remove canceled and submit expired tasks from the current spoke
    currentTick = (i + 1) % wheelSize

    clockFace(i).prune(time) // Remove canceled and submit expired tasks from the next spoke

    if (alive) {
      // Make up for execution time, unlikely to be significant
      val left = tickMilli - (System.currentTimeMillis() - time)
      if (left > 0) Thread.sleep(left)
      cycle()
    }
    else {  // delete all our buckets so we don't hold any references
      for { i <- 0 until wheelSize} clockFace(i) = null
    }
  }

  protected def onNonFatal(t: Throwable) {
    logger.error("Non-Fatal Exception caught while executing scheduled task", t)
  }
  
  private def getBucket(duration: Long): Bucket = {
    val i = ((duration*_tickInv).toLong + currentTick) % wheelSize
    clockFace(i.toInt)
  }

  private class Bucket {
    private val lock = new AnyRef
    private var nodes: Node = null

    /** Removes expired and canceled elements from this bucket, placing expired ones in the
      * expiredTasks variable for subsequent execution
      * @param time current system time (in milliseconds)
      */
    def prune(time: Long) {
      var expiredTasks: Node = null

      // we lock the Bucket and collect expired elements
      lock.synchronized {
        val i = nodes
        nodes = null

        @tailrec
        def go(tail: Node, i: Node): Unit = if (i != null) {
          val n = i.next
          i.next = null
          if (i.cancelled()) go(tail, n)
          else if (i.expiresBy(time)) {
            i.next = expiredTasks
            expiredTasks = i
            go(tail, n)
          }
          else {  // still valid
            if (tail == null) nodes = i // first element
            else tail.next = i
            go(i, n)
          }
        }
        go(null, i)
      }

      // All done pruning and the lock is released. Now we need to execute the expiredTasks
      while(expiredTasks != null) {
        val i = expiredTasks
        expiredTasks = expiredTasks.next
        i.next = null
        try i.run()
        catch { case NonFatal(t) => onNonFatal(t) }
      }
    }
    
    def add(r: Runnable, ec: ExecutionContext, expiration: Long): Cancellable = lock.synchronized {
      nodes = new Node(r, ec, expiration, nodes)
      nodes
    }
  }

  /** A Link in a single linked list which can also be passed to the user as a Cancellable
    * @param r [[java.lang.Runnable]] which will be executed after the expired time
    * @param ec [[scala.concurrent.ExecutionContext]] on which to execute the Runnable
    * @param expiration time in milliseconds after which this Node is expired
    * @param next next Node in the list or null if this is the last element
    */
  private class Node(r: Runnable, ec: ExecutionContext, expiration: Long, var next: Node) extends Cancellable {
    @volatile private var alive = true

    def expiresBy(now: Long): Boolean = now >= expiration

    def cancelled(): Boolean = !alive

    def cancel(): Unit = alive = false

    def run() = ec.execute(r)
  }
}

trait Cancellable {

  def cancel(): Unit

  def cancelled(): Boolean

}

object Cancellable {
  val finished = new Cancellable {
    def cancel(): Unit = {}
    def cancelled(): Boolean = false
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy