forked from higherkindness/rules_scala
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #54 from lucidsoftware/worker-cancellation
Support worker cancellation for all workers
- Loading branch information
Showing
38 changed files
with
796 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
...in/scala/higherkindness/rules_scala/common/error/AnnexWorkRequestCancelledException.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package higherkindness.rules_scala | ||
package common.error | ||
|
||
class AnnexDuplicateActiveRequestException( | ||
val message: String = "", | ||
val cause: Throwable = null, | ||
) extends Exception(message, cause) |
14 changes: 14 additions & 0 deletions
14
src/main/scala/higherkindness/rules_scala/common/interrupt/BUILD
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
load("//rules:scala.bzl", "scala_library") | ||
load("//rules:scalafmt.bzl", "scala_format_test") | ||
|
||
scala_library( | ||
name = "interrupt", | ||
srcs = glob(["*.scala"]), | ||
scala = "//src/main/scala:bootstrap", | ||
visibility = ["//visibility:public"], | ||
) | ||
|
||
scala_format_test( | ||
name = "format", | ||
srcs = glob(["*.scala"]), | ||
) |
10 changes: 10 additions & 0 deletions
10
src/main/scala/higherkindness/rules_scala/common/interrupt/InterruptUtil.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package higherkindness.rules_scala | ||
package common.interrupt | ||
|
||
object InterruptUtil { | ||
def throwIfInterrupted(): Unit = { | ||
if (Thread.interrupted()) { | ||
throw new InterruptedException("WorkRequest was cancelled.") | ||
} | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
src/main/scala/higherkindness/rules_scala/common/worker/CancellableTask.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package higherkindness.rules_scala | ||
package common.worker | ||
|
||
import java.util.concurrent.{Callable, FutureTask} | ||
import scala.concurrent.{ExecutionContext, ExecutionException, Future, Promise} | ||
import scala.util.Try | ||
|
||
/** | ||
* This is more or less a cancellable Future. It stitches together Scala Future, which is not cancellable, with the Java | ||
* FutureTask, which is cancellable. | ||
* | ||
* However, it uses our extension on FutureTask, which, upon cancellation, waits for the callable to be interrupted or | ||
* complete. That way we can be confident the task is no longer running when we respond to Bazel that it has been | ||
* cancelled. | ||
* | ||
* Heavily inspired by the following: https://github.com/NthPortal/cancellable-task/tree/master | ||
* https://stackoverflow.com/a/39986418/6442597 | ||
*/ | ||
class CancellableTask[S] private (fn: => S) { | ||
private val promise = Promise[S]() | ||
val future: Future[S] = promise.future | ||
|
||
private val fnCallable = new Callable[S]() { | ||
def call(): S = fn | ||
} | ||
|
||
private val task = new FutureTaskWaitOnCancel[S](fnCallable) { | ||
override def done() = promise.complete { | ||
Try(get()).recover { | ||
// FutureTask wraps exceptions in an ExecutionException. We want to re-throw the underlying | ||
// error because Scala's Future handles things like fatal exception in a special way that | ||
// we miss out on if they're wrapped in that ExecutionException. Put another way: leaving | ||
// them wrapped in the ExecutionException breaks the contract that Scala Future users expect. | ||
case e: ExecutionException => throw e.getCause() | ||
} | ||
} | ||
} | ||
|
||
def cancel(mayInterruptIfRunning: Boolean): Boolean = task.cancel(mayInterruptIfRunning) | ||
|
||
def execute(executionContext: ExecutionContext): Unit = executionContext.execute(task) | ||
} | ||
|
||
object CancellableTask { | ||
def apply[S](fn: => S): CancellableTask[S] = { | ||
new CancellableTask(fn) | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
src/main/scala/higherkindness/rules_scala/common/worker/FutureTaskWaitOnCancel.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package higherkindness.rules_scala | ||
package common.worker | ||
|
||
import java.util.concurrent.{Callable, CancellationException, FutureTask, TimeUnit} | ||
import java.util.concurrent.locks.ReentrantLock | ||
|
||
/** | ||
* This is a FutureTask that, when cancelled, waits for its callable to end, either by interruption or by completing. | ||
* | ||
* The regular FutureTask is immediately marked done when you cancel it, regardless of the status of the callable. That | ||
* becomes a problem for if the worker receives many work requests, has them cancelled, and then immediately receives | ||
* more work requests. The callable could still be running, but we've received more work to do. | ||
* | ||
* That can create funky book keeping situations for Bazel: imagine you are running compile actions that take 60 seconds | ||
* and can't be interrupted. Bazel asks you to cancel them 30 seconds after they start. You respond to Bazel saying | ||
* they've been cancelled. Bazel sends you more work requests that also take 60 seconds to compile. You don't have any | ||
* threads to run them as they're all still finishing hte original requests. As a result it looks like these new compile | ||
* actions take 90 seconds because they had to wait 30 seconds to get threads in order to start executing. | ||
* | ||
* This class was heavily inspired by the following: | ||
* https://stackoverflow.com/questions/6040962/wait-for-cancel-on-futuretask?rq=3 | ||
*/ | ||
class FutureTaskWaitOnCancel[S]( | ||
callable: CallableLockedWhileRunning[S], | ||
) extends FutureTask[S](callable) { | ||
|
||
def this(callable: Callable[S]) = { | ||
this(new CallableLockedWhileRunning[S](callable)) | ||
} | ||
|
||
private def waitForCallable(): Unit = { | ||
// If the callable is running, wait for it to complete or be interrupted | ||
callable.isRunning.lock() | ||
callable.isRunning.unlock() | ||
} | ||
|
||
override def get(): S = { | ||
try { | ||
super.get() | ||
} catch { | ||
case e: CancellationException => | ||
waitForCallable() | ||
throw e | ||
} | ||
} | ||
|
||
override def get(timeout: Long, unit: TimeUnit): S = { | ||
throw new UnsupportedOperationException() | ||
} | ||
|
||
override def cancel(mayInterruptIfRunning: Boolean): Boolean = { | ||
val result = super.cancel(mayInterruptIfRunning) | ||
waitForCallable() | ||
result | ||
} | ||
} | ||
|
||
private class CallableLockedWhileRunning[S](callable: Callable[S]) extends Callable[S] { | ||
private[worker] val isRunning = new ReentrantLock() | ||
|
||
override def call(): S = { | ||
isRunning.lock() | ||
try { | ||
callable.call() | ||
} finally { | ||
isRunning.unlock() | ||
} | ||
} | ||
} |
Oops, something went wrong.