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

org.scaladebugger.api.utils.LoopingTaskRunner.scala Maven / Gradle / Ivy

The newest version!
package org.scaladebugger.api.utils

import java.util
import java.util.concurrent._
import java.util.concurrent.atomic.AtomicInteger

import scala.util.Try

import LoopingTaskRunner._

/**
 * Contains defaults for the looping task runner.
 */
object LoopingTaskRunner {
  /** Default initial workers is equal to number of available processors */
  val DefaultInitialWorkers: Int = Runtime.getRuntime.availableProcessors()

  /** Default maximum wait time is 100 milliseconds */
  val DefaultMaxTaskWaitTime: (Long, TimeUnit) = (100L, TimeUnit.MILLISECONDS)
}

/**
 * Represents a queue of tasks that will be executed infinitely in order
 * until removed.
 *
 * @param initialWorkers The total number of works to use for this runner on
 *                       startup (more can be added or removed)
 * @param maxTaskWaitTime The maximum time to wait for a task to be pulled off
 *                        of the queue before allowing other tasks to be run
 */
class LoopingTaskRunner(
  private val initialWorkers: Int = DefaultInitialWorkers,
  private val maxTaskWaitTime: (Long, TimeUnit) = DefaultMaxTaskWaitTime
) {
  type TaskId = String

  /**
   * Represents a task that will execute the next task on the provided queue
   * and add it back to the end of the queue when finished.
   *
   * @param taskQueue The queue containing the ids of the tasks to run
   * @param taskMap The mapping of task ids to associated runnable tasks
   */
  private class LoopingTask(
    private val taskQueue: util.concurrent.BlockingQueue[TaskId],
    private val taskMap: util.Map[TaskId, Runnable]
  ) extends Runnable {
    override def run(): Unit = {
      // Update tracking information to reflect an active worker
      currentActiveWorkers.incrementAndGet()

      // Determine the next task to execute (wait for a maximum time duration)
      val taskId = nextTaskId()

      // If there is a new task, perform the operation
      taskId.foreach(executeTask)

      // Start next task once this is free (suppress exceptions in the
      // situation that this runner has been stopped)
      //
      // NOTE: Do not add this runner back on our queue if we want to decrease
      //       the total number of workers via the desired total workers
      if (currentActiveWorkers.decrementAndGet() < desiredTotalWorkers.get()) {
        Try(runNextTask())
      }
    }

    /**
     * Retrieves the id of the next task to execute.
     *
     * @return Some id if there is a new task to execute, otherwise None
     */
    protected def nextTaskId(): Option[TaskId] = Option(taskQueue.poll(
      maxTaskWaitTime._1,
      maxTaskWaitTime._2
    ))

    /**
     * Retrieves and executes the next task, placing it back on the queue
     * once finished.
     *
     * @param taskId The id of the task to execute
     */
    protected def executeTask(taskId: TaskId): Unit = {
      // Retrieve and execute the next task
      val tryTask = Try(taskMap.get(taskId))
      tryTask.foreach(task => Try(task.run()))

      // Task finished, so add back to end of our queue
      // NOTE: Only do so if the map knows about our task (allows removal)
      if (tryTask.isSuccess) taskQueue.put(taskId)
    }
  }

  /** Represents the desired number of workers processing and on queue */
  private val desiredTotalWorkers = new AtomicInteger(0)

  /** Represents the total number of workers processing tasks */
  private val currentActiveWorkers = new AtomicInteger(0)

  /** Contains the ids of tasks to be executed (in order). */
  private val taskQueue = new LinkedBlockingQueue[TaskId]()

  /** Contains mapping of task ids to task implementations. */
  private val taskMap = new ConcurrentHashMap[TaskId, Runnable]()

  /** Represents the executors used to execute the tasks. */
  @volatile private var executorService: Option[ExecutorService] = None

  /**
   * Indicates whether or not the task runner is processing tasks.
   *
   * @return True if it is running, otherwise false
   */
  def isRunning: Boolean = executorService.nonEmpty

  /**
   * Executing begins the process of executing queued up tasks.
   */
  def start(): Unit = {
    assert(!isRunning, "Runner already started!")

    // Create our thread pool executor to process tasks
    executorService = Some(newExecutorService())

    // Start X tasks to be run
    setDesiredTotalWorkers(initialWorkers)
  }

  /**
   * Prevents the runner from executing any more tasks.
   *
   * @param removeAllTasks If true, removes all tasks after being stopped
   */
  def stop(removeAllTasks: Boolean = true): Unit = {
    assert(isRunning, "Runner not started!")

    setDesiredTotalWorkers(0)
    executorService.get.shutdown()
    executorService = None

    if (removeAllTasks) {
      taskQueue.clear()
      taskMap.clear()
    }
  }

  /**
   * Sets the desired total number of workers to eventually be achieved by
   * the task runner.
   *
   * @param value The new desired total number of workers
   */
  def setDesiredTotalWorkers(value: Int): Unit = {
    // Determine if this is an increase or decrease in workers
    val delta = value - desiredTotalWorkers.getAndSet(value)

    // If an increase, we need to spawn new tasks to increase our workers
    if (delta > 0) (1 to delta).foreach(_ => runNextTask())
  }

  /**
   * Retrieves the current desired total number of workers.
   *
   * @return The desired total number of workers
   */
  def getDesiredTotalWorkers: Int = desiredTotalWorkers.get()

  /**
   * Retrieves the total actively-running workers.
   *
   * @return The total active workers at this point in time
   */
  def getCurrentActiveWorkers: Int = currentActiveWorkers.get()

  /**
   * Adds a task to be executed repeatedly (in a queue with other tasks).
   *
   * @param task The task to add
   * @tparam T The return type of the task
   *
   * @return The id of the queued task
   */
  def addTask[T](task: => T): TaskId = {
    val taskId = java.util.UUID.randomUUID().toString

    // Add the task to our lookup table, and then queue it up for processing
    taskMap.put(taskId, new Runnable {
      override def run(): Unit = task
    })
    taskQueue.put(taskId)

    taskId
  }

  /**
   * Removes a task from the repeated execution.
   *
   * @param taskId The id of the task to remove
   *
   * @return Task implementation that was removed
   */
  def removeTask(taskId: TaskId): Runnable = {
    taskQueue.remove(taskId)
    taskMap.remove(taskId)
  }

  /**
   * Creates a new executor service for use by the looping task runner.
   *
   * @return The new executor service instance
   */
  protected def newExecutorService(): ExecutorService = {
    // TODO: Replace cached thread pool with more optimized thread pool
    //       executor that scales better for large number of tasks
    Executors.newCachedThreadPool()
  }

  /**
   * Executes next available task.
   */
  protected def runNextTask(): Unit =
    executorService.foreach(_.execute(newLoopingTask()))

  /**
   * Creates a new looping task to be executed.
   */
  protected def newLoopingTask(): Runnable = new LoopingTask(taskQueue, taskMap)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy