├── project ├── build.properties └── plugins.sbt ├── .gitignore ├── .scalafmt.conf ├── .github ├── release-drafter.yml └── workflows │ ├── scala-steward.yml │ └── ci.yml ├── .git-blame-ignore-revs ├── notes ├── about.markdown └── 0.1.0.markdown ├── odelay-core └── src │ └── main │ ├── scalajs │ ├── platform.scala │ └── JsTimer.scala │ ├── scalajvm │ ├── platform.scala │ └── jdk │ │ └── JdkTimer.scala │ └── scala │ ├── package.scala │ ├── timer.scala │ └── Delay.scala ├── odelay-core-tests └── src │ └── test │ └── scala │ └── jdk │ └── JdkTimerSpec.scala ├── odelay-netty └── src │ ├── test │ └── scala │ │ ├── NettyTimerSpec.scala │ │ └── NettyGroupTimerSpec.scala │ └── main │ ├── ls │ └── 0.1.0.json │ └── scala │ └── NettyTimer.scala ├── odelay-netty3 └── src │ ├── test │ └── scala │ │ └── NettyTimerSpec.scala │ └── main │ ├── ls │ └── 0.1.0.json │ └── scala │ └── NettyTimer.scala ├── odelay-twitter └── src │ ├── test │ └── scala │ │ └── TwitterTimerSpec.scala │ └── main │ ├── ls │ └── 0.1.0.json │ └── scala │ └── TwitterTimer.scala ├── .scala-steward.conf ├── LICENSE ├── .mergify.yml ├── odelay-testing └── src │ └── test │ ├── scalajvm │ └── TimerSpec.scala │ └── scalajs │ └── TimerSpec.scala └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *~ 3 | .idea 4 | .bsp 5 | .metals 6 | .vscode 7 | .cursor -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | runner.dialect = scala3 2 | version = 3.8.3 3 | maxColumn = 120 -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What's Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.7.15 2 | 06eb25c80d98c654a446e0625988333cde075769 3 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | [odelay](https://github.com/softprops/odelay) defines a set or primitives for delaying operations and reacting to their execution 2 | -------------------------------------------------------------------------------- /odelay-core/src/main/scalajs/platform.scala: -------------------------------------------------------------------------------- 1 | package odelay 2 | 3 | private[odelay] object platform { 4 | implicit val defaultTimer: Timer = js.JsTimer.newTimer 5 | } 6 | -------------------------------------------------------------------------------- /odelay-core/src/main/scalajvm/platform.scala: -------------------------------------------------------------------------------- 1 | package odelay 2 | 3 | private[odelay] object platform { 4 | implicit val defaultTimer: Timer = jdk.JdkTimer.newTimer 5 | } 6 | -------------------------------------------------------------------------------- /odelay-core-tests/src/test/scala/jdk/JdkTimerSpec.scala: -------------------------------------------------------------------------------- 1 | package odelay.jdk 2 | 3 | class JdkTimerSpec extends odelay.testing.TimerSpec { 4 | def newTimer = JdkTimer.newTimer 5 | def timerName = "JdkTimer" 6 | } 7 | -------------------------------------------------------------------------------- /odelay-netty/src/test/scala/NettyTimerSpec.scala: -------------------------------------------------------------------------------- 1 | package odelay.netty 2 | 3 | class NettyTimerSpec extends odelay.testing.TimerSpec { 4 | def newTimer = NettyTimer.newTimer 5 | def timerName = "NettyTimer" 6 | } 7 | -------------------------------------------------------------------------------- /odelay-netty3/src/test/scala/NettyTimerSpec.scala: -------------------------------------------------------------------------------- 1 | package odelay.netty 2 | 3 | class Netty3TimerSpec extends odelay.testing.TimerSpec { 4 | def newTimer = NettyTimer.newTimer 5 | def timerName = "Netty3Timer" 6 | } 7 | -------------------------------------------------------------------------------- /odelay-twitter/src/test/scala/TwitterTimerSpec.scala: -------------------------------------------------------------------------------- 1 | package odelay.twitter 2 | 3 | class TwitterTimerSpec extends odelay.testing.TimerSpec { 4 | def newTimer = TwitterTimer.newTimer 5 | def timerName = "TwitterTimer" 6 | } 7 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.pin = [ 2 | {groupId = "org.scala-lang", artifactId = "scala3-library", version = "3.3."}, 3 | {groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = "3.3."}, 4 | {groupId = "org.scala-lang", artifactId = "scala-library", version = "2.13."} 5 | ] 6 | -------------------------------------------------------------------------------- /odelay-netty/src/test/scala/NettyGroupTimerSpec.scala: -------------------------------------------------------------------------------- 1 | package odelay.netty 2 | 3 | import io.netty.util.concurrent.DefaultEventExecutorGroup 4 | 5 | class NettyGroupTimerSpec extends odelay.testing.TimerSpec { 6 | def newTimer = NettyTimer.groupTimer(new DefaultEventExecutorGroup(1)) 7 | def timerName = "NettyGroupTimer" 8 | } 9 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") 2 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") 3 | 4 | val sbtSoftwareMillVersion = "2.0.20" 5 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % sbtSoftwareMillVersion) 6 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % sbtSoftwareMillVersion) 7 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-browser-test-js" % sbtSoftwareMillVersion) 8 | 9 | addSbtPlugin("org.jetbrains" % "sbt-ide-settings" % "1.1.0") 10 | -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | name: Scala Steward 2 | 3 | # This workflow will launch at 00:00 every day 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write # Required to checkout and push changes 11 | pull-requests: write # Required to create PRs for dependency updates 12 | 13 | jobs: 14 | scala-steward: 15 | uses: softwaremill/github-actions-workflows/.github/workflows/scala-steward.yml@main 16 | secrets: 17 | github-token: ${{ secrets.SOFTWAREMILL_CI_PR_TOKEN }} 18 | with: 19 | java-version: '21' 20 | -------------------------------------------------------------------------------- /odelay-netty3/src/main/ls/0.1.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "me.lessis", 3 | "name" : "odelay-netty3", 4 | "version" : "0.1.0", 5 | "description" : "an odelay.Timer implementation backed by netty 3", 6 | "site" : "", 7 | "tags" : [ "delay", "scheduling", "future" ], 8 | "docs" : "", 9 | "resolvers" : [ "http://dl.bintray.com/content/softprops/maven" ], 10 | "dependencies" : [ { 11 | "organization" : "io.netty", 12 | "name" : "netty", 13 | "version" : "3.9.2.Final" 14 | } ], 15 | "scalas" : [ "2.10.4", "2.11.1" ], 16 | "licenses" : [ { 17 | "name" : "MIT", 18 | "url" : "https://github.com/softprops/odelay/blob/0.1.0/LICENSE" 19 | } ], 20 | "sbt" : false 21 | } -------------------------------------------------------------------------------- /odelay-netty/src/main/ls/0.1.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "me.lessis", 3 | "name" : "odelay-netty", 4 | "version" : "0.1.0", 5 | "description" : "an odelay.Timer implementation backed by netty 4", 6 | "site" : "", 7 | "tags" : [ "delay", "scheduling", "future" ], 8 | "docs" : "", 9 | "resolvers" : [ "http://dl.bintray.com/content/softprops/maven" ], 10 | "dependencies" : [ { 11 | "organization" : "io.netty", 12 | "name" : "netty-common", 13 | "version" : "4.0.21.Final" 14 | } ], 15 | "scalas" : [ "2.10.4", "2.11.1" ], 16 | "licenses" : [ { 17 | "name" : "MIT", 18 | "url" : "https://github.com/softprops/odelay/blob/0.1.0/LICENSE" 19 | } ], 20 | "sbt" : false 21 | } -------------------------------------------------------------------------------- /odelay-twitter/src/main/ls/0.1.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "me.lessis", 3 | "name" : "odelay-twitter", 4 | "version" : "0.1.0", 5 | "description" : "an odelay.Timer implementation backed by a com.twitter.util.Timer", 6 | "site" : "", 7 | "tags" : [ "delay", "scheduling", "future" ], 8 | "docs" : "", 9 | "resolvers" : [ "http://dl.bintray.com/content/softprops/maven" ], 10 | "dependencies" : [ { 11 | "organization" : "com.twitter", 12 | "name" : "util-core", 13 | "version" : "6.18.0" 14 | } ], 15 | "scalas" : [ "2.10.4" ], 16 | "licenses" : [ { 17 | "name" : "MIT", 18 | "url" : "https://github.com/softprops/odelay/blob/0.1.0/LICENSE" 19 | } ], 20 | "sbt" : false 21 | } -------------------------------------------------------------------------------- /odelay-core/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | /** Odelay defines a set of primitives for delaying the execution of operations. 2 | * 3 | * This is differs from scala.concurrent.Futures in that the execution of an operation will not occur until a provided 4 | * delay, specified as a scala.concurrent.duration.Duration. The delay of a task may also may be canceled. Operations 5 | * may also be executed after a series of delays, also represented by scala.concurrent.duration.Durations. 6 | * 7 | * These primitives can be used to complement the usage of scala.concurrent.Futures by defining a deterministic delay 8 | * for the future operation as well as a way to cancel the future operation. 9 | * 10 | * An odelay.Delay represents a delayed operation and defines a future method which may be used to trigger dependent 11 | * actions and delay cancellations. 12 | */ 13 | package object odelay 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-14 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /odelay-core/src/main/scala/timer.scala: -------------------------------------------------------------------------------- 1 | package odelay 2 | 3 | import scala.concurrent.duration.FiniteDuration 4 | import scala.annotation.implicitNotFound 5 | 6 | /** The deferrer of some arbitrary operation */ 7 | @implicitNotFound("Cannot find an implicit odelay.Timer, either define one yourself or import odelay.Timer.default") 8 | trait Timer { 9 | 10 | /** Delays the execution of an operation until the provided duration */ 11 | def apply[T](delay: FiniteDuration, op: => T): Delay[T] 12 | 13 | /** Delays the execution of an operation until the provided deplay and then after, repeats the operation at the every 14 | * duration after. Timeouts returned by this expose a Future that will never complete until cancelled 15 | */ 16 | def apply[T](delay: FiniteDuration, every: FiniteDuration, todo: => T): PeriodicDelay[T] 17 | 18 | /** Stops the timer and releases any retained resources. Once a Timer is stoped, it's behavior is undefined. */ 19 | def stop(): Unit 20 | } 21 | 22 | /** Defines default configurations for timers */ 23 | object Timer { 24 | implicit val default: Timer = platform.defaultTimer 25 | } 26 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: delete head branch after merge 3 | conditions: [] 4 | actions: 5 | delete_head_branch: {} 6 | - name: automatic merge for softwaremill-ci pull requests affecting build.sbt 7 | conditions: 8 | - author=softwaremill-ci 9 | - check-success=ci 10 | - "#files=1" 11 | - files=build.sbt 12 | actions: 13 | merge: 14 | method: merge 15 | - name: automatic merge for softwaremill-ci pull requests affecting project plugins.sbt 16 | conditions: 17 | - author=softwaremill-ci 18 | - check-success=ci 19 | - "#files=1" 20 | - files=project/plugins.sbt 21 | actions: 22 | merge: 23 | method: merge 24 | - name: semi-automatic merge for softwaremill-ci pull requests 25 | conditions: 26 | - author=softwaremill-ci 27 | - check-success=ci 28 | - "#approved-reviews-by>=1" 29 | actions: 30 | merge: 31 | method: merge 32 | - name: automatic merge for softwaremill-ci pull requests affecting project build.properties 33 | conditions: 34 | - author=softwaremill-ci 35 | - check-success=ci 36 | - "#files=1" 37 | - files=project/build.properties 38 | actions: 39 | merge: 40 | method: merge 41 | - name: automatic merge for softwaremill-ci pull requests affecting .scalafmt.conf 42 | conditions: 43 | - author=softwaremill-ci 44 | - check-success=ci 45 | - "#files=1" 46 | - files=.scalafmt.conf 47 | actions: 48 | merge: 49 | method: merge 50 | -------------------------------------------------------------------------------- /odelay-core/src/main/scalajs/JsTimer.scala: -------------------------------------------------------------------------------- 1 | package odelay 2 | package js 3 | 4 | import scalajs.js.timers._ 5 | import scala.concurrent.duration.FiniteDuration 6 | 7 | class JsTimer() extends Timer { 8 | 9 | /** Delays the execution of an operation until the provided duration */ 10 | def apply[T](delay: FiniteDuration, op: => T): Delay[T] = { 11 | new PromisingDelay[T] { 12 | val clearable: SetTimeoutHandle = setTimeout(delay) { 13 | completePromise(op) 14 | } 15 | 16 | def cancel(): Unit = { clearTimeout(clearable); cancelPromise() } 17 | } 18 | } 19 | 20 | /** Delays the execution of an operation until the provided deplay and then after, repeats the operation at the every 21 | * duration after. Timeouts returned by this expose a Future that will never complete until cancelled 22 | */ 23 | def apply[T](delay: FiniteDuration, every: FiniteDuration, todo: => T): PeriodicDelay[T] = { 24 | new PeriodicPromisingDelay[T](every) { 25 | var clearable: SetIntervalHandle = null 26 | val initclearable: SetTimeoutHandle = setTimeout(delay) { 27 | clearTimeout(initclearable); 28 | clearable = setInterval(every) { 29 | if (promiseIncomplete) todo 30 | } 31 | } 32 | 33 | def cancel(): Unit = { 34 | if (clearable != null) clearInterval(clearable) 35 | cancelPromise() 36 | } 37 | } 38 | } 39 | 40 | /** Stops the timer and releases any retained resources. Once a Timer is stoped, it's behavior is undefined. */ 41 | def stop(): Unit = () 42 | } 43 | 44 | object JsTimer { 45 | def newTimer: Timer = new JsTimer() 46 | } 47 | -------------------------------------------------------------------------------- /odelay-twitter/src/main/scala/TwitterTimer.scala: -------------------------------------------------------------------------------- 1 | package odelay.twitter 2 | 3 | import com.twitter.util.{Duration, JavaTimer, Timer => TwttrTimer} 4 | import odelay.{Delay, PeriodicDelay, PeriodicPromisingDelay, PromisingDelay, Timer} 5 | import scala.concurrent.Promise 6 | import scala.concurrent.duration.FiniteDuration 7 | import scala.util.control.NonFatal 8 | 9 | case class TwitterTimer(underlying: TwttrTimer) extends Timer { 10 | 11 | def apply[T](delay: FiniteDuration, op: => T): Delay[T] = 12 | new PromisingDelay[T] { 13 | val tto = 14 | try { 15 | Some(underlying.schedule(duration(delay).fromNow)(completePromise(op))) 16 | } catch { 17 | case NonFatal(e) => 18 | failPromise(e) 19 | None 20 | } 21 | 22 | def cancel() = tto.foreach { f => 23 | f.cancel() 24 | cancelPromise() 25 | } 26 | } 27 | 28 | def apply[T](delay: FiniteDuration, every: FiniteDuration, op: => T): PeriodicDelay[T] = 29 | new PeriodicPromisingDelay[T](every) { 30 | val tto = 31 | try { 32 | Some(underlying.schedule(duration(delay).fromNow, duration(every))(op)) 33 | } catch { 34 | case NonFatal(e) => 35 | failPromise(e) 36 | None 37 | } 38 | 39 | def cancel() = tto.foreach { f => 40 | f.cancel() 41 | cancelPromise() 42 | } 43 | } 44 | 45 | def stop(): Unit = underlying.stop() 46 | 47 | private def duration(fd: FiniteDuration) = 48 | Duration.fromNanoseconds(fd.toNanos) 49 | } 50 | 51 | object TwitterTimer { 52 | 53 | /** Default twitter timer backed by a com.twitter.util.JavaTimer */ 54 | def newTimer: Timer = new TwitterTimer(new JavaTimer(true)) 55 | } 56 | -------------------------------------------------------------------------------- /notes/0.1.0.markdown: -------------------------------------------------------------------------------- 1 | ## initial release 2 | 3 | Scala [Futures](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future) are primitives for composing behavior over defered values. Odelay defines a set of primatives for composing behavior over operations delayed for [FiniteDurations](http://www.scala-lang.org/api/current/index.html#scala.concurrent.duration.FiniteDuration). 4 | 5 | ### typical usage 6 | 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import scala.concurrent.duration._ 9 | import odelay.Timer.default 10 | 11 | // declare a delay 12 | val delay = odelay.Delay(5.seconds) { 13 | myDelayedOperation 14 | } 15 | 16 | Delays define a future interface for projecting on the scheduling of an operation. 17 | 18 | // project on a delay's success 19 | delay.future.onSuccess { 20 | case result => println("operation was scheduled") 21 | } 22 | 23 | Unlike scala Futures, Delays may explicitly be cancelled. A Delay may also project on a cancelation state by reacting to its 24 | future's failure state. 25 | 26 | // project on a delay's cancellation 27 | delay.future.onFailure { 28 | case _ => println("operaton was cancelled") 29 | } 30 | 31 | delay.cancel() 32 | 33 | Odelay also supports periodic repeating operations. 34 | 35 | // run myDelatedOperation every 5 seconds with no initial delay 36 | val periodic = odelay.Delay.every(5.seconds)() { 37 | myDelayedOperation 38 | } 39 | 40 | See the project's [readme](https://github.com/softprops/odelay#odelay) for more examples. 41 | 42 | ### Timers 43 | 44 | Odelay defines a Timer interface responsible for delay scheduling. Timer's are defined for various backends: Jdk ScheduledExecutorSerivces, 45 | Netty HashWheeledTimers, and Twitter-util's timer's. A jdk Timer is used by default. Swap in a runtime specific module to use Netty{3,4} or 46 | Twitter Timers. 47 | 48 | See the project's [readme](https://github.com/softprops/odelay#timers) for a more in-depth overview of Timers 49 | 50 | 51 | -------------------------------------------------------------------------------- /odelay-netty3/src/main/scala/NettyTimer.scala: -------------------------------------------------------------------------------- 1 | package odelay.netty 2 | 3 | import java.util.concurrent.{ThreadFactory, TimeUnit} 4 | import java.util.concurrent.atomic.AtomicInteger 5 | import odelay.{Delay, PeriodicDelay, PeriodicPromisingDelay, PromisingDelay, Timer} 6 | import odelay.jdk.JdkTimer 7 | import org.jboss.netty.util.{HashedWheelTimer, Timeout, Timer => NTimer, TimerTask} 8 | import scala.concurrent.duration.FiniteDuration 9 | import scala.util.control.NonFatal 10 | 11 | class NettyTimer(underlying: NTimer = new HashedWheelTimer) extends Timer { 12 | def apply[T](after: FiniteDuration, op: => T): Delay[T] = 13 | new PromisingDelay[T] { 14 | private val to = 15 | try { 16 | Some( 17 | underlying.newTimeout( 18 | new TimerTask { 19 | def run(timeout: Timeout) = completePromise(op) 20 | }, 21 | after.length, 22 | after.unit 23 | ) 24 | ) 25 | } catch { 26 | case NonFatal(e) => 27 | failPromise(e) 28 | None 29 | } 30 | 31 | def cancel() = to.filterNot(_.isCancelled).foreach { f => 32 | f.cancel() 33 | cancelPromise() 34 | } 35 | } 36 | 37 | def apply[T](delay: FiniteDuration, every: FiniteDuration, op: => T): PeriodicDelay[T] = 38 | new PeriodicPromisingDelay[T](every) { 39 | var nextDelay: Option[Delay[T]] = None 40 | val to = 41 | try { 42 | Some( 43 | underlying.newTimeout( 44 | new TimerTask { 45 | def run(timeout: Timeout) = loop() 46 | }, 47 | delay.length, 48 | delay.unit 49 | ) 50 | ) 51 | } catch { 52 | case NonFatal(e) => 53 | failPromise(e) 54 | None 55 | } 56 | 57 | def loop() = 58 | if (promiseIncomplete) { 59 | op 60 | nextDelay = Some(apply(every, every, op)) 61 | } 62 | 63 | def cancel() = 64 | to.filterNot(_.isCancelled).foreach { f => 65 | synchronized { 66 | f.cancel() 67 | nextDelay.foreach(_.cancel()) 68 | cancelPromise() 69 | } 70 | } 71 | } 72 | 73 | def stop(): Unit = underlying.stop() 74 | } 75 | 76 | object NettyTimer { 77 | def newTimer: Timer = new NettyTimer(new HashedWheelTimer(JdkTimer.threadFactory, 10, TimeUnit.MILLISECONDS)) 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: ['**'] 5 | push: 6 | branches: ['**'] 7 | tags: [v*] 8 | jobs: 9 | ci: 10 | # run on 1) push, 2) external PRs, 3) softwaremill-ci PRs 11 | # do not run on internal, non-steward PRs since those will be run by push to branch 12 | if: | 13 | github.event_name == 'push' || 14 | github.event.pull_request.head.repo.full_name != github.repository || 15 | github.event.pull_request.user.login == 'softwaremill-ci' 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Set up JDK 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: 'temurin' 24 | cache: 'sbt' 25 | java-version: 21 26 | - uses: sbt/setup-sbt@3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd # v1, specifically v1.1.14 27 | - name: Compile 28 | run: sbt -v compile 29 | - name: Test 30 | run: sbt -v test 31 | 32 | publish: 33 | name: Publish release 34 | needs: [ci] 35 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 36 | runs-on: ubuntu-24.04 37 | env: 38 | JAVA_OPTS: -Xmx4G 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v2 42 | - name: Set up JDK 43 | uses: actions/setup-java@v4 44 | with: 45 | distribution: 'temurin' 46 | cache: 'sbt' 47 | java-version: 21 48 | - uses: sbt/setup-sbt@3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd # v1, specifically v1.1.14 49 | - name: Compile 50 | run: sbt compile 51 | - name: Publish artifacts 52 | run: sbt ci-release 53 | env: 54 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 55 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 56 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 57 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 58 | - name: Extract version from commit message 59 | run: | 60 | version=${GITHUB_REF/refs\/tags\/v/} 61 | echo "VERSION=$version" >> $GITHUB_ENV 62 | env: 63 | COMMIT_MSG: ${{ github.event.head_commit.message }} 64 | - name: Publish release notes 65 | uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6, specifically v6.1.0 66 | with: 67 | config-name: release-drafter.yml 68 | publish: true 69 | name: "v${{ env.VERSION }}" 70 | tag: "v${{ env.VERSION }}" 71 | version: "v${{ env.VERSION }}" 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | -------------------------------------------------------------------------------- /odelay-core/src/main/scala/Delay.scala: -------------------------------------------------------------------------------- 1 | package odelay 2 | 3 | import scala.concurrent.{Future, Promise} 4 | import scala.concurrent.duration.{Duration, FiniteDuration} 5 | import java.util.concurrent.CancellationException 6 | import scala.util.Try 7 | 8 | /** Provides an interface for producing Delays. Use requires an implicit [[odelay.Timer]] to be in implicit scope. 9 | * {{{ 10 | * val delay = odelay.Delay(2.seconds) { 11 | * todo 12 | * } 13 | * }}} 14 | */ 15 | object Delay { 16 | 17 | /** @return a one-off Delay which may be cancelled */ 18 | def apply[T](delay: FiniteDuration)(todo: => T)(implicit timer: Timer): Delay[T] = 19 | timer(delay, todo) 20 | 21 | /** @return a periodic Delay which may be cancelled */ 22 | def every[T](every: FiniteDuration)(delay: FiniteDuration = Duration.Zero)(todo: => T)(implicit 23 | timer: Timer 24 | ): PeriodicDelay[T] = 25 | timer(delay, every, todo) 26 | 27 | private[odelay] def cancel[T](p: Promise[T]) = 28 | if (!p.isCompleted) p.failure(new CancellationException) 29 | } 30 | 31 | /** A Delay is the default of a deferred operation */ 32 | trait Delay[T] { 33 | 34 | /** @return 35 | * a Future represent the execution of the Delays operation. Delays to be repeated expose a future that will never 36 | * complete until cancelled 37 | */ 38 | def future: Future[T] 39 | 40 | /** Cancels the execution of the delayed operation. Once a Delay is canceled, if additional attempts to cancel will 41 | * result in undefined behavior 42 | */ 43 | def cancel(): Unit 44 | } 45 | 46 | trait PeriodicDelay[T] extends Delay[T] { 47 | def period: FiniteDuration 48 | } 49 | 50 | /** If calling cancel on a Delay's implemention has no other effect than cancelling the underlying promise. Use this as 51 | * a mix in. 52 | * {{{ 53 | * val timer = new Timer { 54 | * def apply(delay: FiniteDuration, op: => T) = new PromisingDelay[T] with SelfCancelation[T] { 55 | * schedule(delay, completePromise(op)) 56 | * } 57 | * ... 58 | * } 59 | * }}} 60 | */ 61 | trait SelfCancelation[T] { self: PromisingDelay[T] => 62 | def cancel() = cancelPromise() 63 | } 64 | 65 | /** A building block for writing your own [[odelay.Timer]]. Call `completePromise(_)` with the value of the result of 66 | * the operation. Call `cancelPromise()` to cancel it. To query the current state of the promise, use 67 | * `promiseIncomplete` 68 | */ 69 | abstract class PromisingDelay[T] extends Delay[T] { 70 | private val promise = Promise[T]() 71 | 72 | /** Cancels the underlying promise if it's not already completed */ 73 | protected def cancelPromise(): Unit = 74 | Delay.cancel(promise) 75 | 76 | /** if it's not already completed */ 77 | protected def failPromise(why: Throwable): Unit = 78 | if (promiseIncomplete) promise.failure(why) 79 | 80 | /** Completes the Promise with a success if the promise is not already completed */ 81 | protected def completePromise(value: => T): Unit = 82 | if (promiseIncomplete) promise.complete(Try(value)) 83 | 84 | /** @return true if the promise is not completed */ 85 | protected def promiseIncomplete = 86 | !promise.isCompleted 87 | 88 | /** @return a Future view of the timeouts Promise */ 89 | def future: Future[T] = promise.future 90 | } 91 | 92 | abstract class PeriodicPromisingDelay[T](val period: FiniteDuration) extends PromisingDelay[T] with PeriodicDelay[T] 93 | -------------------------------------------------------------------------------- /odelay-testing/src/test/scalajvm/TimerSpec.scala: -------------------------------------------------------------------------------- 1 | package odelay.testing 2 | 3 | import odelay.{Delay, Timer} 4 | import org.scalatest.BeforeAndAfterAll 5 | import org.scalatest.funspec.AsyncFunSpec 6 | import scala.concurrent._ 7 | import scala.concurrent.duration._ 8 | import java.util.concurrent.CancellationException 9 | import java.util.concurrent.atomic.AtomicInteger 10 | import scala.util.Failure 11 | 12 | trait TimerSpec extends AsyncFunSpec with BeforeAndAfterAll { 13 | 14 | implicit def ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global 15 | 16 | def newTimer: Timer 17 | def timerName: String 18 | implicit val timer: Timer = newTimer 19 | 20 | describe(timerName) { 21 | it("should execute an operation after an initial delay") { 22 | val start = System.currentTimeMillis 23 | val fut = Delay(1.seconds) { 24 | System.currentTimeMillis - start 25 | }.future 26 | fut.map(value => assert(value.millis.toSeconds === 1)) 27 | } 28 | 29 | it("should permit cancellation of delayed operations") { 30 | val cancel = Delay(1.seconds)(sys.error("this should never print")) 31 | Delay(150.millis) { 32 | cancel.cancel() 33 | }.future.map(_ => succeed) 34 | } 35 | 36 | it("cancellation of delayed operations should result in future failure") { 37 | val cancel = Delay(1.second)(sys.error("this should never print")) 38 | val cancelF = Delay(150.millis) { 39 | cancel.cancel() 40 | } 41 | for { 42 | cf <- cancelF.future 43 | c <- cancel.future.failed 44 | } yield { 45 | assert(c.getClass === classOf[CancellationException]) 46 | } 47 | } 48 | 49 | it("successful completion of delayed operations should result in a future success") { 50 | val future = Delay(1.second)(true).future 51 | future.map { value => assert(value === true) } 52 | } 53 | 54 | it("unsuccessful completion of delayed operations should result in a future failure") { 55 | case object CustomException extends Exception 56 | 57 | val future = Delay(1.second)(throw CustomException).future 58 | future.transformWith { 59 | case Failure(exception) => assert(exception === CustomException) 60 | case _ => fail("The delayed future was expected to fail") 61 | } 62 | } 63 | 64 | it("should repeatedly execute an operation on a fixed delay") { 65 | val start = System.currentTimeMillis 66 | val delay = Delay.every(150.millis)() { 67 | val diff = System.currentTimeMillis - start 68 | print('.') 69 | diff 70 | } 71 | delay.future.failed.foreach { _ => println() } 72 | Delay(2.seconds) { 73 | delay.cancel() 74 | }.future.map(_ => succeed) 75 | } 76 | 77 | it("cancellation of repeatedly delayed operations should result in future failure") { 78 | val counter = new AtomicInteger(0) 79 | val cancel = Delay.every(150.seconds)()(true) 80 | val cancelFut = cancel.future.recoverWith { 81 | case e: CancellationException => 82 | counter.incrementAndGet() 83 | Future.successful(true) 84 | case _ => Future.successful(true) 85 | } 86 | val fut = Delay(2.seconds) { 87 | cancel.cancel() 88 | } 89 | 90 | cancelFut.map(_ => assert(counter.get() === 1)) 91 | } 92 | 93 | } 94 | 95 | override def afterAll(): Unit = { 96 | timer.stop() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /odelay-testing/src/test/scalajs/TimerSpec.scala: -------------------------------------------------------------------------------- 1 | package odelay.testing 2 | 3 | import odelay.{Delay, Timer} 4 | import org.scalatest.BeforeAndAfterAll 5 | import org.scalatest.funspec.AsyncFunSpec 6 | 7 | import scala.concurrent.Await 8 | import scala.concurrent.duration._ 9 | import scala.util.control.NonFatal 10 | import java.util.concurrent.CancellationException 11 | import java.util.concurrent.atomic.AtomicInteger 12 | import odelay.js._ 13 | 14 | import scala.util.Failure 15 | 16 | class TimerSpec extends AsyncFunSpec with BeforeAndAfterAll { 17 | 18 | // needed so we do not get a scalatest EC error 19 | implicit override def executionContext = 20 | scala.concurrent.ExecutionContext.Implicits.global 21 | 22 | val newTimer: Timer = JsTimer.newTimer 23 | val timerName: String = "jstimer" 24 | implicit val timer: Timer = newTimer 25 | 26 | describe(timerName) { 27 | it("should execute an operation after an initial delay") { 28 | val start = System.currentTimeMillis 29 | val fut = Delay(1.seconds) { 30 | System.currentTimeMillis - start 31 | }.future 32 | fut.map(value => assert(value.millis.toSeconds.seconds === 1.seconds)) 33 | } 34 | 35 | it("should permit cancellation of delayed operations") { 36 | val cancel = Delay(1.seconds)(sys.error("this should never print")) 37 | val fut = Delay(150.millis) { 38 | cancel.cancel() 39 | }.future 40 | fut.map(_ => succeed) 41 | } 42 | 43 | it("cancellation of delayed operations should result in future failure") { 44 | val cancel = Delay(1.second)(sys.error("this should never print")) 45 | Delay(150.millis) { 46 | cancel.cancel() 47 | } 48 | cancel.future.recover { 49 | case x: CancellationException => succeed 50 | case _ => fail() 51 | } 52 | } 53 | 54 | it("successful completion of delayed operations should result in a future success") { 55 | val future = Delay(1.second)(true).future 56 | future 57 | .recover { case NonFatal(_) => 58 | sys.error("this should never print") 59 | } 60 | .map(value => assert(value === true)) 61 | } 62 | 63 | it("successful completion of delayed operations should result in a future failure") { 64 | case object CustomException extends Exception 65 | 66 | val future = Delay(1.second)(throw CustomException).future 67 | future.transformWith { 68 | case Failure(exception) => assert(exception === CustomException) 69 | case _ => fail("The delayed future was expected to fail") 70 | } 71 | } 72 | 73 | it("should repeatedly execute an operation on a fixed delay") { 74 | val start = System.currentTimeMillis 75 | val delay = Delay.every(150.millis)() { 76 | val diff = System.currentTimeMillis - start 77 | print('.') 78 | diff 79 | } 80 | delay.future.failed.foreach { _ => println() } 81 | val cancelit = Delay(5.seconds) { 82 | delay.cancel() 83 | } 84 | cancelit.future.map(_ => succeed) 85 | } 86 | 87 | it("cancellation of repeatedly delayed operations should result in future failure") { 88 | val cancel = Delay.every(150.seconds)()(true) 89 | val counter = new AtomicInteger(0) 90 | cancel.future.failed.foreach { 91 | case NonFatal(e) => 92 | assert(e.getClass === classOf[CancellationException]) 93 | counter.incrementAndGet() 94 | case _ => 95 | } 96 | 97 | val canceltrueloop = Delay(2.seconds) { 98 | cancel.cancel() 99 | } 100 | canceltrueloop.future.map(_ => assert(counter.get() === 1)) 101 | } 102 | 103 | } 104 | 105 | override def afterAll(): Unit = { 106 | timer.stop() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /odelay-core/src/main/scalajvm/jdk/JdkTimer.scala: -------------------------------------------------------------------------------- 1 | package odelay.jdk 2 | 3 | import java.util.concurrent.{ 4 | Future => JFuture, 5 | RejectedExecutionHandler, 6 | ScheduledExecutorService, 7 | ScheduledThreadPoolExecutor, 8 | ThreadFactory 9 | } 10 | import java.util.concurrent.atomic.AtomicInteger 11 | import odelay.{Delay, PeriodicDelay, PeriodicPromisingDelay, PromisingDelay, Timer} 12 | import scala.concurrent.Promise 13 | import scala.concurrent.duration.FiniteDuration 14 | import scala.util.control.NonFatal 15 | 16 | /** A Timer implemented in terms of a jdk ScheduledThreadPoolExecutor */ 17 | class JdkTimer(underlying: ScheduledExecutorService, interruptOnCancel: Boolean) extends Timer { 18 | 19 | /** customizing constructor */ 20 | def this( 21 | poolSize: Int = JdkTimer.poolSize, 22 | threads: ThreadFactory = JdkTimer.threadFactory, 23 | handler: Option[RejectedExecutionHandler] = JdkTimer.rejectionHandler, 24 | interruptOnCancel: Boolean = JdkTimer.interruptOnCancel 25 | ) = 26 | this( 27 | handler 28 | .map(rejections => new ScheduledThreadPoolExecutor(poolSize, threads, rejections)) 29 | .getOrElse(new ScheduledThreadPoolExecutor(poolSize, threads)), 30 | interruptOnCancel 31 | ) 32 | 33 | def apply[T](delay: FiniteDuration, op: => T): Delay[T] = 34 | new PromisingDelay[T] { 35 | val jfuture: Option[JFuture[_]] = 36 | try { 37 | Some( 38 | underlying.schedule( 39 | new Runnable { 40 | def run() = completePromise(op) 41 | }, 42 | delay.length, 43 | delay.unit 44 | ) 45 | ) 46 | } catch { 47 | case NonFatal(e) => 48 | failPromise(e) 49 | None 50 | } 51 | 52 | def cancel() = jfuture.filterNot(_.isCancelled).foreach { f => 53 | f.cancel(interruptOnCancel) 54 | cancelPromise() 55 | } 56 | } 57 | 58 | def apply[T](delay: FiniteDuration, every: FiniteDuration, op: => T): PeriodicDelay[T] = 59 | new PeriodicPromisingDelay[T](every) { 60 | val jfuture: Option[JFuture[_]] = 61 | try { 62 | Some( 63 | underlying.scheduleWithFixedDelay( 64 | new Runnable { 65 | def run = if (promiseIncomplete) op 66 | }, 67 | delay.toUnit(every.unit).toLong, 68 | every.length, 69 | every.unit 70 | ) 71 | ) 72 | } catch { 73 | case NonFatal(e) => 74 | failPromise(e) 75 | None 76 | } 77 | 78 | def cancel() = jfuture.filterNot(_.isCancelled).foreach { f => 79 | f.cancel(interruptOnCancel) 80 | cancelPromise() 81 | } 82 | } 83 | 84 | def stop() = if (!underlying.isShutdown) underlying.shutdownNow() 85 | } 86 | 87 | /** defaults for jdk timers */ 88 | object JdkTimer { 89 | lazy val poolSize = Runtime.getRuntime().availableProcessors() 90 | 91 | /** @return a new ThreadFactory with that produces new threads named odelay-{threadNum} */ 92 | def threadFactory: ThreadFactory = new ThreadFactory { 93 | val grp = new ThreadGroup(Thread.currentThread().getThreadGroup(), "odelay") 94 | val threads = new AtomicInteger(1) 95 | def newThread(runs: Runnable) = 96 | new Thread(grp, runs, "odelay-%s" format threads.getAndIncrement()) { 97 | setDaemon(true) 98 | } 99 | } 100 | 101 | val rejectionHandler: Option[RejectedExecutionHandler] = None 102 | 103 | val interruptOnCancel = true 104 | 105 | /** @return a _new_ Timer. when used clients should be sure to call stop() on all instances for a clean shutdown */ 106 | def newTimer: Timer = new JdkTimer(poolSize, threadFactory, rejectionHandler, interruptOnCancel) 107 | } 108 | -------------------------------------------------------------------------------- /odelay-netty/src/main/scala/NettyTimer.scala: -------------------------------------------------------------------------------- 1 | package odelay.netty 2 | 3 | import io.netty.util.{HashedWheelTimer, Timeout, Timer => NTimer, TimerTask} 4 | import io.netty.util.concurrent.{EventExecutorGroup, Future => NFuture} 5 | import odelay.{Delay, PeriodicDelay, PeriodicPromisingDelay, PromisingDelay, Timer} 6 | import odelay.jdk.JdkTimer 7 | import java.util.concurrent.TimeUnit 8 | import scala.concurrent.duration.FiniteDuration 9 | import scala.util.control.NonFatal 10 | 11 | class NettyGroupTimer(grp: EventExecutorGroup, interruptOnCancel: Boolean = NettyTimer.interruptOnCancel) 12 | extends Timer { 13 | 14 | def apply[T](delay: FiniteDuration, op: => T): Delay[T] = 15 | new PromisingDelay[T] { 16 | val sf: Option[NFuture[_]] = 17 | try { 18 | Some( 19 | grp.schedule( 20 | new Runnable { 21 | def run = completePromise(op) 22 | }, 23 | delay.length, 24 | delay.unit 25 | ) 26 | ) 27 | } catch { 28 | case NonFatal(e) => 29 | failPromise(e) 30 | None 31 | } 32 | 33 | def cancel() = sf.filterNot(_.isCancelled).foreach { f => 34 | f.cancel(interruptOnCancel) 35 | cancelPromise() 36 | } 37 | } 38 | 39 | def apply[T](delay: FiniteDuration, every: FiniteDuration, op: => T): PeriodicDelay[T] = 40 | new PeriodicPromisingDelay[T](every) { 41 | val sf: Option[NFuture[_]] = 42 | try { 43 | Some( 44 | grp.scheduleWithFixedDelay( 45 | new Runnable { 46 | def run = if (promiseIncomplete) op 47 | }, 48 | delay.toUnit(every.unit).toLong, 49 | every.length, 50 | every.unit 51 | ) 52 | ) 53 | } catch { 54 | case NonFatal(e) => 55 | failPromise(e) 56 | None 57 | } 58 | 59 | def cancel() = sf.filterNot(_.isCancelled).foreach { f => 60 | f.cancel(interruptOnCancel) 61 | cancelPromise() 62 | } 63 | } 64 | 65 | def stop() = if (!grp.isShuttingDown()) grp.shutdownGracefully() 66 | } 67 | 68 | class NettyTimer(underlying: NTimer = new HashedWheelTimer) extends Timer { 69 | 70 | def apply[T](delay: FiniteDuration, op: => T): Delay[T] = 71 | new PromisingDelay[T] { 72 | val to = 73 | try { 74 | Some( 75 | underlying.newTimeout( 76 | new TimerTask { 77 | def run(timeout: Timeout) = 78 | completePromise(op) 79 | }, 80 | delay.length, 81 | delay.unit 82 | ) 83 | ) 84 | } catch { 85 | case NonFatal(e) => 86 | failPromise(e) 87 | None 88 | } 89 | 90 | def cancel() = to.filterNot(_.isCancelled).foreach { f => 91 | f.cancel() 92 | cancelPromise() 93 | } 94 | } 95 | 96 | def apply[T](delay: FiniteDuration, every: FiniteDuration, op: => T): PeriodicDelay[T] = 97 | new PeriodicPromisingDelay[T](every) { 98 | var nextDelay: Option[Delay[T]] = None 99 | val to = 100 | try { 101 | Some( 102 | underlying.newTimeout( 103 | new TimerTask { 104 | def run(timeout: Timeout) = loop() 105 | }, 106 | delay.length, 107 | delay.unit 108 | ) 109 | ) 110 | } catch { 111 | case NonFatal(e) => 112 | failPromise(e) 113 | None 114 | } 115 | 116 | def loop() = 117 | if (promiseIncomplete) { 118 | op 119 | nextDelay = Some(apply(every, every, op)) 120 | } 121 | 122 | def cancel() = 123 | to.filterNot(_.isCancelled).foreach { f => 124 | synchronized { 125 | f.cancel() 126 | nextDelay.foreach(_.cancel()) 127 | cancelPromise() 128 | } 129 | } 130 | } 131 | 132 | def stop(): Unit = underlying.stop() 133 | } 134 | 135 | object NettyTimer { 136 | val interruptOnCancel = true 137 | 138 | def groupTimer(grp: EventExecutorGroup) = 139 | new NettyGroupTimer(grp) 140 | 141 | /** @return a _new_ NettyTimer backed by a HashedWheelTimer */ 142 | def newTimer: Timer = new NettyTimer(new HashedWheelTimer(JdkTimer.threadFactory, 10, TimeUnit.MILLISECONDS)) 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # odelay 2 | 3 | [![Build Status](https://travis-ci.org/softwaremill/odelay.png?branch=master)](https://travis-ci.org/softwaremill/odelay) 4 | 5 | Delayed reactions, fashioned from tools you already have sitting around your shed. 6 | 7 | ## installation 8 | 9 | The current version of odelay is `0.4.1` and targets scala 2.12+. The odelay-twitter module is not published for 2.11.*. 10 | 11 | ### modules 12 | 13 | * `odelay-core` odelay core interfaces and default jdk backed timer 14 | 15 | ```scala 16 | libraryDependencies += "com.softwaremill.odelay" %% "odelay-core" % "0.4.1" 17 | ``` 18 | 19 | * `odelay-netty` netty 4 backed odelay timer interface 20 | 21 | ```scala 22 | libraryDependencies += "com.softwaremill.odelay" %% "odelay-netty" % "0.4.1" 23 | ``` 24 | 25 | * `odelay-netty3` netty 3 backed odelay timer interface 26 | 27 | ```scala 28 | libraryDependencies += "com.softwaremill.odelay" %% "odelay-netty3" % "0.4.1" 29 | ``` 30 | 31 | * `odelay-twitter` twitter util backed odelay timer interface 32 | 33 | ```scala 34 | libraryDependencies += "com.softwaremill.odelay" %% "odelay-twitter" % "0.4.1" 35 | ``` 36 | 37 | ## usage 38 | 39 | Odelay provides a simple interface producing Delays. Delays are to operations as [Futures][fut] are to values, for given [FiniteDurations][fd]. 40 | 41 | ### primitives 42 | 43 | Odelay separates execution from interface by defining two primitives: 44 | 45 | * an `odelay.Timer`, which defers task execution 46 | * an `odelay.Delay`, which represents a delayed operation. 47 | 48 | A delayed operation requires a [FiniteDuration][fd] and some arbitrary block of code which will execute after that duration. 49 | 50 | Typical usage is as follows. 51 | 52 | ```scala 53 | import scala.concurrent.duration._ 54 | 55 | // print "executed" after a 2 second delay 56 | odelay.Delay(2.seconds) { 57 | println("executed") 58 | } 59 | ``` 60 | 61 | ### Timers 62 | 63 | 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. 64 | 65 | `odelay.Timers` define an interface for task scheduling. Implementations of `odelay.Timers` are defined for a number of environments and platforms. 66 | 67 | #### JdkTimer 68 | 69 | The default Timer is a standard jdk [ScheduledExecutorService][ses] backed Timer. 70 | 71 | To make the example above compile, import the default `Timer`. 72 | 73 | ```scala 74 | import scala.concurrent.duration._ 75 | 76 | // bring default timer into scope 77 | import odelay.Timer.default 78 | 79 | odelay.Delay(2.seconds) { 80 | println("executed") 81 | } 82 | ``` 83 | 84 | If you have already allocated your own [ScheduledExecutorService][ses], you may define your own jdk timer reusing those thread resources and bring that into implicit scope. 85 | 86 | ```scala 87 | import scala.concurrent.duration._ 88 | 89 | // define a new JdkTimer instance with resources preallocated 90 | implicit val myJdkTimer = new odelay.jdk.JdkTimer( 91 | myScheduledExecutorService, interuptOnCancel) 92 | 93 | odelay.Delay(2.seconds) { 94 | println("executed") 95 | } 96 | ``` 97 | 98 | #### Netty(3)Timers 99 | 100 | If your application's classpath includes [netty][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][hwt]. 101 | 102 | To use one of these, bring an instance of the default netty timer into scope 103 | 104 | ```scala 105 | import scala.concurrent.duration._ 106 | 107 | // create a new netty timer and bring it into explicit scope 108 | implicit val timer = odelay.netty.NettyTimer.newTimer 109 | 110 | odelay.Delay(2.seconds) { 111 | println("executed") 112 | } 113 | ``` 114 | 115 | If your application has already allocated a HashedWheelTimer, you can easily create your own odelay.Timer instance backed with resources you have 116 | already allocated. 117 | 118 | ```scala 119 | import scala.concurrent.duration._ 120 | 121 | implicit val timer = new odelay.netty.NettyTimer(myHashedWheelTimer) 122 | 123 | odelay.Delay(2.seconds) { 124 | println("executed") 125 | } 126 | ``` 127 | 128 | 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 129 | netty pipeline. To create a Timer instance from one of those, you can do the following 130 | 131 | ```scala 132 | import scala.concurrent.duration._ 133 | 134 | implicit val timer = new odelay.netty.NettyGroupTimer( 135 | myEventExecutorGroup) 136 | 137 | odelay.Delay(2.seconds) { 138 | println("executed") 139 | } 140 | ``` 141 | 142 | #### TwitterTimers 143 | 144 | If your application has the [twitter util][tu] 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` 145 | 146 | ```scala 147 | import scala.concurrent.duration._ 148 | implicit val timer = odelay.twitter.TwitterTimer.newTimer 149 | odelay.Delay(2.seconds) { 150 | println("executed") 151 | } 152 | ``` 153 | 154 | You may also define your own `odelay.Timer` in terms of a `com.twitter.util.Timer` which you may already have in scope. 155 | 156 | ```scala 157 | import scala.concurrent.duration._ 158 | implicit val timer = new odelay.twitter.TwitterTimer(myTwitterTimer) 159 | odelay.Delay(2.seconds) { 160 | println("executed") 161 | } 162 | ``` 163 | 164 | ### Releasing resources 165 | 166 | `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. 167 | Depending on your applications needs, you should really only need _one_ instance of an `odelay.Timer` for a given process. 168 | 169 | 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 170 | a new Delay is attempted with the stopped timer. 171 | 172 | ### Periodic delays 173 | 174 | Odelay also provides an interface for cases where you wish to execute a task on a repeating interval of periodic delays. 175 | 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. 176 | 177 | The following example will print "executed" every two seconds until the resulting time out is canceled or the timer is stopped. 178 | 179 | ```scala 180 | import scala.concurrent.duration._ 181 | 182 | import odelay.Timer.default 183 | 184 | odelay.Delay.every(2.seconds)() { 185 | println("executed") 186 | } 187 | ``` 188 | 189 | ### Delays 190 | 191 | Like [Futures][fut], 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. 192 | 193 | 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. 194 | 195 | `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)`. 196 | 197 | Below is an example of reacting the a delay's execution. 198 | 199 | ```scala 200 | import scala.concurrent.duration._ 201 | 202 | // future execution 203 | import scala.concurrent.ExecutionContext.Implicits.global 204 | 205 | // delay execution 206 | import odelay.Timer.default 207 | 208 | odelay.Delay(2.seconds) { 209 | println("executed") 210 | }.future.onSuccess { 211 | case _ => println("task scheduled") 212 | } 213 | ``` 214 | 215 | Note, the import of the `ExecutionContext`. An implicit instance of one must be in scope for the invocation of a Future's `onSuccess` method. 216 | 217 | #### Periodically Delayed futures 218 | 219 | 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. 220 | 221 | However, a canceled periodic delay will satisfy a periodic delay's Future in a failure state. 222 | 223 | ```scala 224 | import scala.concurrent.duration._ 225 | import scala.concurrent.ExecutionContext.Implicits.global 226 | import odelay.Timer.default 227 | 228 | val delay = odelay.Delay.every(2.seconds)() { 229 | println("executed") 230 | } 231 | 232 | delay.future.onSuccess { 233 | case _ => println("this will never get called") 234 | } 235 | 236 | delay.future.onFailure { 237 | case _ => println("this can get called, if you call delay.cancel()") 238 | } 239 | ``` 240 | 241 | ## Credits 242 | 243 | Originally created by [Doug Tangren](https://github.com/softprops), maintained by [SoftwareMill](https://softwaremill.com). 244 | 245 | [fd]: http://www.scala-lang.org/api/current/index.html#scala.concurrent.duration.FiniteDuration 246 | [fut]: http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future 247 | [ses]: http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ScheduledExecutorService.html 248 | [netty]: http://netty.io/ 249 | [hwt]: http://netty.io/4.0/api/io/netty/util/HashedWheelTimer.html 250 | [tu]: http://twitter.github.io/util/ 251 | --------------------------------------------------------------------------------