Skip to content

Commit

Permalink
Add Cancellable utility method
Browse files Browse the repository at this point in the history
  • Loading branch information
mdedetrich committed Mar 30, 2022
1 parent 50c3ffa commit 3954e75
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 0 deletions.
44 changes: 44 additions & 0 deletions src/main/scala/markatta/futiles/Cancellable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package markatta.futiles

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.util.Try
import java.util.concurrent.{Callable, FutureTask}

class Cancellable[T](executionContext: ExecutionContext, block: => T) {
private val promise = Promise[T]()

def future: Future[T] = promise.future

private val jf: FutureTask[T] = new FutureTask[T](
new Callable[T] {
override def call(): T = block
}
) {
override def done() = promise.complete(Try(get()))
}

/** Attempts to cancel the underlying [[scala.concurrent.Future]]. Note that this is a best effort attempt
*/
def cancel(): Unit = jf.cancel(true)

executionContext.execute(jf)
}

object Cancellable {

/** Allows you to put a computation inside of a [[scala.concurrent.Future]] which can later be cancelled
* @param block
* The computation to run inside of the [[scala.concurrent.Future]]
* @param executionContext
* The [[scala.concurrent.ExecutionContext]] to run the [[scala.concurrent.Future]] on
* @return
* A [[markatta.futiles]] providing both the [[scala.concurrent.Future]] and a `cancel` method allowing you to
* terminate the [[scala.concurrent.Future]] at any time
* @see
* Adapted from https://stackoverflow.com/a/39986418/1519631
*/
def apply[T](block: => T)(implicit executionContext: ExecutionContext): Cancellable[T] =
new Cancellable[T](executionContext, block)
}
55 changes: 55 additions & 0 deletions src/test/scala/markatta/futiles/CancellableSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package markatta.futiles

import java.util.concurrent.atomic.AtomicBoolean
import scala.concurrent.{CancellationException, ExecutionException}

class CancellableSpec extends Spec {
describe("The cancellable utility") {

describe("Basic cancel") {

it("successfully cancels a Future") {
val atomicBoolean = new AtomicBoolean(true)

val cancellable = Cancellable {
Thread.sleep(100)
atomicBoolean.set(false)
}

Thread.sleep(50)
cancellable.cancel()
Thread.sleep(100)
atomicBoolean.get() shouldEqual true
}

it("works as a normal Future when not cancelling") {
val cancellable = Cancellable {
true
}

cancellable.future.futureValue shouldEqual true
}

it("works as normal Future when throwing exception") {
val cancellable = Cancellable {
throw new IllegalArgumentException
}

val exception = cancellable.future.failed.futureValue
exception shouldBe an[ExecutionException]
exception.getCause shouldBe an[IllegalArgumentException]
}

it("when cancelling the exception should be a CancellationException exception") {
val cancellable = Cancellable {
Thread.sleep(100)
}
cancellable.cancel()
cancellable.future.failed.futureValue shouldBe an[CancellationException]
}

}

}

}

0 comments on commit 3954e75

Please sign in to comment.