From 4b632c4537bd20c9ef8933219f34dec98b787548 Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Tue, 26 Nov 2019 14:26:49 +0100 Subject: [PATCH] Add convenience method to start timer without key (#27875) * Add convenience method to start timer without key It is probably common that there is no need to allow different timers that send the same message, and this makes that more convenient to write. Updated one method to gather feedback, if we like the change I can apply it to the others as well. * Add alternative to all typed timer API's Update java/scaladoc, update tests Not updated classic actors and FSM API's --- .../typed/javadsl/ManualTimerExampleTest.java | 8 +- .../typed/scaladsl/TestProbeSpec.scala | 2 +- .../scaladsl/ManualTimerExampleSpec.scala | 4 +- .../actor/typed/javadsl/ActorCompile.java | 9 ++ .../akka/typed/StyleGuideDocExamples.java | 10 +-- .../scala/akka/actor/typed/TimerSpec.scala | 28 +++---- .../actor/typed/TransformMessagesSpec.scala | 4 +- .../scala/docs/akka/typed/FSMDocSpec.scala | 2 +- .../akka/typed/StyleGuideDocExamples.scala | 12 +-- .../scala/docs/akka/typed/TailChopping.scala | 4 +- .../actor/typed/javadsl/TimerScheduler.scala | 82 ++++++++++++++++--- .../actor/typed/scaladsl/TimerScheduler.scala | 80 ++++++++++++++++-- .../main/scala/akka/actor/AbstractFSM.scala | 18 ++-- .../src/main/scala/akka/actor/FSM.scala | 18 ++-- .../src/main/scala/akka/actor/Timers.scala | 36 ++++---- .../receptionist/ClusterReceptionist.scala | 4 +- .../paradox/typed/interaction-patterns.md | 2 +- .../typed/tutorial_5/DeviceGroupQuery.java | 2 +- .../scaladsl/EventSourcedBehaviorSpec.scala | 6 +- .../PersistentActorCompileOnlyTest.scala | 2 +- .../persistence/fsm/PersistentFSMBase.scala | 36 ++++---- 21 files changed, 253 insertions(+), 116 deletions(-) diff --git a/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/ManualTimerExampleTest.java b/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/ManualTimerExampleTest.java index ffb8c1ce48..5de218204f 100644 --- a/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/ManualTimerExampleTest.java +++ b/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/ManualTimerExampleTest.java @@ -30,7 +30,11 @@ public class ManualTimerExampleTest extends JUnitSuite { private final ManualTime manualTime = ManualTime.get(testKit.system()); - static final class Tick {} + static final class Tick { + private Tick() {} + + static final Tick INSTANCE = new Tick(); + } static final class Tock {} @@ -40,7 +44,7 @@ public class ManualTimerExampleTest extends JUnitSuite { Behavior behavior = Behaviors.withTimers( timer -> { - timer.startSingleTimer("T", new Tick(), Duration.ofMillis(10)); + timer.startSingleTimer(Tick.INSTANCE, Duration.ofMillis(10)); return Behaviors.receiveMessage( tick -> { probe.ref().tell(new Tock()); diff --git a/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/TestProbeSpec.scala b/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/TestProbeSpec.scala index 1909feae3e..f821b7236c 100644 --- a/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/TestProbeSpec.scala +++ b/akka-actor-testkit-typed/src/test/scala/akka/actor/testkit/typed/scaladsl/TestProbeSpec.scala @@ -47,7 +47,7 @@ class TestProbeSpec extends ScalaTestWithActorTestKit with WordSpecLike with Log val probe = TestProbe() val ref = spawn(Behaviors.receive[Stop.type]((_, _) => Behaviors.withTimers { timer => - timer.startSingleTimer("key", Stop, 300.millis) + timer.startSingleTimer(Stop, 300.millis) Behaviors.receive((_, _) => Behaviors.stopped) })) diff --git a/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/ManualTimerExampleSpec.scala b/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/ManualTimerExampleSpec.scala index adc78fdfc1..5e8ee26084 100644 --- a/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/ManualTimerExampleSpec.scala +++ b/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/ManualTimerExampleSpec.scala @@ -25,7 +25,7 @@ class ManualTimerExampleSpec extends ScalaTestWithActorTestKit(ManualTime.config val probe = TestProbe[Tock.type]() val behavior = Behaviors.withTimers[Tick.type] { timer => - timer.startSingleTimer("T", Tick, 10.millis) + timer.startSingleTimer(Tick, 10.millis) Behaviors.receiveMessage { _ => probe.ref ! Tock Behaviors.same @@ -49,7 +49,7 @@ class ManualTimerExampleSpec extends ScalaTestWithActorTestKit(ManualTime.config val probe = TestProbe[Tock.type]() val behavior = Behaviors.withTimers[Tick.type] { timer => - timer.startTimerWithFixedDelay("T", Tick, 10.millis) + timer.startTimerWithFixedDelay(Tick, 10.millis) Behaviors.receiveMessage { _ => probe.ref ! Tock Behaviors.same diff --git a/akka-actor-typed-tests/src/test/java/akka/actor/typed/javadsl/ActorCompile.java b/akka-actor-typed-tests/src/test/java/akka/actor/typed/javadsl/ActorCompile.java index dc8fb0b6a5..bdba59ce29 100644 --- a/akka-actor-typed-tests/src/test/java/akka/actor/typed/javadsl/ActorCompile.java +++ b/akka-actor-typed-tests/src/test/java/akka/actor/typed/javadsl/ActorCompile.java @@ -133,6 +133,15 @@ public class ActorCompile { }); } + { + Behavior b = + Behaviors.withTimers( + timers -> { + timers.startTimerWithFixedDelay(new MyMsgB("tick"), Duration.ofSeconds(1)); + return Behaviors.ignore(); + }); + } + static class MyBehavior extends ExtensibleBehavior { @Override diff --git a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java index 84001ca736..ad3add1f2f 100644 --- a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java +++ b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java @@ -211,7 +211,7 @@ interface StyleGuideDocExamples { name, command.interval, n); - timers.startTimerWithFixedDelay("repeat", Increment.INSTANCE, command.interval); + timers.startTimerWithFixedDelay(Increment.INSTANCE, command.interval); return Behaviors.same(); } @@ -306,7 +306,7 @@ interface StyleGuideDocExamples { setup.name, command.interval, n); - setup.timers.startTimerWithFixedDelay("repeat", Increment.INSTANCE, command.interval); + setup.timers.startTimerWithFixedDelay(Increment.INSTANCE, command.interval); return Behaviors.same(); } @@ -394,7 +394,7 @@ interface StyleGuideDocExamples { name, command.interval, n); - timers.startTimerWithFixedDelay("repeat", Increment.INSTANCE, command.interval); + timers.startTimerWithFixedDelay(Increment.INSTANCE, command.interval); return Behaviors.same(); } @@ -552,7 +552,7 @@ interface StyleGuideDocExamples { context -> Behaviors.withTimers( timers -> { - timers.startTimerWithFixedDelay("tick", Tick.INSTANCE, tickInterval); + timers.startTimerWithFixedDelay(Tick.INSTANCE, tickInterval); return new Counter(name, context); })); } @@ -688,7 +688,7 @@ interface StyleGuideDocExamples { (ActorContext context) -> Behaviors.withTimers( timers -> { - timers.startTimerWithFixedDelay("tick", Tick.INSTANCE, tickInterval); + timers.startTimerWithFixedDelay(Tick.INSTANCE, tickInterval); return new Counter(name, context); })) .narrow(); // note narrow here diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala index 7396b9f3ee..8b374d8754 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala @@ -88,7 +88,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "schedule non-repeated ticks" taggedAs TimingTest in { val probe = TestProbe[Event]("evt") val behv = Behaviors.withTimers[Command] { timer => - timer.startSingleTimer("T", Tick(1), 10.millis) + timer.startSingleTimer(Tick(1), 10.millis) target(probe.ref, timer, 1) } @@ -103,7 +103,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "schedule repeated ticks" taggedAs TimingTest in { val probe = TestProbe[Event]("evt") val behv = Behaviors.withTimers[Command] { timer => - timer.startTimerWithFixedDelay("T", Tick(1), interval) + timer.startTimerWithFixedDelay(Tick(1), interval) target(probe.ref, timer, 1) } @@ -121,7 +121,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "replace timer" taggedAs TimingTest in { val probe = TestProbe[Event]("evt") val behv = Behaviors.withTimers[Command] { timer => - timer.startTimerWithFixedDelay("T", Tick(1), interval) + timer.startTimerWithFixedDelay(Tick(1), interval) target(probe.ref, timer, 1) } @@ -141,7 +141,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "cancel timer" taggedAs TimingTest in { val probe = TestProbe[Event]("evt") val behv = Behaviors.withTimers[Command] { timer => - timer.startTimerWithFixedDelay("T", Tick(1), interval) + timer.startTimerWithFixedDelay(Tick(1), interval) target(probe.ref, timer, 1) } @@ -193,7 +193,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt val probe = TestProbe[Event]("evt") val behv = Behaviors .supervise(Behaviors.withTimers[Command] { timer => - timer.startTimerWithFixedDelay("T", Tick(1), interval) + timer.startTimerWithFixedDelay(Tick(1), interval) target(probe.ref, timer, 1) }) .onFailure[Exception](SupervisorStrategy.restart) @@ -222,7 +222,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "cancel timers when stopped from exception" taggedAs TimingTest in { val probe = TestProbe[Event]() val behv = Behaviors.withTimers[Command] { timer => - timer.startTimerWithFixedDelay("T", Tick(1), interval) + timer.startTimerWithFixedDelay(Tick(1), interval) target(probe.ref, timer, 1) } val ref = spawn(behv) @@ -235,7 +235,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "cancel timers when stopped voluntarily" taggedAs TimingTest in { val probe = TestProbe[Event]() val behv = Behaviors.withTimers[Command] { timer => - timer.startTimerWithFixedDelay("T", Tick(1), interval) + timer.startTimerWithFixedDelay(Tick(1), interval) target(probe.ref, timer, 1) } val ref = spawn(behv) @@ -246,9 +246,9 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "allow for nested timers" in { val probe = TestProbe[String]() val ref = spawn(Behaviors.withTimers[String] { outerTimer => - outerTimer.startTimerWithFixedDelay("outer-key", "outer-message", 50.millis) + outerTimer.startTimerWithFixedDelay("outer-message", 50.millis) Behaviors.withTimers { innerTimer => - innerTimer.startTimerWithFixedDelay("inner-key", "inner-message", 50.millis) + innerTimer.startTimerWithFixedDelay("inner-message", 50.millis) Behaviors.receiveMessage { message => if (message == "stop") Behaviors.stopped else { @@ -273,7 +273,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt "keep timers when behavior changes" in { val probe = TestProbe[String]() def newBehavior(n: Int): Behavior[String] = Behaviors.withTimers[String] { timers => - timers.startTimerWithFixedDelay(s"key${n}", s"message${n}", 50.milli) + timers.startTimerWithFixedDelay(s"message${n}", 50.milli) Behaviors.receiveMessage { message => if (message == "stop") Behaviors.stopped else { @@ -299,7 +299,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt val probe = TestProbe[DeadLetter]() val ref = spawn(Behaviors.withTimers[String] { timers => Behaviors.setup { _ => - timers.startTimerWithFixedDelay("test", "test", 250.millis) + timers.startTimerWithFixedDelay("test", 250.millis) Behaviors.receive { (context, _) => Behaviors.stopped(() => context.log.info(s"stopping")) } @@ -323,11 +323,11 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt case Tick(-1) => probe.ref ! Tock(-1) Behaviors.withTimers[Command] { timer => - timer.startSingleTimer("T0", Tick(0), 5.millis) + timer.startSingleTimer(Tick(0), 5.millis) Behaviors.receiveMessage[Command] { case Tick(0) => probe.ref ! Tock(0) - timer.startSingleTimer("T1", Tick(1), 5.millis) + timer.startSingleTimer(Tick(1), 5.millis) // let Tick(0) arrive in mailbox, test will not fail if it arrives later Thread.sleep(100) throw TestException("boom") @@ -365,7 +365,7 @@ class TimerSpec extends ScalaTestWithActorTestKit with WordSpecLike with LogCapt case Tick(-1) => probe.ref ! Tock(-1) Behaviors.withTimers[Command] { timer => - timer.startSingleTimer("T0", Tick(0), 5.millis) + timer.startSingleTimer(Tick(0), 5.millis) // let Tick(0) arrive in mailbox, test will not fail if it arrives later Thread.sleep(100) throw TestException("boom") diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TransformMessagesSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TransformMessagesSpec.scala index 6595f40129..b54d5e24d5 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TransformMessagesSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TransformMessagesSpec.scala @@ -148,7 +148,7 @@ class TransformMessagesSpec extends ScalaTestWithActorTestKit with WordSpecLike val probe = TestProbe[String]() val behv = Behaviors .withTimers[String] { timers => - timers.startSingleTimer("timer", "a", 10.millis) + timers.startSingleTimer("a", 10.millis) Behaviors.receiveMessage { msg => probe.ref ! msg Behaviors.same @@ -169,7 +169,7 @@ class TransformMessagesSpec extends ScalaTestWithActorTestKit with WordSpecLike "be possible to combine with outer timers" in { val probe = TestProbe[String]() val behv = Behaviors.withTimers[String] { timers => - timers.startSingleTimer("timer", "a", 10.millis) + timers.startSingleTimer("a", 10.millis) Behaviors .receiveMessage[String] { msg => probe.ref ! msg diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/FSMDocSpec.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/FSMDocSpec.scala index 1e577a7211..5c71577f71 100644 --- a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/FSMDocSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/FSMDocSpec.scala @@ -57,7 +57,7 @@ object FSMDocSpec { private def active(data: Todo): Behavior[Event] = Behaviors.withTimers[Event] { timers => // instead of FSM state timeout - timers.startSingleTimer(Timeout, Timeout, 1.second) + timers.startSingleTimer(Timeout, 1.second) Behaviors.receiveMessagePartial { case Flush | Timeout => data.target ! Batch(data.queue) diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala index 9d2872adea..199ac4d832 100644 --- a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala @@ -123,7 +123,7 @@ object StyleGuideDocExamples { name, interval.toString, n.toString) - timers.startTimerWithFixedDelay("repeat", Increment, interval) + timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 @@ -166,7 +166,7 @@ object StyleGuideDocExamples { setup.name, interval, n) - setup.timers.startTimerWithFixedDelay("repeat", Increment, interval) + setup.timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 @@ -213,7 +213,7 @@ object StyleGuideDocExamples { name, interval, n) - timers.startTimerWithFixedDelay("repeat", Increment, interval) + timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 @@ -249,7 +249,7 @@ object StyleGuideDocExamples { name, interval, n) - timers.startTimerWithFixedDelay("repeat", Increment, interval) + timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 @@ -341,7 +341,7 @@ object StyleGuideDocExamples { def apply(name: String, tickInterval: FiniteDuration): Behavior[Command] = Behaviors.setup { context => Behaviors.withTimers { timers => - timers.startTimerWithFixedDelay("tick", Tick, tickInterval) + timers.startTimerWithFixedDelay(Tick, tickInterval) new Counter(name, context).counter(0) } } @@ -390,7 +390,7 @@ object StyleGuideDocExamples { Behaviors .setup[Counter.Message] { context => Behaviors.withTimers { timers => - timers.startTimerWithFixedDelay("tick", Tick, tickInterval) + timers.startTimerWithFixedDelay(Tick, tickInterval) new Counter(name, context).counter(0) } } diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/TailChopping.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/TailChopping.scala index fdae36c34a..1b2e02d30d 100644 --- a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/TailChopping.scala +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/TailChopping.scala @@ -46,9 +46,9 @@ object TailChopping { def sendNextRequest(requestCount: Int): Behavior[Command] = { if (sendRequest(requestCount, replyAdapter)) { - timers.startSingleTimer(RequestTimeout, RequestTimeout, nextRequestAfter) + timers.startSingleTimer(RequestTimeout, nextRequestAfter) } else { - timers.startSingleTimer(FinalTimeout, FinalTimeout, finalTimeout) + timers.startSingleTimer(FinalTimeout, finalTimeout) } waiting(requestCount) } diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala index 4715888510..f1428029ba 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/javadsl/TimerScheduler.scala @@ -29,12 +29,32 @@ trait TimerScheduler[T] { * the reciprocal of the specified `delay`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerWithFixedDelay(key: Any, msg: T, delay: java.time.Duration): Unit + /** + * Schedules a message to be sent repeatedly to the `self` actor with a + * fixed `delay` between messages. + * + * It will not compensate the delay between messages if scheduling is delayed + * longer than specified for some reason. The delay between sending of subsequent + * messages will always be (at least) the given `delay`. + * + * In the long run, the frequency of messages will generally be slightly lower than + * the reciprocal of the specified `delay`. + * + * When a new timer is started with the same message, + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. If you do not want this, + * you can start start them as individual timers by specifying distinct keys. + */ + def startTimerWithFixedDelay(msg: T, delay: java.time.Duration): Unit = + startTimerWithFixedDelay(msg, msg, delay) + /** * Schedules a message to be sent repeatedly to the `self` actor with a * given frequency. @@ -56,12 +76,41 @@ trait TimerScheduler[T] { * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerAtFixedRate(key: Any, msg: T, interval: java.time.Duration): Unit + /** + * Schedules a message to be sent repeatedly to the `self` actor with a + * given frequency. + * + * It will compensate the delay for a subsequent message if the sending of previous + * message was delayed more than specified. In such cases, the actual message interval + * will differ from the interval passed to the method. + * + * If the execution is delayed longer than the `interval`, the subsequent message will + * be sent immediately after the prior one. This also has the consequence that after + * long garbage collection pauses or other reasons when the JVM was suspended all + * "missed" messages will be sent when the process wakes up again. + * + * In the long run, the frequency of messages will be exactly the reciprocal of the + * specified `interval`. + * + * Warning: `startTimerAtFixedRate` can result in bursts of scheduled messages after long + * garbage collection pauses, which may in worst case cause undesired load on the system. + * Therefore `startTimerWithFixedDelay` is often preferred. + * + * When a new timer is started with the same message, + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. If you do not want this, + * you can start start them as individual timers by specifying distinct keys. + */ + def startTimerAtFixedRate(msg: T, interval: java.time.Duration): Unit = + startTimerAtFixedRate(msg, msg, interval) + /** * Deprecated API: See [[TimerScheduler#startTimerWithFixedDelay]] or [[TimerScheduler#startTimerAtFixedRate]]. */ @@ -72,16 +121,29 @@ trait TimerScheduler[T] { def startPeriodicTimer(key: Any, msg: T, interval: Duration): Unit /** - * * Start a timer that will send `msg` once to the `self` actor after + * Start a timer that will send `msg` once to the `self` actor after * the given `delay`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startSingleTimer(key: Any, msg: T, delay: Duration): Unit + /** + * Start a timer that will send `msg` once to the `self` actor after + * the given `delay`. + * + * When a new timer is started with the same message + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. If you do not want this, + * you can start start them as individual timers by specifying distinct keys. + */ + def startSingleTimer(msg: T, delay: Duration): Unit = + startSingleTimer(msg, msg, delay) + /** * Check if a timer with a given `key` is active. */ diff --git a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala index 9ff8c2ba51..466d4fdffa 100644 --- a/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala +++ b/akka-actor-typed/src/main/scala/akka/actor/typed/scaladsl/TimerScheduler.scala @@ -29,12 +29,32 @@ trait TimerScheduler[T] { * the reciprocal of the specified `delay`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already be enqueued + * in the mailbox before the new timer was started. */ def startTimerWithFixedDelay(key: Any, msg: T, delay: FiniteDuration): Unit + /** + * Schedules a message to be sent repeatedly to the `self` actor with a + * fixed `delay` between messages. + * + * It will not compensate the delay between messages if scheduling is delayed + * longer than specified for some reason. The delay between sending of subsequent + * messages will always be (at least) the given `delay`. + * + * In the long run, the frequency of messages will generally be slightly lower than + * the reciprocal of the specified `delay`. + * + * When a new timer is started with the same message, + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. If you do not want this, + * you can start start them as individual timers by specifying distinct keys. + */ + def startTimerWithFixedDelay(msg: T, delay: FiniteDuration): Unit = + startTimerWithFixedDelay(msg, msg, delay) + /** * Schedules a message to be sent repeatedly to the `self` actor with a * given frequency. @@ -56,12 +76,41 @@ trait TimerScheduler[T] { * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerAtFixedRate(key: Any, msg: T, interval: FiniteDuration): Unit + /** + * Schedules a message to be sent repeatedly to the `self` actor with a + * given frequency. + * + * It will compensate the delay for a subsequent message if the sending of previous + * message was delayed more than specified. In such cases, the actual message interval + * will differ from the interval passed to the method. + * + * If the execution is delayed longer than the `interval`, the subsequent message will + * be sent immediately after the prior one. This also has the consequence that after + * long garbage collection pauses or other reasons when the JVM was suspended all + * "missed" messages will be sent when the process wakes up again. + * + * In the long run, the frequency of messages will be exactly the reciprocal of the + * specified `interval`. + * + * Warning: `startTimerAtFixedRate` can result in bursts of scheduled messages after long + * garbage collection pauses, which may in worst case cause undesired load on the system. + * Therefore `startTimerWithFixedDelay` is often preferred. + * + * When a new timer is started with the same message + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. If you do not want this, + * you can start start them as individual timers by specifying distinct keys. + */ + def startTimerAtFixedRate(msg: T, interval: FiniteDuration): Unit = + startTimerAtFixedRate(msg, msg, interval) + /** * Deprecated API: See [[TimerScheduler#startTimerWithFixedDelay]] or [[TimerScheduler#startTimerAtFixedRate]]. */ @@ -76,12 +125,25 @@ trait TimerScheduler[T] { * the given `delay`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startSingleTimer(key: Any, msg: T, delay: FiniteDuration): Unit + /** + * Start a timer that will send `msg` once to the `self` actor after + * the given `delay`. + * + * If a new timer is started with the same message + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. If you do not want this, + * you can start start them as individual timers by specifying distinct keys. + */ + def startSingleTimer(msg: T, delay: FiniteDuration): Unit = + startSingleTimer(msg, msg, delay) + /** * Check if a timer with a given `key` is active. */ diff --git a/akka-actor/src/main/scala/akka/actor/AbstractFSM.scala b/akka-actor/src/main/scala/akka/actor/AbstractFSM.scala index 091452f32a..3b1ebaf187 100644 --- a/akka-actor/src/main/scala/akka/actor/AbstractFSM.scala +++ b/akka-actor/src/main/scala/akka/actor/AbstractFSM.scala @@ -444,9 +444,9 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { * the reciprocal of the specified `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerWithFixedDelay(name: String, msg: Any, delay: java.time.Duration): Unit = startTimerWithFixedDelay(name, msg, delay.asScala) @@ -472,9 +472,9 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerAtFixedRate(name: String, msg: Any, interval: java.time.Duration): Unit = startTimerAtFixedRate(name, msg, interval.asScala) @@ -484,9 +484,9 @@ abstract class AbstractFSM[S, D] extends FSM[S, D] { * the given `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startSingleTimer(name: String, msg: Any, delay: java.time.Duration): Unit = startSingleTimer(name, msg, delay.asScala) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index e451586253..3b04422604 100644 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -486,9 +486,9 @@ trait FSM[S, D] extends Actor with Listeners with ActorLogging { * the reciprocal of the specified `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerWithFixedDelay(name: String, msg: Any, delay: FiniteDuration): Unit = startTimer(name, msg, delay, FixedDelayMode) @@ -514,9 +514,9 @@ trait FSM[S, D] extends Actor with Listeners with ActorLogging { * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerAtFixedRate(name: String, msg: Any, interval: FiniteDuration): Unit = startTimer(name, msg, interval, FixedRateMode) @@ -526,9 +526,9 @@ trait FSM[S, D] extends Actor with Listeners with ActorLogging { * the given `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startSingleTimer(name: String, msg: Any, delay: FiniteDuration): Unit = startTimer(name, msg, delay, SingleMode) diff --git a/akka-actor/src/main/scala/akka/actor/Timers.scala b/akka-actor/src/main/scala/akka/actor/Timers.scala index 3b342936f8..761d47f8e9 100644 --- a/akka-actor/src/main/scala/akka/actor/Timers.scala +++ b/akka-actor/src/main/scala/akka/actor/Timers.scala @@ -97,9 +97,9 @@ abstract class AbstractActorWithTimers extends AbstractActor with Timers { * the reciprocal of the specified `delay`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerWithFixedDelay(key: Any, msg: Any, delay: FiniteDuration): Unit @@ -115,9 +115,9 @@ abstract class AbstractActorWithTimers extends AbstractActor with Timers { * the reciprocal of the specified `delay`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ final def startTimerWithFixedDelay(key: Any, msg: Any, delay: java.time.Duration): Unit = startTimerWithFixedDelay(key, msg, delay.asScala) @@ -143,9 +143,9 @@ abstract class AbstractActorWithTimers extends AbstractActor with Timers { * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerAtFixedRate(key: Any, msg: Any, interval: FiniteDuration): Unit @@ -170,9 +170,9 @@ abstract class AbstractActorWithTimers extends AbstractActor with Timers { * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ final def startTimerAtFixedRate(key: Any, msg: Any, interval: java.time.Duration): Unit = startTimerAtFixedRate(key, msg, interval.asScala) @@ -201,9 +201,9 @@ abstract class AbstractActorWithTimers extends AbstractActor with Timers { * the given `timeout`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startSingleTimer(key: Any, msg: Any, timeout: FiniteDuration): Unit @@ -212,9 +212,9 @@ abstract class AbstractActorWithTimers extends AbstractActor with Timers { * the given `timeout`. * * Each timer has a key and if a new timer with same key is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ final def startSingleTimer(key: Any, msg: Any, timeout: java.time.Duration): Unit = startSingleTimer(key, msg, timeout.asScala) diff --git a/akka-cluster-typed/src/main/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionist.scala b/akka-cluster-typed/src/main/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionist.scala index 697478ad4e..78f2b8e83f 100644 --- a/akka-cluster-typed/src/main/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionist.scala +++ b/akka-cluster-typed/src/main/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionist.scala @@ -136,11 +136,11 @@ private[typed] object ClusterReceptionist extends ReceptionistBehaviorProvider { // also periodic cleanup in case removal from ORMultiMap is skipped due to concurrent update, // which is possible for OR CRDTs - done with an adapter to leverage the existing NodesRemoved message - timers.startTimerWithFixedDelay("remove-nodes", RemoveTick, setup.settings.pruningInterval) + timers.startTimerWithFixedDelay(RemoveTick, setup.settings.pruningInterval) // default tomstone keepalive is 24h (based on prune-gossip-tombstones-after) and keeping the actorrefs // around isn't very costly so don't prune often - timers.startTimerWithFixedDelay("prune-tombstones", PruneTombstonesTick, setup.keepTombstonesFor / 24) + timers.startTimerWithFixedDelay(PruneTombstonesTick, setup.keepTombstonesFor / 24) behavior(setup, registry, TypedMultiMap.empty[AbstractServiceKey, SubscriptionsKV]) } diff --git a/akka-docs/src/main/paradox/typed/interaction-patterns.md b/akka-docs/src/main/paradox/typed/interaction-patterns.md index 9f6b9d5c05..59b969937e 100644 --- a/akka-docs/src/main/paradox/typed/interaction-patterns.md +++ b/akka-docs/src/main/paradox/typed/interaction-patterns.md @@ -398,7 +398,7 @@ There are a few things worth noting here: * To get access to the timers you start with `Behaviors.withTimers` that will pass a `TimerScheduler` instance to the function. This can be used with any type of `Behavior`, including `receive`, `receiveMessage`, but also `setup` or any other behavior. -* Each timer has a key and if a new timer with the same key is started, the previous is cancelled and it's guaranteed that a message from the previous timer is not received, even though it might already be enqueued in the mailbox when the new timer is started. +* Each timer has a key and if a new timer with the same key is started, the previous is cancelled. It is guaranteed that a message from the previous timer is not received, even if it was already enqueued in the mailbox when the new timer was started. * Both periodic and single message timers are supported. * The `TimerScheduler` is mutable in itself, because it performs and manages the side effects of registering the scheduled tasks. * The `TimerScheduler` is bound to the lifecycle of the actor that owns it and it's cancelled automatically when the actor is stopped. diff --git a/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java b/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java index 53d1b48bdf..42779b460a 100644 --- a/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java +++ b/akka-docs/src/test/java/jdocs/typed/tutorial_5/DeviceGroupQuery.java @@ -78,7 +78,7 @@ public class DeviceGroupQuery extends AbstractBehavior this.requestId = requestId; this.requester = requester; - timers.startSingleTimer(CollectionTimeout.class, CollectionTimeout.INSTANCE, timeout); + timers.startSingleTimer(CollectionTimeout.INSTANCE, timeout); ActorRef respondTemperatureAdapter = context.messageAdapter(Device.RespondTemperature.class, WrappedRespondTemperature::new); diff --git a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala index 20231abb2d..2ade41b6aa 100644 --- a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala +++ b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/EventSourcedBehaviorSpec.scala @@ -205,7 +205,7 @@ object EventSourcedBehaviorSpec { case IncrementLater => // purpose is to test signals val delay = ctx.spawnAnonymous(Behaviors.withTimers[Tick.type] { timers => - timers.startSingleTimer(Tick, Tick, 10.millis) + timers.startSingleTimer(Tick, 10.millis) Behaviors.receive((_, msg) => msg match { case Tick => Behaviors.stopped @@ -467,7 +467,7 @@ class EventSourcedBehaviorSpec "handle scheduled message arriving before recovery completed " in { val c = spawn(Behaviors.withTimers[Command] { timers => - timers.startSingleTimer("tick", Increment, 1.millis) + timers.startSingleTimer(Increment, 1.millis) Thread.sleep(30) // now it's probably already in the mailbox, and will be stashed counter(nextPid) }) @@ -483,7 +483,7 @@ class EventSourcedBehaviorSpec "handle scheduled message arriving after recovery completed " in { val c = spawn(Behaviors.withTimers[Command] { timers => // probably arrives after recovery completed - timers.startSingleTimer("tick", Increment, 200.millis) + timers.startSingleTimer(Increment, 200.millis) counter(nextPid) }) diff --git a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala index 69ecc01bf7..c3586913a0 100644 --- a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala +++ b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorCompileOnlyTest.scala @@ -145,7 +145,7 @@ object PersistentActorCompileOnlyTest { }) Behaviors.withTimers((timers: TimerScheduler[Command]) => { - timers.startTimerWithFixedDelay("swing", MoodSwing, 10.seconds) + timers.startTimerWithFixedDelay(MoodSwing, 10.seconds) b }) } diff --git a/akka-persistence/src/main/scala/akka/persistence/fsm/PersistentFSMBase.scala b/akka-persistence/src/main/scala/akka/persistence/fsm/PersistentFSMBase.scala index 1daf5dcb70..6fb36d4a0a 100644 --- a/akka-persistence/src/main/scala/akka/persistence/fsm/PersistentFSMBase.scala +++ b/akka-persistence/src/main/scala/akka/persistence/fsm/PersistentFSMBase.scala @@ -213,9 +213,9 @@ trait PersistentFSMBase[S, D, E] extends Actor with Listeners with ActorLogging * the reciprocal of the specified `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerWithFixedDelay(name: String, msg: Any, delay: FiniteDuration): Unit = startTimer(name, msg, delay, FixedDelayMode) @@ -241,9 +241,9 @@ trait PersistentFSMBase[S, D, E] extends Actor with Listeners with ActorLogging * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerAtFixedRate(name: String, msg: Any, interval: FiniteDuration): Unit = startTimer(name, msg, interval, FixedRateMode) @@ -253,9 +253,9 @@ trait PersistentFSMBase[S, D, E] extends Actor with Listeners with ActorLogging * the given `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startSingleTimer(name: String, msg: Any, delay: FiniteDuration): Unit = startTimer(name, msg, delay, SingleMode) @@ -1103,9 +1103,9 @@ abstract class AbstractPersistentFSMBase[S, D, E] extends PersistentFSMBase[S, D * the reciprocal of the specified `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerWithFixedDelay(name: String, msg: Any, delay: java.time.Duration): Unit = startTimerWithFixedDelay(name, msg, delay.asScala) @@ -1131,9 +1131,9 @@ abstract class AbstractPersistentFSMBase[S, D, E] extends PersistentFSMBase[S, D * Therefore `startTimerWithFixedDelay` is often preferred. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startTimerAtFixedRate(name: String, msg: Any, interval: java.time.Duration): Unit = startTimerAtFixedRate(name, msg, interval.asScala) @@ -1143,9 +1143,9 @@ abstract class AbstractPersistentFSMBase[S, D, E] extends PersistentFSMBase[S, D * the given `delay`. * * Each timer has a `name` and if a new timer with same `name` is started - * the previous is cancelled and it's guaranteed that a message from the - * previous timer is not received, even though it might already be enqueued - * in the mailbox when the new timer is started. + * the previous is cancelled. It is guaranteed that a message from the + * previous timer is not received, even if it was already enqueued + * in the mailbox when the new timer was started. */ def startSingleTimer(name: String, msg: Any, delay: java.time.Duration): Unit = startSingleTimer(name, msg, delay.asScala)