softwaremill / odelay

delayed reactions

GitHub

odelay

Build Status

Delayed reactions, fashioned from tools you already have sitting around your shed.

installation

The current version of odelay is 0.3.0 and targets scala 2.12+. The odelay-twitter module is not published for 2.11.*.

modules

  • odelay-core odelay core interfaces and default jdk backed timer
libraryDependencies += "com.softwaremill.odelay" %% "odelay-core" % "0.3.0"
  • odelay-netty netty 4 backed odelay timer interface
libraryDependencies += "com.softwaremill.odelay" %% "odelay-netty" % "0.3.0"
  • odelay-netty3 netty 3 backed odelay timer interface
libraryDependencies += "com.softwaremill.odelay" %% "odelay-netty3" % "0.3.0"
  • odelay-twitter twitter util backed odelay timer interface
libraryDependencies += "com.softwaremill.odelay" %% "odelay-twitter" % "0.3.0"

usage

Odelay provides a simple interface producing Delays. Delays are to operations as Futures are to values, for given FiniteDurations.

primitives

Odelay separates execution from interface by defining two primitives:

  • an odelay.Timer, which defers task execution
  • an odelay.Delay, which represents a delayed operation.

A delayed operation requires a FiniteDuration and some arbitrary block of code which will execute after that duration.

Typical usage is as follows.

import scala.concurrent.duration._

// print "executed" after a 2 second delay
odelay.Delay(2.seconds) {
  println("executed")
}

Timers

In order for the example above to compile, an instance of an odelay.Timer needs to be in implicit scope, just as an ExecutionContext would when working with Scala Futures.

odelay.Timers define an interface for task scheduling. Implementations of odelay.Timers are defined for a number of environments and platforms.

JdkTimer

The default Timer is a standard jdk ScheduledExecutorService backed Timer.

To make the example above compile, import the default Timer.

import scala.concurrent.duration._

// bring default timer into scope
import odelay.Timer.default

odelay.Delay(2.seconds) {
  println("executed")
}

If you have already allocated your own ScheduledExecutorService, you may define your own jdk timer reusing those thread resources and bring that into implicit scope.

import scala.concurrent.duration._

// define a new JdkTimer instance with resources preallocated
implicit val myJdkTimer = new odelay.jdk.JdkTimer(
  myScheduledExecutorService, interuptOnCancel)
 
odelay.Delay(2.seconds) {
  println("executed")
}

Netty(3)Timers

If your application's classpath includes netty, a widely adopted library for writing asynchronous services on the JVM, there's a good chance you will want to use the odelay-netty ( netty 4 ) or odelay-netty3 ( netty 3 ) modules which are backed by a netty HashedWheelTimer.

To use one of these, bring an instance of the default netty timer into scope

import scala.concurrent.duration._

// create a new netty timer and bring it into explicit scope
implicit val timer = odelay.netty.NettyTimer.newTimer

odelay.Delay(2.seconds) {
  println("executed")
}

If your application has already allocated a HashedWheelTimer, you can easily create your own odelay.Timer instance backed with resources you have already allocated.

import scala.concurrent.duration._

implicit val timer = new odelay.netty.NettyTimer(myHashedWheelTimer)

odelay.Delay(2.seconds) {
  println("executed")
}

Netty 4+ defines a new concurrency primitive called an io.netty.util.concurrent.EventExecutorGroup. Odelay's netty module defines a Timer interface for that as well. You will most likely have an EventExecutorGroup defines in your netty pipeline. To create a Timer instance from one of those, you can do the following

import scala.concurrent.duration._

implicit val timer = new odelay.netty.NettyGroupTimer(
  myEventExecutorGroup)

odelay.Delay(2.seconds) {
  println("executed")
}

TwitterTimers

If your application has the twitter util suite of utilities on its classpath, there's a good chance you will want to use the odelay-twitter module which defines an odelay.Timer in terms of twitter util's own timer interface, com.twitter.util.Timer. A default Timer is provided backed by a com.twitter.util.JavaTimer

import scala.concurrent.duration._
implicit val timer = odelay.twitter.TwitterTimer.newTimer
odelay.Delay(2.seconds) {
  println("executed")
}

You may also define your own odelay.Timer in terms of a com.twitter.util.Timer which you may already have in scope.

import scala.concurrent.duration._
implicit val timer = new odelay.twitter.TwitterTimer(myTwitterTimer)
odelay.Delay(2.seconds) {
  println("executed")
}

Releasing resources

odelay.Timers use thread resources to do their work. In order for a jvm to be shutdown cleanly, these thread resources need to be released. Depending on your applications needs, you should really only need one instance of an odelay.Timer for a given process.

When an application terminates, it should be instrumented in a way that ensures the stop() method of that odelay.Timer is invoked. This ensures thread resources are released so your application can shutdown cleanly. Calling stop() on a Timer will most likely result failed promises if a new Delay is attempted with the stopped timer.

Periodic delays

Odelay also provides an interface for cases where you wish to execute a task on a repeating interval of periodic delays. You can do so with the odelay.Delay#every interface which takes 3 curried arguments: a scala.concurrent.duration.FiniteDuration representing the periodic delay, an optional scala.concurrent.duration.FiniteDuration representing the initial delay (the default is no delay), and a block of code to execute periodically.

The following example will print "executed" every two seconds until the resulting time out is canceled or the timer is stopped.

import scala.concurrent.duration._

import odelay.Timer.default

odelay.Delay.every(2.seconds)() {
  println("executed")
}

Delays

Like Futures, which provide a interface for reacting to changes of a deferred value, odelay operations produce odelay.Delay values, which can be used to react to timer operations.

Since Delays represent deferred operations, odelay.Delays expose a future method which returns a Future that will be satisfied as a success with the return type of block supplied to odelay.Delay when the operation is scheduled.

odelay.Delays may be canceled. Cancellation will satisfy the Future in a failure state. Armed with this knowledge, you can chain dependent actions in a "data flow" fashion. Note, the return type of a Delay's Future is determined by the block of code supplied. If your block returns a Future itself, the Delay's future being satisfied doesn't imply the blocks future will also be satisfied as well. If you wish to chain these together, simply flatMap the results, delay.future.flatMap(identity).

Below is an example of reacting the a delay's execution.

import scala.concurrent.duration._

// future execution
import scala.concurrent.ExecutionContext.Implicits.global

// delay execution
import odelay.Timer.default

odelay.Delay(2.seconds) {
  println("executed")
}.future.onSuccess {
  case _ => println("task scheduled")
}

Note, the import of the ExecutionContext. An implicit instance of one must be in scope for the invocation of a Future's onSuccess method.

Periodically Delayed futures

A periodic delay's future should intuitively never complete, as a Future can only be satisfied once and a period delay will be executed a number of times.

However, a canceled periodic delay will satisfy a periodic delay's Future in a failure state.

import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import odelay.Timer.default

val delay = odelay.Delay.every(2.seconds)() {
  println("executed")
}

delay.future.onSuccess {
  case _ => println("this will never get called")
}

delay.future.onFailure {
  case _ => println("this can get called, if you call delay.cancel()")
}

Credits

Originally created by Doug Tangren, maintained by SoftwareMill.