1

I am writing a simple function to return a Future[Unit] that completes after a given delay.

def delayedFuture(delay: FiniteDuration): Future[Unit] = {
  val promise = Promise[Unit]()
  val timerTask = new java.util.TimerTask {
    override def run(): Unit = promise.complete(Success(()))
  }
  new java.util.Timer().schedule(timerTask, delay.toMillis)
  promise.future
}

This implementation can probably work but I don't like creating a new Timer for each invocation because each Timer instance creates a thread. I can pass a Timer instance to delayedFuture as an argument but I don't want client code to know about Timer. So I guess I cannot use java.util.Timer at all.

I would like to use ExecutionContext for task scheduling and maybe define delayedFuture like this:

def delayedFuture(delay: FiniteDuration)
                 (implicit ec: ExecutoionContext): Future[Unit] = ???

What is the simplest way to implement delayedFuture like this without java.util.Timer ?

Michael
  • 41,026
  • 70
  • 193
  • 341
  • 1
    It's a bit complicated really—when it comes to Timers there are improtant factors which affect system runtime performance. What Accuracy is supported? What Resolution is supported? How scalable is the timer (i.e. how many tasks can it comfortably handle at once)? How is the lifecycle of the timer managed? (i.e. does scheduled things *not* get executed if the timer is shut down? Who shuts it down?) – Viktor Klang Mar 24 '21 at 14:48
  • Wow ! That's a lot of questions. Not sure I can answer them ... – Michael Mar 24 '21 at 16:18
  • 1
    I know :) This is incidentally also why I've not added a builtin Timer facility for Scala Futures (yet). It's a difficult problem space—to solve generically. – Viktor Klang Mar 24 '21 at 18:20
  • I got it :)) Yet another problem is making it testable. `java.util.Timer` seems to use a system clock, which I cannot fake :(( – Michael Mar 24 '21 at 19:56
  • 1
    Indeed—making things into globals make them extremely tricky to test. The option one has there is to make the implementation instantiable for tests and perform the tests on that. But that means testin the timer separately from the logic which uses the global timer. Unless one uses some clever tricks to replace the global with a mock (reflection?) during tests. – Viktor Klang Mar 24 '21 at 20:51
  • 2
    Don’t use `java.util.Timer`. It has been superseded by [`ScheduledExecutorService`](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/util/concurrent/ScheduledExecutorService.html) long ago. You can create an instance, e.g. via [`Executors.newScheduledThreadPool(int)`](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/util/concurrent/Executors.html#newScheduledThreadPool(int)) or `Executors.newSingleThreadScheduledExecutor()`. – Holger Mar 25 '21 at 09:54
  • Java 9 added `CompletableFuture.delayedExecutor(duration)` which internally uses JVM-wide scheduled executor. This is a very convenient, global thread to leverage. – Ben Manes Mar 26 '21 at 20:43

2 Answers2

3

You can use one of Java's ScheduledExecutorServices, e.g. create a single-thread pool and use it for scheduling only:

val svc = new ScheduledThreadPoolExecutor(1, (r: Runnable) => {
    val t = new Thread(r)
    t.setName("future-delayer")
    t.setDaemon(true) // don't hog the app with this thread existing
    t
})

def delayedFuture(d: FiniteDuration) = {
  val p = Promise[Unit]()
  svc.schedule(() => p.success(()), d.length, d.unit)
  p.future
}
Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
  • This response answers the question but I guess that `delayedFuture` should receive `ScheduledExecutorService` as an argument but I don't want client code to know about `ScheduledExecutorService`. I should clarify the question ... – Michael Mar 24 '21 at 10:48
  • 1
    I suggest you hide one as a private val in an object that has `delayedFuture`. You can't use `ExecutionContext` alone. In [monix](https://github.com/monix/monix) async/effect libary, we roll our own `Scheduler` type because of this, and the way we handle delays is quite similar to what I've posted here - we combine EC with something that can do scheduling, such as that one threaded scheduled executor. – Oleg Pyzhcov Mar 24 '21 at 11:11
  • It's also worth noting that futures will quickly switch to ExecutionContext supplied at next `.map`/`.flatMap` call so that one thread won't be occupied except for dispatching. – Oleg Pyzhcov Mar 24 '21 at 11:14
  • If I had an object that has `delayedFuture` I would use `java.util.Timer` I think. – Michael Mar 24 '21 at 11:14
  • As I understand you suggest creating `object DelayedFutureFactory { private val svc: ScheduledThreadPoolExecutor = ???; def delayedFuture(delay: FiniteDuration): Future[Unit] = ??? }` Ok. Maybe that's the solution ... – Michael Mar 24 '21 at 11:22
3

You don't need to create a new Timer for each invocation. Just make one global one.

object Delayed {
  private val timer = new Timer
  def apply[T](delay: Duration)(task: => T): Future[T] = {
    val promise = Promise[T]()
    val tt = new TimerTask {
      override def run(): Unit = promise.success(task)
    }
    timer.schedule(tt, delay.toMillis)
    promise.future
  }

  def unit(delay: Duration) = apply[Unit](delay) { () }
}

Then, Delayed.unit(10 seconds) gives you a future unit that satisfies in 10 seconds.

Dima
  • 39,570
  • 6
  • 44
  • 70
  • Well ... I'm afraid this code is not testable :(( since `java.util.Timer` uses a system clock ... – Michael Mar 24 '21 at 19:57
  • 1
    @Michael Yes, this is code is a sample for you to illustrate how to not create a an instance of timer every time you need a timer. To make it testable, it would take some additional effort (make it a class, with `timer` as a parameter), then have object extend it passing system timer for prod, and create instances with mock timers in tests. Pretty standard stuff, no rocket science. – Dima Mar 24 '21 at 20:40
  • You are right. It should not be very complicated. Thanks. – Michael Mar 24 '21 at 20:46