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 bda0e5e
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/main/scala/markatta/futiles/Cancellable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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]()

/** @return
* The underlying [[scala.concurrent.Future]] which you can use
*/
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.Cancellable]] 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)
}
59 changes: 59 additions & 0 deletions src/test/scala/markatta/futiles/CancellableSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package markatta.futiles

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

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

describe("without cancellation") {

it("works as a normal Future") {
val cancellable = Cancellable {
()
}

cancellable.future.futureValue shouldEqual ()
}

it("throws an Exception correctly") {
val cancellable = Cancellable {
throw new IllegalArgumentException
}

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

}

describe("with cancellation") {

it("prevents Future from completing") {
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("throws a CancellationException exception") {
val cancellable = Cancellable {
Thread.sleep(100)
}
cancellable.cancel()
cancellable.future.failed.futureValue shouldBe an[CancellationException]
}

}

}

}

0 comments on commit bda0e5e

Please sign in to comment.