sbt/tasks/ParallelRunner.scala

190 lines
6.7 KiB
Scala

/* sbt -- Simple Build Tool
* Copyright 2009 Mark Harrah
*/
package xsbt
/** This file provides the parallel execution engine of sbt. It is a fairly general module, with pluggable Schedulers and Strategies.
*
* There are three main componenets to the engine: Distributors, Schedulers, and Strategies.
*
* A Scheduler provides work that is ready to execute.
*
* A Strategy is used by a Scheduler to select the work to process from the work that is ready. It is notified as work
* becomes ready. It is requested to select work to process from the work that is ready.
*
* A Distributor uses a Scheduler to obtain work up to the maximum work allowed to run at once. It runs each
* unit of work in its own Thread. It then returns the work and either the computed value or the error that occured.
*
* The Scheduler and Strategy are called from the main thread and therefore do not need to be thread-safe.
**/
import java.util.concurrent.LinkedBlockingQueue
import scala.collection.{immutable, mutable}
import immutable.TreeSet
/** Processes work. */
trait Compute[Work[_],Result[_]] { def apply[A](w: Work[A]): Result[A] }
/** Requests work from `scheduler` and processes it using `compute`. This class limits the amount of work processing at any given time
* to `workers`.*/
final class Distributor[O,Work[_],Result[_]](val scheduler: Scheduler[O,Work,Result], compute: Compute[Work,Result], workers: Int) extends NotNull
{
require(workers > 0)
final def run() = (new Run).run()
private final class Run extends NotNull
{
import java.util.concurrent.LinkedBlockingQueue
private[this] val schedule = scheduler.run
/** The number of threads currently running. */
private[this] var running = 0
/** Pending notifications of completed work. */
private[this] val complete = new LinkedBlockingQueue[Done[_]]
private[Distributor] def run(): O =
{
def runImpl(): O =
{
next()
if(isIdle && !schedule.hasPending) // test if all work is complete
{
assume(schedule.isComplete, "Distributor idle and the scheduler indicated no work pending, but scheduler indicates it is not complete.")
schedule.result
}
else
{
waitForCompletedWork() // wait for some work to complete
runImpl() // continue
}
}
try { runImpl() }
finally { shutdown() }
}
private def shutdown(): Unit = all.foreach(_.work.put(None))
// true if the maximum number of worker threads are currently running
private def atMaximum = running == workers
private def availableWorkers = workers - running
// true if no worker threads are currently running
private def isIdle = running == 0
// process more work
private def next()
{
// if the maximum threads are being used, do nothing
// if all work is complete or the scheduler is waiting for current work to complete, do nothing
if(!atMaximum && schedule.hasPending)
{
val nextWork = schedule.next(availableWorkers)
val nextSize = nextWork.size
assume(nextSize <= availableWorkers, "Scheduler provided more work (" + nextSize + ") than allowed (" + availableWorkers + ")")
assume(nextSize > 0 || !isIdle, "Distributor idle and the scheduler indicated work pending, but provided no work.")
nextWork.foreach(work => process(work))
}
}
// wait on the blocking queue `complete` until some work finishes and notify the scheduler
private def waitForCompletedWork()
{
assume(running > 0)
val done = complete.take()
running -= 1
notifyScheduler(done)
}
private def notifyScheduler[T](done: Done[T]): Unit = schedule.complete(done.work, done.result)
private def process[T](work: Work[T])
{
assume(running + 1 <= workers)
running += 1
available.take().work.put(Some(work))
}
private[this] val all = List.tabulate(workers, i => new Worker)
private[this] val available =
{
val q = new LinkedBlockingQueue[Worker]
all.foreach(q.put)
q
}
private final class Worker extends Thread with NotNull
{
lazy val work =
{
start()
new LinkedBlockingQueue[Option[Work[_]]]
}
override def run()
{
def processData[T](data: Work[T])
{
val result = ErrorHandling.wideConvert(compute(data))
complete.put( new Done(result, data) )
}
def runImpl()
{
work.take() match
{
case Some(data) =>
processData(data)
available.put(this)
runImpl()
case None => ()
}
}
try { runImpl() }
catch { case e: InterruptedException => () }
}
}
}
private final class Done[T](val result: Either[Throwable, Result[T]], val work: Work[T]) extends NotNull
}
/** Schedules work of type Work that produces results of type Result. A Scheduler determines what work is ready to be processed.
* A Scheduler is itself immutable. It creates a mutable object for each scheduler run.*/
trait Scheduler[O,Work[_],Result[_]] extends NotNull
{
/** Starts a new run. The returned object is a new Run, representing a single scheduler run. All state for the run
* is encapsulated in this object.*/
def run: Run
trait Run extends NotNull
{
/** Notifies this scheduler that work has completed with the given result.*/
def complete[A](d: Work[A], result: Either[Throwable,Result[A]]): Unit
/** Returns true if there is any more work to be done, although remaining work can be blocked
* waiting for currently running work to complete.*/
def hasPending: Boolean
/**Returns true if this scheduler has no more work to be done, ever.*/
def isComplete: Boolean
/** Returns up to 'max' units of work. `max` is always positive. The returned sequence cannot be empty if there is
* no work currently being processed.*/
def next(max: Int): Seq[Work[_]]
/** The final result after all work has completed. */
def result: O
}
}
/** A Strategy selects the work to process from work that is ready to be processed.*/
trait ScheduleStrategy[D] extends NotNull
{
/** Starts a new run. The returned object is a new Run, representing a single strategy run. All state for the run
* is handled through this object and is encapsulated in this object.*/
def run: Run
trait Run extends NotNull
{
/** Adds the given work to the list of work that is ready to run.*/
def workReady(dep: D): Unit
/** Returns true if there is work ready to be run. */
def hasReady: Boolean
/** Provides up to `max` units of work. `max` is always positive and this method is not called
* if hasReady is false. The returned list cannot be empty is there is work ready to be run.*/
def next(max: Int): List[D]
}
}
final class SimpleStrategy[D] extends ScheduleStrategy[D]
{
def run = new Run
{
private var ready = List[D]()
def workReady(dep: D) { ready ::= dep }
def hasReady = !ready.isEmpty
def next(max: Int): List[D] =
{
val ret = ready.take(max)
ready = ready.drop(max)
ret
}
}
}