From ec5be9b23b3c8935d11e60a08cfca6ebc8857093 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Sun, 19 Dec 2010 22:16:15 +0100 Subject: [PATCH 01/31] improvements on FSM - change to akka.util.Duration - add proper implicits to enable timeouts like "5 seconds" - add concept of state timeouts, which are actually attached to states - add timer handling for conveniently modeling timing irrespective of message "interruptions" - add generic Buncher class as usage example and useful utility --- .../src/main/scala/akka/actor/Buncher.scala | 74 ++++ .../src/main/scala/akka/actor/FSM.scala | 417 ++++++++++-------- 2 files changed, 319 insertions(+), 172 deletions(-) create mode 100755 akka-actor/src/main/scala/akka/actor/Buncher.scala mode change 100644 => 100755 akka-actor/src/main/scala/akka/actor/FSM.scala diff --git a/akka-actor/src/main/scala/akka/actor/Buncher.scala b/akka-actor/src/main/scala/akka/actor/Buncher.scala new file mode 100755 index 0000000000..35cf111266 --- /dev/null +++ b/akka-actor/src/main/scala/akka/actor/Buncher.scala @@ -0,0 +1,74 @@ +package akka.actor + +import scala.reflect.ClassManifest +import akka.util.Duration + +/* + * generic typed object buncher. + * + * To instantiate it, use the factory method like so: + * Buncher(100, 500)(x : List[AnyRef] => x foreach println) + * which will yield a fully functional and started ActorRef. + * The type of messages allowed is strongly typed to match the + * supplied processing method; other messages are discarded (and + * possibly logged). + */ +object Buncher { + trait State + case object Idle extends State + case object Active extends State + + case object Flush // send out current queue immediately + case object Stop // poison pill + + case class Data[A](start : Long, xs : List[A]) + + def apply[A : Manifest](singleTimeout : Duration, + multiTimeout : Duration)(f : List[A] => Unit) = + Actor.actorOf(new Buncher[A](singleTimeout, multiTimeout).deliver(f)) +} + +class Buncher[A : Manifest] private (val singleTimeout : Duration, val multiTimeout : Duration) + extends Actor with FSM[Buncher.State, Buncher.Data[A]] { + import Buncher._ + import FSM._ + + private val manifestA = manifest[A] + + private var send : List[A] => Unit = _ + private def deliver(f : List[A] => Unit) = { send = f; this } + + private def now = System.currentTimeMillis + private def check(m : AnyRef) = ClassManifest.fromClass(m.getClass) <:< manifestA + + startWith(Idle, Data(0, Nil)) + + when(Idle) { + case Event(m : AnyRef, _) if check(m) => + goto(Active) using Data(now, m.asInstanceOf[A] :: Nil) + case Event(Flush, _) => stay + case Event(Stop, _) => stop + } + + when(Active, stateTimeout = Some(singleTimeout)) { + case Event(m : AnyRef, Data(start, xs)) if check(m) => + val l = m.asInstanceOf[A] :: xs + if (now - start > multiTimeout.toMillis) { + send(l.reverse) + goto(Idle) using Data(0, Nil) + } else { + stay using Data(start, l) + } + case Event(StateTimeout, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Flush, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Stop, Data(_, xs)) => + send(xs.reverse) + stop + } + + initialize +} diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala old mode 100644 new mode 100755 index 0649eb4605..122c34671b --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -1,172 +1,245 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ - -package akka.actor - -import scala.collection.mutable -import java.util.concurrent.{ScheduledFuture, TimeUnit} - -object FSM { - sealed trait Reason - case object Normal extends Reason - case object Shutdown extends Reason - case class Failure(cause: Any) extends Reason - - case object StateTimeout - case class TimeoutMarker(generation: Long) -} - -trait FSM[S, D] { - this: Actor => - import FSM._ - - type StateFunction = scala.PartialFunction[Event, State] - - /** DSL */ - protected final def notifying(transitionHandler: PartialFunction[Transition, Unit]) = { - transitionEvent = transitionHandler - } - - protected final def when(stateName: S)(stateFunction: StateFunction) = { - register(stateName, stateFunction) - } - - protected final def startWith(stateName: S, - stateData: D, - timeout: Option[(Long, TimeUnit)] = None) = { - applyState(State(stateName, stateData, timeout)) - } - - protected final def goto(nextStateName: S): State = { - State(nextStateName, currentState.stateData) - } - - protected final def stay(): State = { - goto(currentState.stateName) - } - - protected final def stop(): State = { - stop(Normal) - } - - protected final def stop(reason: Reason): State = { - stop(reason, currentState.stateData) - } - - protected final def stop(reason: Reason, stateData: D): State = { - stay using stateData withStopReason(reason) - } - - def whenUnhandled(stateFunction: StateFunction) = { - handleEvent = stateFunction - } - - def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { - terminateEvent = terminationHandler - } - - /** FSM State data and default handlers */ - private var currentState: State = _ - private var timeoutFuture: Option[ScheduledFuture[AnyRef]] = None - private var generation: Long = 0L - - - private val stateFunctions = mutable.Map[S, StateFunction]() - private def register(name: S, function: StateFunction) { - if (stateFunctions contains name) { - stateFunctions(name) = stateFunctions(name) orElse function - } else { - stateFunctions(name) = function - } - } - - private var handleEvent: StateFunction = { - case Event(value, stateData) => - log.slf4j.warn("Event {} not handled in state {}, staying at current state", value, currentState.stateName) - stay - } - - private var terminateEvent: PartialFunction[Reason, Unit] = { - case failure@Failure(_) => log.slf4j.error("Stopping because of a {}", failure) - case reason => log.slf4j.info("Stopping because of reason: {}", reason) - } - - private var transitionEvent: PartialFunction[Transition, Unit] = { - case Transition(from, to) => log.slf4j.debug("Transitioning from state {} to {}", from, to) - } - - override final protected def receive: Receive = { - case TimeoutMarker(gen) => - if (generation == gen) { - processEvent(StateTimeout) - } - case value => { - timeoutFuture = timeoutFuture.flatMap {ref => ref.cancel(true); None} - generation += 1 - processEvent(value) - } - } - - private def processEvent(value: Any) = { - val event = Event(value, currentState.stateData) - val nextState = (stateFunctions(currentState.stateName) orElse handleEvent).apply(event) - nextState.stopReason match { - case Some(reason) => terminate(reason) - case None => makeTransition(nextState) - } - } - - private def makeTransition(nextState: State) = { - if (!stateFunctions.contains(nextState.stateName)) { - terminate(Failure("Next state %s does not exist".format(nextState.stateName))) - } else { - if (currentState.stateName != nextState.stateName) { - transitionEvent.apply(Transition(currentState.stateName, nextState.stateName)) - } - applyState(nextState) - } - } - - private def applyState(nextState: State) = { - currentState = nextState - currentState.timeout.foreach { t => - timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t._1, t._2)) - } - } - - private def terminate(reason: Reason) = { - terminateEvent.apply(reason) - self.stop - } - - - case class Event(event: Any, stateData: D) - - case class State(stateName: S, stateData: D, timeout: Option[(Long, TimeUnit)] = None) { - - def forMax(timeout: (Long, TimeUnit)): State = { - copy(timeout = Some(timeout)) - } - - def replying(replyValue:Any): State = { - self.sender match { - case Some(sender) => sender ! replyValue - case None => log.slf4j.error("Unable to send reply value {}, no sender reference to reply to", replyValue) - } - this - } - - def using(nextStateDate: D): State = { - copy(stateData = nextStateDate) - } - - private[akka] var stopReason: Option[Reason] = None - private[akka] def withStopReason(reason: Reason): State = { - stopReason = Some(reason) - this - } - } - - case class Transition(from: S, to: S) -} +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.actor + +import akka.util._ + +import scala.collection.mutable +import java.util.concurrent.{ScheduledFuture, TimeUnit} + +object FSM { + sealed trait Reason + case object Normal extends Reason + case object Shutdown extends Reason + case class Failure(cause: Any) extends Reason + + case object StateTimeout + case class TimeoutMarker(generation: Long) + case class Timer(name : String, msg : AnyRef, repeat : Boolean) { + private var ref : Option[ScheduledFuture[AnyRef]] = _ + def schedule(actor : ActorRef, timeout : Duration) { + if (repeat) { + ref = Some(Scheduler.schedule(actor, this, timeout.length, timeout.length, timeout.unit)) + } else { + ref = Some(Scheduler.scheduleOnce(actor, this, timeout.length, timeout.unit)) + } + } + def cancel { + ref = ref flatMap {t => t.cancel(true); None} + } + } + + /* + * With these implicits in scope, you can write "5 seconds" anywhere a + * Duration or Option[Duration] is expected. This is conveniently true + * for derived classes. + */ + implicit def d2od(d : Duration) : Option[Duration] = Some(d) + implicit def p2od(p : (Long, TimeUnit)) : Duration = new Duration(p._1, p._2) + implicit def i2d(i : Int) : DurationInt = new DurationInt(i) + implicit def l2d(l : Long) : DurationLong = new DurationLong(l) +} + +trait FSM[S, D] { + this: Actor => + import FSM._ + + type StateFunction = scala.PartialFunction[Event, State] + type Timeout = Option[Duration] + + /** DSL */ + protected final def notifying(transitionHandler: PartialFunction[Transition, Unit]) = { + transitionEvent = transitionHandler + } + + protected final def when(stateName: S, stateTimeout: Timeout = None)(stateFunction: StateFunction) = { + register(stateName, stateFunction, stateTimeout) + } + + protected final def startWith(stateName: S, + stateData: D, + timeout: Timeout = None) = { + currentState = State(stateName, stateData, timeout) + } + + protected final def goto(nextStateName: S): State = { + State(nextStateName, currentState.stateData) + } + + protected final def stay(): State = { + // cannot directly use currentState because of the timeout field + goto(currentState.stateName) + } + + protected final def stop(): State = { + stop(Normal) + } + + protected final def stop(reason: Reason): State = { + stop(reason, currentState.stateData) + } + + protected final def stop(reason: Reason, stateData: D): State = { + stay using stateData withStopReason(reason) + } + + /** + * Schedule named timer to deliver message after given delay, possibly repeating. + * @param name identifier to be used with cancelTimer() + * @param msg message to be delivered + * @param timeout delay of first message delivery and between subsequent messages + * @param repeat send once if false, scheduleAtFixedRate if true + * @return current State + */ + protected final def setTimer(name : String, msg : AnyRef, timeout : Duration, repeat : Boolean):State = { + if (timers contains name) { + timers(name).cancel + } + val timer = Timer(name, msg, repeat) + timer.schedule(self, timeout) + timers(name) = timer + stay + } + + /** + * Cancel named timer, ensuring that the message is not subsequently delivered (no race). + * @param name + * @return + */ + protected final def cancelTimer(name : String) = { + if (timers contains name) { + timers(name).cancel + timers -= name + } + } + + protected final def timerActive_?(name : String) = timers contains name + + def whenUnhandled(stateFunction: StateFunction) = { + handleEvent = stateFunction + } + + def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { + terminateEvent = terminationHandler + } + + def initialize { + // check existence of initial state and setup timeout + makeTransition(currentState) + } + + /** FSM State data and default handlers */ + private var currentState: State = _ + private var timeoutFuture: Option[ScheduledFuture[AnyRef]] = None + private var generation: Long = 0L + + private val timers = mutable.Map[String, Timer]() + + private val stateFunctions = mutable.Map[S, StateFunction]() + private val stateTimeouts = mutable.Map[S, Timeout]() + + private def register(name: S, function: StateFunction, timeout: Timeout) { + if (stateFunctions contains name) { + stateFunctions(name) = stateFunctions(name) orElse function + stateTimeouts(name) = timeout orElse stateTimeouts(name) + } else { + stateFunctions(name) = function + stateTimeouts(name) = timeout + } + } + + private var handleEvent: StateFunction = { + case Event(value, stateData) => + log.slf4j.warn("Event {} not handled in state {}, staying at current state", value, currentState.stateName) + stay + } + + private var terminateEvent: PartialFunction[Reason, Unit] = { + case failure@Failure(_) => log.slf4j.error("Stopping because of a {}", failure) + case reason => log.slf4j.info("Stopping because of reason: {}", reason) + } + + private var transitionEvent: PartialFunction[Transition, Unit] = { + case Transition(from, to) => log.slf4j.debug("Transitioning from state {} to {}", from, to) + } + + override final protected def receive: Receive = { + case TimeoutMarker(gen) => + if (generation == gen) { + processEvent(StateTimeout) + } + case t @ Timer(name, msg, repeat) => + if (timerActive_?(name)) { + processEvent(msg) + if (!repeat) { + timers -= name + } + } + case value => { + timeoutFuture = timeoutFuture.flatMap {ref => ref.cancel(true); None} + generation += 1 + processEvent(value) + } + } + + private def processEvent(value: Any) = { + val event = Event(value, currentState.stateData) + val nextState = (stateFunctions(currentState.stateName) orElse handleEvent).apply(event) + nextState.stopReason match { + case Some(reason) => terminate(reason) + case None => makeTransition(nextState) + } + } + + private def makeTransition(nextState: State) = { + if (!stateFunctions.contains(nextState.stateName)) { + terminate(Failure("Next state %s does not exist".format(nextState.stateName))) + } else { + if (currentState.stateName != nextState.stateName) { + transitionEvent.apply(Transition(currentState.stateName, nextState.stateName)) + } + currentState = nextState + currentState.timeout orElse stateTimeouts(currentState.stateName) foreach { t => + if (t.length >= 0) { + timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t.length, t.unit)) + } + } + } + } + + private def terminate(reason: Reason) = { + terminateEvent.apply(reason) + self.stop + } + + + case class Event(event: Any, stateData: D) + + case class State(stateName: S, stateData: D, timeout: Timeout = None) { + + def forMax(timeout: Duration): State = { + copy(timeout = Some(timeout)) + } + + def replying(replyValue:Any): State = { + self.sender match { + case Some(sender) => sender ! replyValue + case None => log.slf4j.error("Unable to send reply value {}, no sender reference to reply to", replyValue) + } + this + } + + def using(nextStateDate: D): State = { + copy(stateData = nextStateDate) + } + + private[akka] var stopReason: Option[Reason] = None + private[akka] def withStopReason(reason: Reason): State = { + stopReason = Some(reason) + this + } + } + + case class Transition(from: S, to: S) +} From 716aab270ff251bf52aa333cadc84caeaacdb44f Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Sun, 19 Dec 2010 22:27:11 +0100 Subject: [PATCH 02/31] change ff=unix --- .../src/main/scala/akka/actor/Buncher.scala | 148 +++--- .../src/main/scala/akka/actor/FSM.scala | 490 +++++++++--------- 2 files changed, 319 insertions(+), 319 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/Buncher.scala b/akka-actor/src/main/scala/akka/actor/Buncher.scala index 35cf111266..ce926e9d15 100755 --- a/akka-actor/src/main/scala/akka/actor/Buncher.scala +++ b/akka-actor/src/main/scala/akka/actor/Buncher.scala @@ -1,74 +1,74 @@ -package akka.actor - -import scala.reflect.ClassManifest -import akka.util.Duration - -/* - * generic typed object buncher. - * - * To instantiate it, use the factory method like so: - * Buncher(100, 500)(x : List[AnyRef] => x foreach println) - * which will yield a fully functional and started ActorRef. - * The type of messages allowed is strongly typed to match the - * supplied processing method; other messages are discarded (and - * possibly logged). - */ -object Buncher { - trait State - case object Idle extends State - case object Active extends State - - case object Flush // send out current queue immediately - case object Stop // poison pill - - case class Data[A](start : Long, xs : List[A]) - - def apply[A : Manifest](singleTimeout : Duration, - multiTimeout : Duration)(f : List[A] => Unit) = - Actor.actorOf(new Buncher[A](singleTimeout, multiTimeout).deliver(f)) -} - -class Buncher[A : Manifest] private (val singleTimeout : Duration, val multiTimeout : Duration) - extends Actor with FSM[Buncher.State, Buncher.Data[A]] { - import Buncher._ - import FSM._ - - private val manifestA = manifest[A] - - private var send : List[A] => Unit = _ - private def deliver(f : List[A] => Unit) = { send = f; this } - - private def now = System.currentTimeMillis - private def check(m : AnyRef) = ClassManifest.fromClass(m.getClass) <:< manifestA - - startWith(Idle, Data(0, Nil)) - - when(Idle) { - case Event(m : AnyRef, _) if check(m) => - goto(Active) using Data(now, m.asInstanceOf[A] :: Nil) - case Event(Flush, _) => stay - case Event(Stop, _) => stop - } - - when(Active, stateTimeout = Some(singleTimeout)) { - case Event(m : AnyRef, Data(start, xs)) if check(m) => - val l = m.asInstanceOf[A] :: xs - if (now - start > multiTimeout.toMillis) { - send(l.reverse) - goto(Idle) using Data(0, Nil) - } else { - stay using Data(start, l) - } - case Event(StateTimeout, Data(_, xs)) => - send(xs.reverse) - goto(Idle) using Data(0, Nil) - case Event(Flush, Data(_, xs)) => - send(xs.reverse) - goto(Idle) using Data(0, Nil) - case Event(Stop, Data(_, xs)) => - send(xs.reverse) - stop - } - - initialize -} +package akka.actor + +import scala.reflect.ClassManifest +import akka.util.Duration + +/* + * generic typed object buncher. + * + * To instantiate it, use the factory method like so: + * Buncher(100, 500)(x : List[AnyRef] => x foreach println) + * which will yield a fully functional and started ActorRef. + * The type of messages allowed is strongly typed to match the + * supplied processing method; other messages are discarded (and + * possibly logged). + */ +object Buncher { + trait State + case object Idle extends State + case object Active extends State + + case object Flush // send out current queue immediately + case object Stop // poison pill + + case class Data[A](start : Long, xs : List[A]) + + def apply[A : Manifest](singleTimeout : Duration, + multiTimeout : Duration)(f : List[A] => Unit) = + Actor.actorOf(new Buncher[A](singleTimeout, multiTimeout).deliver(f)) +} + +class Buncher[A : Manifest] private (val singleTimeout : Duration, val multiTimeout : Duration) + extends Actor with FSM[Buncher.State, Buncher.Data[A]] { + import Buncher._ + import FSM._ + + private val manifestA = manifest[A] + + private var send : List[A] => Unit = _ + private def deliver(f : List[A] => Unit) = { send = f; this } + + private def now = System.currentTimeMillis + private def check(m : AnyRef) = ClassManifest.fromClass(m.getClass) <:< manifestA + + startWith(Idle, Data(0, Nil)) + + when(Idle) { + case Event(m : AnyRef, _) if check(m) => + goto(Active) using Data(now, m.asInstanceOf[A] :: Nil) + case Event(Flush, _) => stay + case Event(Stop, _) => stop + } + + when(Active, stateTimeout = Some(singleTimeout)) { + case Event(m : AnyRef, Data(start, xs)) if check(m) => + val l = m.asInstanceOf[A] :: xs + if (now - start > multiTimeout.toMillis) { + send(l.reverse) + goto(Idle) using Data(0, Nil) + } else { + stay using Data(start, l) + } + case Event(StateTimeout, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Flush, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Stop, Data(_, xs)) => + send(xs.reverse) + stop + } + + initialize +} diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 122c34671b..ae59c641bb 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -1,245 +1,245 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ -package akka.actor - -import akka.util._ - -import scala.collection.mutable -import java.util.concurrent.{ScheduledFuture, TimeUnit} - -object FSM { - sealed trait Reason - case object Normal extends Reason - case object Shutdown extends Reason - case class Failure(cause: Any) extends Reason - - case object StateTimeout - case class TimeoutMarker(generation: Long) - case class Timer(name : String, msg : AnyRef, repeat : Boolean) { - private var ref : Option[ScheduledFuture[AnyRef]] = _ - def schedule(actor : ActorRef, timeout : Duration) { - if (repeat) { - ref = Some(Scheduler.schedule(actor, this, timeout.length, timeout.length, timeout.unit)) - } else { - ref = Some(Scheduler.scheduleOnce(actor, this, timeout.length, timeout.unit)) - } - } - def cancel { - ref = ref flatMap {t => t.cancel(true); None} - } - } - - /* - * With these implicits in scope, you can write "5 seconds" anywhere a - * Duration or Option[Duration] is expected. This is conveniently true - * for derived classes. - */ - implicit def d2od(d : Duration) : Option[Duration] = Some(d) - implicit def p2od(p : (Long, TimeUnit)) : Duration = new Duration(p._1, p._2) - implicit def i2d(i : Int) : DurationInt = new DurationInt(i) - implicit def l2d(l : Long) : DurationLong = new DurationLong(l) -} - -trait FSM[S, D] { - this: Actor => - import FSM._ - - type StateFunction = scala.PartialFunction[Event, State] - type Timeout = Option[Duration] - - /** DSL */ - protected final def notifying(transitionHandler: PartialFunction[Transition, Unit]) = { - transitionEvent = transitionHandler - } - - protected final def when(stateName: S, stateTimeout: Timeout = None)(stateFunction: StateFunction) = { - register(stateName, stateFunction, stateTimeout) - } - - protected final def startWith(stateName: S, - stateData: D, - timeout: Timeout = None) = { - currentState = State(stateName, stateData, timeout) - } - - protected final def goto(nextStateName: S): State = { - State(nextStateName, currentState.stateData) - } - - protected final def stay(): State = { - // cannot directly use currentState because of the timeout field - goto(currentState.stateName) - } - - protected final def stop(): State = { - stop(Normal) - } - - protected final def stop(reason: Reason): State = { - stop(reason, currentState.stateData) - } - - protected final def stop(reason: Reason, stateData: D): State = { - stay using stateData withStopReason(reason) - } - - /** - * Schedule named timer to deliver message after given delay, possibly repeating. - * @param name identifier to be used with cancelTimer() - * @param msg message to be delivered - * @param timeout delay of first message delivery and between subsequent messages - * @param repeat send once if false, scheduleAtFixedRate if true - * @return current State - */ - protected final def setTimer(name : String, msg : AnyRef, timeout : Duration, repeat : Boolean):State = { - if (timers contains name) { - timers(name).cancel - } - val timer = Timer(name, msg, repeat) - timer.schedule(self, timeout) - timers(name) = timer - stay - } - - /** - * Cancel named timer, ensuring that the message is not subsequently delivered (no race). - * @param name - * @return - */ - protected final def cancelTimer(name : String) = { - if (timers contains name) { - timers(name).cancel - timers -= name - } - } - - protected final def timerActive_?(name : String) = timers contains name - - def whenUnhandled(stateFunction: StateFunction) = { - handleEvent = stateFunction - } - - def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { - terminateEvent = terminationHandler - } - - def initialize { - // check existence of initial state and setup timeout - makeTransition(currentState) - } - - /** FSM State data and default handlers */ - private var currentState: State = _ - private var timeoutFuture: Option[ScheduledFuture[AnyRef]] = None - private var generation: Long = 0L - - private val timers = mutable.Map[String, Timer]() - - private val stateFunctions = mutable.Map[S, StateFunction]() - private val stateTimeouts = mutable.Map[S, Timeout]() - - private def register(name: S, function: StateFunction, timeout: Timeout) { - if (stateFunctions contains name) { - stateFunctions(name) = stateFunctions(name) orElse function - stateTimeouts(name) = timeout orElse stateTimeouts(name) - } else { - stateFunctions(name) = function - stateTimeouts(name) = timeout - } - } - - private var handleEvent: StateFunction = { - case Event(value, stateData) => - log.slf4j.warn("Event {} not handled in state {}, staying at current state", value, currentState.stateName) - stay - } - - private var terminateEvent: PartialFunction[Reason, Unit] = { - case failure@Failure(_) => log.slf4j.error("Stopping because of a {}", failure) - case reason => log.slf4j.info("Stopping because of reason: {}", reason) - } - - private var transitionEvent: PartialFunction[Transition, Unit] = { - case Transition(from, to) => log.slf4j.debug("Transitioning from state {} to {}", from, to) - } - - override final protected def receive: Receive = { - case TimeoutMarker(gen) => - if (generation == gen) { - processEvent(StateTimeout) - } - case t @ Timer(name, msg, repeat) => - if (timerActive_?(name)) { - processEvent(msg) - if (!repeat) { - timers -= name - } - } - case value => { - timeoutFuture = timeoutFuture.flatMap {ref => ref.cancel(true); None} - generation += 1 - processEvent(value) - } - } - - private def processEvent(value: Any) = { - val event = Event(value, currentState.stateData) - val nextState = (stateFunctions(currentState.stateName) orElse handleEvent).apply(event) - nextState.stopReason match { - case Some(reason) => terminate(reason) - case None => makeTransition(nextState) - } - } - - private def makeTransition(nextState: State) = { - if (!stateFunctions.contains(nextState.stateName)) { - terminate(Failure("Next state %s does not exist".format(nextState.stateName))) - } else { - if (currentState.stateName != nextState.stateName) { - transitionEvent.apply(Transition(currentState.stateName, nextState.stateName)) - } - currentState = nextState - currentState.timeout orElse stateTimeouts(currentState.stateName) foreach { t => - if (t.length >= 0) { - timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t.length, t.unit)) - } - } - } - } - - private def terminate(reason: Reason) = { - terminateEvent.apply(reason) - self.stop - } - - - case class Event(event: Any, stateData: D) - - case class State(stateName: S, stateData: D, timeout: Timeout = None) { - - def forMax(timeout: Duration): State = { - copy(timeout = Some(timeout)) - } - - def replying(replyValue:Any): State = { - self.sender match { - case Some(sender) => sender ! replyValue - case None => log.slf4j.error("Unable to send reply value {}, no sender reference to reply to", replyValue) - } - this - } - - def using(nextStateDate: D): State = { - copy(stateData = nextStateDate) - } - - private[akka] var stopReason: Option[Reason] = None - private[akka] def withStopReason(reason: Reason): State = { - stopReason = Some(reason) - this - } - } - - case class Transition(from: S, to: S) -} +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package akka.actor + +import akka.util._ + +import scala.collection.mutable +import java.util.concurrent.{ScheduledFuture, TimeUnit} + +object FSM { + sealed trait Reason + case object Normal extends Reason + case object Shutdown extends Reason + case class Failure(cause: Any) extends Reason + + case object StateTimeout + case class TimeoutMarker(generation: Long) + case class Timer(name : String, msg : AnyRef, repeat : Boolean) { + private var ref : Option[ScheduledFuture[AnyRef]] = _ + def schedule(actor : ActorRef, timeout : Duration) { + if (repeat) { + ref = Some(Scheduler.schedule(actor, this, timeout.length, timeout.length, timeout.unit)) + } else { + ref = Some(Scheduler.scheduleOnce(actor, this, timeout.length, timeout.unit)) + } + } + def cancel { + ref = ref flatMap {t => t.cancel(true); None} + } + } + + /* + * With these implicits in scope, you can write "5 seconds" anywhere a + * Duration or Option[Duration] is expected. This is conveniently true + * for derived classes. + */ + implicit def d2od(d : Duration) : Option[Duration] = Some(d) + implicit def p2od(p : (Long, TimeUnit)) : Duration = new Duration(p._1, p._2) + implicit def i2d(i : Int) : DurationInt = new DurationInt(i) + implicit def l2d(l : Long) : DurationLong = new DurationLong(l) +} + +trait FSM[S, D] { + this: Actor => + import FSM._ + + type StateFunction = scala.PartialFunction[Event, State] + type Timeout = Option[Duration] + + /** DSL */ + protected final def notifying(transitionHandler: PartialFunction[Transition, Unit]) = { + transitionEvent = transitionHandler + } + + protected final def when(stateName: S, stateTimeout: Timeout = None)(stateFunction: StateFunction) = { + register(stateName, stateFunction, stateTimeout) + } + + protected final def startWith(stateName: S, + stateData: D, + timeout: Timeout = None) = { + currentState = State(stateName, stateData, timeout) + } + + protected final def goto(nextStateName: S): State = { + State(nextStateName, currentState.stateData) + } + + protected final def stay(): State = { + // cannot directly use currentState because of the timeout field + goto(currentState.stateName) + } + + protected final def stop(): State = { + stop(Normal) + } + + protected final def stop(reason: Reason): State = { + stop(reason, currentState.stateData) + } + + protected final def stop(reason: Reason, stateData: D): State = { + stay using stateData withStopReason(reason) + } + + /** + * Schedule named timer to deliver message after given delay, possibly repeating. + * @param name identifier to be used with cancelTimer() + * @param msg message to be delivered + * @param timeout delay of first message delivery and between subsequent messages + * @param repeat send once if false, scheduleAtFixedRate if true + * @return current State + */ + protected final def setTimer(name : String, msg : AnyRef, timeout : Duration, repeat : Boolean):State = { + if (timers contains name) { + timers(name).cancel + } + val timer = Timer(name, msg, repeat) + timer.schedule(self, timeout) + timers(name) = timer + stay + } + + /** + * Cancel named timer, ensuring that the message is not subsequently delivered (no race). + * @param name + * @return + */ + protected final def cancelTimer(name : String) = { + if (timers contains name) { + timers(name).cancel + timers -= name + } + } + + protected final def timerActive_?(name : String) = timers contains name + + def whenUnhandled(stateFunction: StateFunction) = { + handleEvent = stateFunction + } + + def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { + terminateEvent = terminationHandler + } + + def initialize { + // check existence of initial state and setup timeout + makeTransition(currentState) + } + + /** FSM State data and default handlers */ + private var currentState: State = _ + private var timeoutFuture: Option[ScheduledFuture[AnyRef]] = None + private var generation: Long = 0L + + private val timers = mutable.Map[String, Timer]() + + private val stateFunctions = mutable.Map[S, StateFunction]() + private val stateTimeouts = mutable.Map[S, Timeout]() + + private def register(name: S, function: StateFunction, timeout: Timeout) { + if (stateFunctions contains name) { + stateFunctions(name) = stateFunctions(name) orElse function + stateTimeouts(name) = timeout orElse stateTimeouts(name) + } else { + stateFunctions(name) = function + stateTimeouts(name) = timeout + } + } + + private var handleEvent: StateFunction = { + case Event(value, stateData) => + log.slf4j.warn("Event {} not handled in state {}, staying at current state", value, currentState.stateName) + stay + } + + private var terminateEvent: PartialFunction[Reason, Unit] = { + case failure@Failure(_) => log.slf4j.error("Stopping because of a {}", failure) + case reason => log.slf4j.info("Stopping because of reason: {}", reason) + } + + private var transitionEvent: PartialFunction[Transition, Unit] = { + case Transition(from, to) => log.slf4j.debug("Transitioning from state {} to {}", from, to) + } + + override final protected def receive: Receive = { + case TimeoutMarker(gen) => + if (generation == gen) { + processEvent(StateTimeout) + } + case t @ Timer(name, msg, repeat) => + if (timerActive_?(name)) { + processEvent(msg) + if (!repeat) { + timers -= name + } + } + case value => { + timeoutFuture = timeoutFuture.flatMap {ref => ref.cancel(true); None} + generation += 1 + processEvent(value) + } + } + + private def processEvent(value: Any) = { + val event = Event(value, currentState.stateData) + val nextState = (stateFunctions(currentState.stateName) orElse handleEvent).apply(event) + nextState.stopReason match { + case Some(reason) => terminate(reason) + case None => makeTransition(nextState) + } + } + + private def makeTransition(nextState: State) = { + if (!stateFunctions.contains(nextState.stateName)) { + terminate(Failure("Next state %s does not exist".format(nextState.stateName))) + } else { + if (currentState.stateName != nextState.stateName) { + transitionEvent.apply(Transition(currentState.stateName, nextState.stateName)) + } + currentState = nextState + currentState.timeout orElse stateTimeouts(currentState.stateName) foreach { t => + if (t.length >= 0) { + timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t.length, t.unit)) + } + } + } + } + + private def terminate(reason: Reason) = { + terminateEvent.apply(reason) + self.stop + } + + + case class Event(event: Any, stateData: D) + + case class State(stateName: S, stateData: D, timeout: Timeout = None) { + + def forMax(timeout: Duration): State = { + copy(timeout = Some(timeout)) + } + + def replying(replyValue:Any): State = { + self.sender match { + case Some(sender) => sender ! replyValue + case None => log.slf4j.error("Unable to send reply value {}, no sender reference to reply to", replyValue) + } + this + } + + def using(nextStateDate: D): State = { + copy(stateData = nextStateDate) + } + + private[akka] var stopReason: Option[Reason] = None + private[akka] def withStopReason(reason: Reason): State = { + stopReason = Some(reason) + this + } + } + + case class Transition(from: S, to: S) +} From acee86f9cd9ebc14dbaf850575d60be5cae7ca84 Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 20 Dec 2010 10:40:23 +0100 Subject: [PATCH 03/31] - merge in transition callback handling - renamed notifying -> onTransition - updated dining hakkers example and unit test - moved buncher to fsm sample project --- .../src/main/scala/akka/actor/FSM.scala | 163 ++++++++++-------- .../scala/akka/actor/actor/FSMActorSpec.scala | 27 ++- .../src/main/scala}/Buncher.scala | 21 +-- .../src/main/scala/DiningHakkersOnFsm.scala | 20 +-- 4 files changed, 135 insertions(+), 96 deletions(-) rename {akka-actor/src/main/scala/akka/actor => akka-samples/akka-sample-fsm/src/main/scala}/Buncher.scala (80%) mode change 100755 => 100644 diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index ae59c641bb..373d372677 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -9,50 +9,57 @@ import scala.collection.mutable import java.util.concurrent.{ScheduledFuture, TimeUnit} object FSM { + sealed trait Reason case object Normal extends Reason case object Shutdown extends Reason case class Failure(cause: Any) extends Reason + case class Event[D](event: Any, stateData: D) + + case class Transition[S](from: S, to: S) + case class SubscribeTransitionCallBack(actorRef: ActorRef) + case class UnsubscribeTransitionCallBack(actorRef: ActorRef) + case object StateTimeout case class TimeoutMarker(generation: Long) - case class Timer(name : String, msg : AnyRef, repeat : Boolean) { - private var ref : Option[ScheduledFuture[AnyRef]] = _ - def schedule(actor : ActorRef, timeout : Duration) { - if (repeat) { - ref = Some(Scheduler.schedule(actor, this, timeout.length, timeout.length, timeout.unit)) - } else { - ref = Some(Scheduler.scheduleOnce(actor, this, timeout.length, timeout.unit)) - } - } - def cancel { - ref = ref flatMap {t => t.cancel(true); None} - } + + case class Timer(name: String, msg: AnyRef, repeat: Boolean) { + private var ref: Option[ScheduledFuture[AnyRef]] = _ + + def schedule(actor: ActorRef, timeout: Duration) { + if (repeat) { + ref = Some(Scheduler.schedule(actor, this, timeout.length, timeout.length, timeout.unit)) + } else { + ref = Some(Scheduler.scheduleOnce(actor, this, timeout.length, timeout.unit)) + } + } + + def cancel { + ref = ref flatMap { + t => t.cancel(true); None + } + } } - + /* - * With these implicits in scope, you can write "5 seconds" anywhere a - * Duration or Option[Duration] is expected. This is conveniently true - * for derived classes. - */ - implicit def d2od(d : Duration) : Option[Duration] = Some(d) - implicit def p2od(p : (Long, TimeUnit)) : Duration = new Duration(p._1, p._2) - implicit def i2d(i : Int) : DurationInt = new DurationInt(i) - implicit def l2d(l : Long) : DurationLong = new DurationLong(l) + * With these implicits in scope, you can write "5 seconds" anywhere a + * Duration or Option[Duration] is expected. This is conveniently true + * for derived classes. + */ + implicit def d2od(d: Duration): Option[Duration] = Some(d) + implicit def p2od(p: (Long, TimeUnit)): Duration = new Duration(p._1, p._2) } trait FSM[S, D] { this: Actor => + import FSM._ - type StateFunction = scala.PartialFunction[Event, State] + type StateFunction = scala.PartialFunction[Event[D], State] type Timeout = Option[Duration] - /** DSL */ - protected final def notifying(transitionHandler: PartialFunction[Transition, Unit]) = { - transitionEvent = transitionHandler - } - + /**DSL */ protected final def when(stateName: S, stateTimeout: Timeout = None)(stateFunction: StateFunction) = { register(stateName, stateFunction, stateTimeout) } @@ -60,7 +67,7 @@ trait FSM[S, D] { protected final def startWith(stateName: S, stateData: D, timeout: Timeout = None) = { - currentState = State(stateName, stateData, timeout) + applyState(State(stateName, stateData, timeout)) } protected final def goto(nextStateName: S): State = { @@ -81,7 +88,7 @@ trait FSM[S, D] { } protected final def stop(reason: Reason, stateData: D): State = { - stay using stateData withStopReason(reason) + stay using stateData withStopReason (reason) } /** @@ -92,53 +99,60 @@ trait FSM[S, D] { * @param repeat send once if false, scheduleAtFixedRate if true * @return current State */ - protected final def setTimer(name : String, msg : AnyRef, timeout : Duration, repeat : Boolean):State = { + protected final def setTimer(name: String, msg: AnyRef, timeout: Duration, repeat: Boolean): State = { if (timers contains name) { - timers(name).cancel + timers(name).cancel } - val timer = Timer(name, msg, repeat) + val timer = Timer(name, msg, repeat) timer.schedule(self, timeout) timers(name) = timer stay } - + /** * Cancel named timer, ensuring that the message is not subsequently delivered (no race). * @param name * @return */ - protected final def cancelTimer(name : String) = { - if (timers contains name) { - timers(name).cancel - timers -= name - } + protected final def cancelTimer(name: String) = { + if (timers contains name) { + timers(name).cancel + timers -= name + } } - - protected final def timerActive_?(name : String) = timers contains name - def whenUnhandled(stateFunction: StateFunction) = { + protected final def timerActive_?(name: String) = timers contains name + + /**callbacks */ + protected final def onTransition(transitionHandler: PartialFunction[Transition[S], Unit]) = { + transitionEvent = transitionHandler + } + + protected final def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { + terminateEvent = terminationHandler + } + + protected final def whenUnhandled(stateFunction: StateFunction) = { handleEvent = stateFunction } - def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { - terminateEvent = terminationHandler - } - def initialize { - // check existence of initial state and setup timeout - makeTransition(currentState) + // check existence of initial state and setup timeout + makeTransition(currentState) } - /** FSM State data and default handlers */ + /**FSM State data and default handlers */ private var currentState: State = _ private var timeoutFuture: Option[ScheduledFuture[AnyRef]] = None private var generation: Long = 0L + private var transitionCallBackList: List[ActorRef] = Nil + private val timers = mutable.Map[String, Timer]() private val stateFunctions = mutable.Map[S, StateFunction]() private val stateTimeouts = mutable.Map[S, Timeout]() - + private def register(name: S, function: StateFunction, timeout: Timeout) { if (stateFunctions contains name) { stateFunctions(name) = stateFunctions(name) orElse function @@ -160,7 +174,7 @@ trait FSM[S, D] { case reason => log.slf4j.info("Stopping because of reason: {}", reason) } - private var transitionEvent: PartialFunction[Transition, Unit] = { + private var transitionEvent: PartialFunction[Transition[S], Unit] = { case Transition(from, to) => log.slf4j.debug("Transitioning from state {} to {}", from, to) } @@ -169,15 +183,23 @@ trait FSM[S, D] { if (generation == gen) { processEvent(StateTimeout) } - case t @ Timer(name, msg, repeat) => + case t@Timer(name, msg, repeat) => if (timerActive_?(name)) { - processEvent(msg) - if (!repeat) { - timers -= name - } + processEvent(msg) + if (!repeat) { + timers -= name + } } + case SubscribeTransitionCallBack(actorRef) => + // send current state back as reference point + actorRef ! currentState.stateName + transitionCallBackList ::= actorRef + case UnsubscribeTransitionCallBack(actorRef) => + transitionCallBackList = transitionCallBackList.filterNot(_ == actorRef) case value => { - timeoutFuture = timeoutFuture.flatMap {ref => ref.cancel(true); None} + timeoutFuture = timeoutFuture.flatMap{ + ref => ref.cancel(true); None + } generation += 1 processEvent(value) } @@ -197,14 +219,21 @@ trait FSM[S, D] { terminate(Failure("Next state %s does not exist".format(nextState.stateName))) } else { if (currentState.stateName != nextState.stateName) { - transitionEvent.apply(Transition(currentState.stateName, nextState.stateName)) - } - currentState = nextState - currentState.timeout orElse stateTimeouts(currentState.stateName) foreach { t => - if (t.length >= 0) { - timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t.length, t.unit)) - } + val transition = Transition(currentState.stateName, nextState.stateName) + transitionEvent.apply(transition) + transitionCallBackList.foreach(_ ! transition) } + applyState(nextState) + } + } + + private def applyState(nextState: State) = { + currentState = nextState + currentState.timeout orElse stateTimeouts(currentState.stateName) foreach { + t => + if (t.length >= 0) { + timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t.length, t.unit)) + } } } @@ -214,15 +243,13 @@ trait FSM[S, D] { } - case class Event(event: Any, stateData: D) - case class State(stateName: S, stateData: D, timeout: Timeout = None) { def forMax(timeout: Duration): State = { copy(timeout = Some(timeout)) } - - def replying(replyValue:Any): State = { + + def replying(replyValue: Any): State = { self.sender match { case Some(sender) => sender ! replyValue case None => log.slf4j.error("Unable to send reply value {}, no sender reference to reply to", replyValue) @@ -235,11 +262,11 @@ trait FSM[S, D] { } private[akka] var stopReason: Option[Reason] = None + private[akka] def withStopReason(reason: Reason): State = { stopReason = Some(reason) this } } - case class Transition(from: S, to: S) } diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala index ed9a433b73..ab7c052e8a 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala @@ -6,19 +6,21 @@ package akka.actor import org.scalatest.junit.JUnitSuite import org.junit.Test +import FSM._ import org.multiverse.api.latches.StandardLatch import java.util.concurrent.TimeUnit object FSMActorSpec { - import FSM._ + val unlockedLatch = new StandardLatch val lockedLatch = new StandardLatch val unhandledLatch = new StandardLatch val terminatedLatch = new StandardLatch val transitionLatch = new StandardLatch + val transitionCallBackLatch = new StandardLatch sealed trait LockState case object Locked extends LockState @@ -26,11 +28,6 @@ object FSMActorSpec { class Lock(code: String, timeout: (Long, TimeUnit)) extends Actor with FSM[LockState, CodeState] { - notifying { - case Transition(Locked, Open) => transitionLatch.open - case Transition(_, _) => () - } - when(Locked) { case Event(digit: Char, CodeState(soFar, code)) => { soFar + digit match { @@ -57,8 +54,6 @@ object FSMActorSpec { } } - startWith(Locked, CodeState("", code)) - whenUnhandled { case Event(_, stateData) => { log.slf4j.info("Unhandled") @@ -67,10 +62,17 @@ object FSMActorSpec { } } + onTransition { + case Transition(Locked, Open) => transitionLatch.open + case Transition(_, _) => () + } + onTermination { case reason => terminatedLatch.open } + startWith(Locked, CodeState("", code)) + private def doLock() { log.slf4j.info("Locked") lockedLatch.open @@ -88,12 +90,19 @@ object FSMActorSpec { class FSMActorSpec extends JUnitSuite { import FSMActorSpec._ + @Test def unlockTheLock = { // lock that locked after being open for 1 sec val lock = Actor.actorOf(new Lock("33221", (1, TimeUnit.SECONDS))).start + val transitionTester = Actor.actorOf(new Actor { def receive = { + case Transition(_, _) => transitionCallBackLatch.open + }}).start + + lock ! SubscribeTransitionCallBack(transitionTester) + lock ! '3' lock ! '3' lock ! '2' @@ -102,8 +111,10 @@ class FSMActorSpec extends JUnitSuite { assert(unlockedLatch.tryAwait(1, TimeUnit.SECONDS)) assert(transitionLatch.tryAwait(1, TimeUnit.SECONDS)) + assert(transitionCallBackLatch.tryAwait(1, TimeUnit.SECONDS)) assert(lockedLatch.tryAwait(2, TimeUnit.SECONDS)) + lock ! "not_handled" assert(unhandledLatch.tryAwait(2, TimeUnit.SECONDS)) diff --git a/akka-actor/src/main/scala/akka/actor/Buncher.scala b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala old mode 100755 new mode 100644 similarity index 80% rename from akka-actor/src/main/scala/akka/actor/Buncher.scala rename to akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala index ce926e9d15..e9d54cccbe --- a/akka-actor/src/main/scala/akka/actor/Buncher.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala @@ -1,18 +1,19 @@ -package akka.actor +package sample.fsm.buncher import scala.reflect.ClassManifest import akka.util.Duration +import akka.actor.{FSM, Actor} /* - * generic typed object buncher. - * - * To instantiate it, use the factory method like so: - * Buncher(100, 500)(x : List[AnyRef] => x foreach println) - * which will yield a fully functional and started ActorRef. - * The type of messages allowed is strongly typed to match the - * supplied processing method; other messages are discarded (and - * possibly logged). - */ +* generic typed object buncher. +* +* To instantiate it, use the factory method like so: +* Buncher(100, 500)(x : List[AnyRef] => x foreach println) +* which will yield a fully functional and started ActorRef. +* The type of messages allowed is strongly typed to match the +* supplied processing method; other messages are discarded (and +* possibly logged). +*/ object Buncher { trait State case object Idle extends State diff --git a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala index 4104f6c18f..1147e6d0bd 100644 --- a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala @@ -3,8 +3,8 @@ package sample.fsm.dining.fsm import akka.actor.{ActorRef, Actor, FSM} import akka.actor.FSM._ import Actor._ -import java.util.concurrent.TimeUnit -import TimeUnit._ +import akka.util.Duration +import akka.util.duration._ /* * Some messages for the chopstick @@ -84,7 +84,7 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit when(Waiting) { case Event(Think, _) => log.info("%s starts to think", name) - startThinking(5, SECONDS) + startThinking(5 seconds) } //When a hakker is thinking it can become hungry @@ -118,12 +118,12 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit case Event(Busy(chopstick), TakenChopsticks(leftOption, rightOption)) => leftOption.foreach(_ ! Put) rightOption.foreach(_ ! Put) - startThinking(10, MILLISECONDS) + startThinking(10 milliseconds) } private def startEating(left: ActorRef, right: ActorRef): State = { log.info("%s has picked up %s and %s, and starts to eat", name, left.id, right.id) - goto(Eating) using TakenChopsticks(Some(left), Some(right)) forMax (5, SECONDS) + goto(Eating) using TakenChopsticks(Some(left), Some(right)) forMax (5 seconds) } // When the results of the other grab comes back, @@ -132,9 +132,9 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit when(FirstChopstickDenied) { case Event(Taken(secondChopstick), _) => secondChopstick ! Put - startThinking(10, MILLISECONDS) + startThinking(10 milliseconds) case Event(Busy(chopstick), _) => - startThinking(10, MILLISECONDS) + startThinking(10 milliseconds) } // When a hakker is eating, he can decide to start to think, @@ -144,11 +144,11 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit log.info("%s puts down his chopsticks and starts to think", name) left ! Put right ! Put - startThinking(5, SECONDS) + startThinking(5 seconds) } - private def startThinking(period: Int, timeUnit: TimeUnit): State = { - goto(Thinking) using TakenChopsticks(None, None) forMax (period, timeUnit) + private def startThinking(duration: Duration): State = { + goto(Thinking) using TakenChopsticks(None, None) forMax duration } //All hakkers start waiting From 63617e1b712ba72123cc555c767d6bb20a39c122 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Mon, 20 Dec 2010 20:52:27 +0100 Subject: [PATCH 04/31] add user documentation comments to FSM - also remove checks from startWith again, but point users to initialize --- .../src/main/scala/akka/actor/FSM.scala | 143 +++++++++++++++++- 1 file changed, 136 insertions(+), 7 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 373d372677..ec55aa0e04 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -51,6 +51,65 @@ object FSM { implicit def p2od(p: (Long, TimeUnit)): Duration = new Duration(p._1, p._2) } +/** + * Finite State Machine actor trait. Use as follows: + * + *
+ *   object A {
+ *     trait State
+ *     case class One extends State
+ *     case class Two extends State
+ *
+ *     case class Data(i : Int)
+ *   }
+ *
+ *   class A extends Actor with FSM[A.State, A.Data] {
+ *     import A._
+ *
+ *     startWith(One, Data(42))
+ *     when(One) { [some partial function] }
+ *     when(Two, stateTimeout = 5 seconds) { ... }
+ *     initialize
+ *   }
+ * 
+ * + * Within the partial function the following values are returned for effecting + * state transitions: + * + * - stay for staying in the same state + * - stay using Data(...) for staying in the same state, but with + * different data + * - stay forMax 5.millis for staying with a state timeout; can be + * combined with using + * - goto(...) for changing into a different state; also supports + * using and forMax + * - stop for terminating this FSM actor + * + * Each of the above also supports the method replying(AnyRef) for + * sending a reply before changing state. + * + * Another feature is that other actors may subscribe for transition events by + * sending a SubscribeTransitionCallback message to this actor; + * use UnsubscribeTransitionCallback before stopping the other + * actor. + * + * State timeouts set an upper bound to the time which may pass before another + * message is received in the current state. If no external message is + * available, then upon expiry of the timeout a StateTimeout message is sent. + * Note that this message will only be received in the state for which the + * timeout was set and that any message received will cancel the timeout + * (possibly to be started again by the next transition). + * + * Another feature is the ability to install and cancel single-shot as well as + * repeated timers which arrange for the sending of a user-specified message: + * + *
+ *   setTimer("tock", TockMsg, 1 second, true) // repeating
+ *   setTimer("lifetime", TerminateMsg, 1 hour, false) // single-shot
+ *   cancelTimer("tock")
+ *   timerActive_? ("tock")
+ * 
+ */ trait FSM[S, D] { this: Actor => @@ -59,34 +118,74 @@ trait FSM[S, D] { type StateFunction = scala.PartialFunction[Event[D], State] type Timeout = Option[Duration] - /**DSL */ + /* DSL */ + + /** + * Insert a new StateFunction at the end of the processing chain for the + * given state. If the stateTimeout parameter is set, entering this state + * without a differing explicit timeout setting will trigger a StateTimeout + * event; the same is true when using #stay. + * + * @param stateName designator for the state + * @param stateTimeout default state timeout for this state + * @param stateFunction partial function describing response to input + */ protected final def when(stateName: S, stateTimeout: Timeout = None)(stateFunction: StateFunction) = { register(stateName, stateFunction, stateTimeout) } + /** + * Set initial state. Call this method from the constructor before the #initialize method. + * + * @param stateName initial state designator + * @param stateData initial state data + * @param timeout state timeout for the initial state, overriding the default timeout for that state + */ protected final def startWith(stateName: S, stateData: D, timeout: Timeout = None) = { - applyState(State(stateName, stateData, timeout)) + currentState = State(stateName, stateData, timeout) } + /** + * Produce transition to other state. Return this from a state function in + * order to effect the transition. + * + * @param nextStateName state designator for the next state + * @return state transition descriptor + */ protected final def goto(nextStateName: S): State = { State(nextStateName, currentState.stateData) } + /** + * Produce "empty" transition descriptor. Return this from a state function + * when no state change is to be effected. + * + * @return descriptor for staying in current state + */ protected final def stay(): State = { // cannot directly use currentState because of the timeout field goto(currentState.stateName) } + /** + * Produce change descriptor to stop this FSM actor with reason "Normal". + */ protected final def stop(): State = { stop(Normal) } + /** + * Produce change descriptor to stop this FSM actor including specified reason. + */ protected final def stop(reason: Reason): State = { stop(reason, currentState.stateData) } + /** + * Produce change descriptor to stop this FSM actor including specified reason. + */ protected final def stop(reason: Reason, stateData: D): State = { stay using stateData withStopReason (reason) } @@ -97,7 +196,7 @@ trait FSM[S, D] { * @param msg message to be delivered * @param timeout delay of first message delivery and between subsequent messages * @param repeat send once if false, scheduleAtFixedRate if true - * @return current State + * @return current state descriptor */ protected final def setTimer(name: String, msg: AnyRef, timeout: Duration, repeat: Boolean): State = { if (timers contains name) { @@ -111,8 +210,7 @@ trait FSM[S, D] { /** * Cancel named timer, ensuring that the message is not subsequently delivered (no race). - * @param name - * @return + * @param name of the timer to cancel */ protected final def cancelTimer(name: String) = { if (timers contains name) { @@ -121,23 +219,40 @@ trait FSM[S, D] { } } + /** + * Inquire whether the named timer is still active. Returns true unless the + * timer does not exist, has previously been canceled or if it was a + * single-shot timer whose message was already received. + */ protected final def timerActive_?(name: String) = timers contains name - /**callbacks */ + /** + * Set handler which is called upon each state transition, i.e. not when + * staying in the same state. + */ protected final def onTransition(transitionHandler: PartialFunction[Transition[S], Unit]) = { transitionEvent = transitionHandler } + /** + * Set handler which is called upon termination of this FSM actor. + */ protected final def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { terminateEvent = terminationHandler } + /** + * Set handler which is called upon reception of unhandled messages. + */ protected final def whenUnhandled(stateFunction: StateFunction) = { handleEvent = stateFunction } + /** + * Verify existence of initial state and setup timers. This should be the + * last call within the constructor. + */ def initialize { - // check existence of initial state and setup timeout makeTransition(currentState) } @@ -245,10 +360,20 @@ trait FSM[S, D] { case class State(stateName: S, stateData: D, timeout: Timeout = None) { + /** + * Modify state transition descriptor to include a state timeout for the + * next state. This timeout overrides any default timeout set for the next + * state. + */ def forMax(timeout: Duration): State = { copy(timeout = Some(timeout)) } + /** + * Send reply to sender of the current message, if available. + * + * @return this state transition descriptor + */ def replying(replyValue: Any): State = { self.sender match { case Some(sender) => sender ! replyValue @@ -257,6 +382,10 @@ trait FSM[S, D] { this } + /** + * Modify state transition descriptor with new state data. The data will be + * set when transitioning to the new state. + */ def using(nextStateDate: D): State = { copy(stateData = nextStateDate) } From 993b60af3516ebd8efbdfac293e77271dc3ebc3a Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Mon, 20 Dec 2010 20:53:30 +0100 Subject: [PATCH 05/31] add minutes and hours to Duration --- akka-actor/src/main/scala/akka/util/Duration.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/akka-actor/src/main/scala/akka/util/Duration.scala b/akka-actor/src/main/scala/akka/util/Duration.scala index 4b8c6e7f9a..9ed9051136 100644 --- a/akka-actor/src/main/scala/akka/util/Duration.scala +++ b/akka-actor/src/main/scala/akka/util/Duration.scala @@ -85,6 +85,12 @@ class DurationInt(n: Int) { def seconds = Duration(n, TimeUnit.SECONDS) def second = Duration(n, TimeUnit.SECONDS) + + def minutes = Duration(60 * n, TimeUnit.SECONDS) + def minute = Duration(60 * n, TimeUnit.SECONDS) + + def hours = Duration(3600 * n, TimeUnit.SECONDS) + def hour = Duration(3600 * n, TimeUnit.SECONDS) } class DurationLong(n: Long) { @@ -105,4 +111,10 @@ class DurationLong(n: Long) { def seconds = Duration(n, TimeUnit.SECONDS) def second = Duration(n, TimeUnit.SECONDS) + + def minutes = Duration(60 * n, TimeUnit.SECONDS) + def minute = Duration(60 * n, TimeUnit.SECONDS) + + def hours = Duration(3600 * n, TimeUnit.SECONDS) + def hour = Duration(3600 * n, TimeUnit.SECONDS) } From 4ba3ed666bff49454347f78caeb29ee26caf4bc6 Mon Sep 17 00:00:00 2001 From: momania Date: Fri, 24 Dec 2010 16:30:45 +0100 Subject: [PATCH 06/31] wrap stop reason in stop even with current state, so state can be referenced in onTermination call for cleanup reasons etc --- .../src/main/scala/akka/actor/FSM.scala | 22 ++++++++++--------- .../scala/akka/actor/actor/FSMActorSpec.scala | 6 +++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index ec55aa0e04..69fe13effa 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -10,17 +10,18 @@ import java.util.concurrent.{ScheduledFuture, TimeUnit} object FSM { - sealed trait Reason - case object Normal extends Reason - case object Shutdown extends Reason - case class Failure(cause: Any) extends Reason - case class Event[D](event: Any, stateData: D) case class Transition[S](from: S, to: S) case class SubscribeTransitionCallBack(actorRef: ActorRef) case class UnsubscribeTransitionCallBack(actorRef: ActorRef) + sealed trait Reason + case object Normal extends Reason + case object Shutdown extends Reason + case class Failure(cause: Any) extends Reason + case class StopEvent[S, D](reason: Reason, currentState: S, stateData: D) + case object StateTimeout case class TimeoutMarker(generation: Long) @@ -237,7 +238,7 @@ trait FSM[S, D] { /** * Set handler which is called upon termination of this FSM actor. */ - protected final def onTermination(terminationHandler: PartialFunction[Reason, Unit]) = { + protected final def onTermination(terminationHandler: PartialFunction[StopEvent[S,D], Unit]) = { terminateEvent = terminationHandler } @@ -284,9 +285,10 @@ trait FSM[S, D] { stay } - private var terminateEvent: PartialFunction[Reason, Unit] = { - case failure@Failure(_) => log.slf4j.error("Stopping because of a {}", failure) - case reason => log.slf4j.info("Stopping because of reason: {}", reason) + private var terminateEvent: PartialFunction[StopEvent[S,D], Unit] = { + case StopEvent(Failure(cause), _, _) => + log.slf4j.error("Stopping because of a failure with cause {}", cause) + case StopEvent(reason, _, _) => log.slf4j.info("Stopping because of reason: {}", reason) } private var transitionEvent: PartialFunction[Transition[S], Unit] = { @@ -353,7 +355,7 @@ trait FSM[S, D] { } private def terminate(reason: Reason) = { - terminateEvent.apply(reason) + terminateEvent.apply(StopEvent(reason, currentState.stateName, currentState.stateData)) self.stop } diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala index ab7c052e8a..445ae3f7c7 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala @@ -44,7 +44,7 @@ object FSMActorSpec { } } case Event("hello", _) => stay replying "world" - case Event("bye", _) => stop + case Event("bye", _) => stop(Shutdown) } when(Open) { @@ -68,7 +68,9 @@ object FSMActorSpec { } onTermination { - case reason => terminatedLatch.open + case StopEvent(Shutdown, Locked, _) => + // stop is called from lockstate with shutdown as reason... + terminatedLatch.open } startWith(Locked, CodeState("", code)) From d17519131eac988725786819a9723d5f0cc7e78f Mon Sep 17 00:00:00 2001 From: momania Date: Fri, 24 Dec 2010 16:43:17 +0100 Subject: [PATCH 07/31] improved test - test for intial state on transition call back and use initialize function from FSM to kick of the machine. --- .../src/test/scala/akka/actor/actor/FSMActorSpec.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala index 445ae3f7c7..7520058fe3 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala @@ -20,6 +20,7 @@ object FSMActorSpec { val unhandledLatch = new StandardLatch val terminatedLatch = new StandardLatch val transitionLatch = new StandardLatch + val initialStateLatch = new StandardLatch val transitionCallBackLatch = new StandardLatch sealed trait LockState @@ -28,6 +29,8 @@ object FSMActorSpec { class Lock(code: String, timeout: (Long, TimeUnit)) extends Actor with FSM[LockState, CodeState] { + startWith(Locked, CodeState("", code)) + when(Locked) { case Event(digit: Char, CodeState(soFar, code)) => { soFar + digit match { @@ -73,7 +76,8 @@ object FSMActorSpec { terminatedLatch.open } - startWith(Locked, CodeState("", code)) + // initialize the lock + initialize private def doLock() { log.slf4j.info("Locked") @@ -101,9 +105,11 @@ class FSMActorSpec extends JUnitSuite { val transitionTester = Actor.actorOf(new Actor { def receive = { case Transition(_, _) => transitionCallBackLatch.open + case Locked => initialStateLatch.open }}).start lock ! SubscribeTransitionCallBack(transitionTester) + assert(initialStateLatch.tryAwait(1, TimeUnit.SECONDS)) lock ! '3' lock ! '3' From bdeef69b9a7c58d907c789012be3411f084204b4 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Sun, 26 Dec 2010 17:32:32 +0100 Subject: [PATCH 08/31] revamp akka.util.Duration - add comparison and arithmetic - add infinite subtypes --- .../src/main/scala/akka/util/Duration.scala | 204 +++++++++++++++++- 1 file changed, 201 insertions(+), 3 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/Duration.scala b/akka-actor/src/main/scala/akka/util/Duration.scala index 1ab14164fe..d11168dcf5 100644 --- a/akka-actor/src/main/scala/akka/util/Duration.scala +++ b/akka-actor/src/main/scala/akka/util/Duration.scala @@ -7,8 +7,8 @@ package akka.util import java.util.concurrent.TimeUnit object Duration { - def apply(length: Long, unit: TimeUnit) = new Duration(length, unit) - def apply(length: Long, unit: String) = new Duration(length, timeUnit(unit)) + def apply(length: Long, unit: TimeUnit) : Duration = new FiniteDuration(length, unit) + def apply(length: Long, unit: String) : Duration = new FiniteDuration(length, timeUnit(unit)) def timeUnit(unit: String) = unit.toLowerCase match { case "nanoseconds" | "nanos" | "nanosecond" | "nano" => TimeUnit.NANOSECONDS @@ -16,6 +16,45 @@ object Duration { case "milliseconds" | "millis" | "millisecond" | "milli" => TimeUnit.MILLISECONDS case _ => TimeUnit.SECONDS } + + trait Infinite { + this : Duration => + + override def equals(other : Any) = false + + def +(other : Duration) : Duration = this + def -(other : Duration) : Duration = this + def *(other : Double) : Duration = this + def /(other : Double) : Duration = this + + def finite_? = false + + def length : Long = error("length not allowed on infinite Durations") + def unit : TimeUnit = error("unit not allowed on infinite Durations") + def toNanos : Long = error("toNanos not allowed on infinite Durations") + def toMicros : Long = error("toMicros not allowed on infinite Durations") + def toMillis : Long = error("toMillis not allowed on infinite Durations") + def toSeconds : Long = error("toSeconds not allowed on infinite Durations") + } + + object Inf extends Duration with Infinite { + override def toString = "Duration.Inf" + def >(other : Duration) = false + def >=(other : Duration) = false + def <(other : Duration) = true + def <=(other : Duration) = true + def unary_- : Duration = MinusInf + } + + object MinusInf extends Duration with Infinite { + override def toString = "Duration.MinusInf" + def >(other : Duration) = true + def >=(other : Duration) = true + def <(other : Duration) = false + def <=(other : Duration) = false + def unary_- : Duration = MinusInf + } + } /** @@ -53,18 +92,177 @@ object Duration { * val duration = 100.millis * */ -class Duration(val length: Long, val unit: TimeUnit) { +trait Duration { + def length : Long + def unit : TimeUnit + def toNanos : Long + def toMicros : Long + def toMillis : Long + def toSeconds : Long + def <(other : Duration) : Boolean + def <=(other : Duration) : Boolean + def >(other : Duration) : Boolean + def >=(other : Duration) : Boolean + def +(other : Duration) : Duration + def -(other : Duration) : Duration + def *(factor : Double) : Duration + def /(factor : Double) : Duration + def unary_- : Duration + def finite_? : Boolean +} + +class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { def this(length: Long, unit: String) = this(length, Duration.timeUnit(unit)) def toNanos = unit.toNanos(length) def toMicros = unit.toMicros(length) def toMillis = unit.toMillis(length) def toSeconds = unit.toSeconds(length) override def toString = "Duration(" + length + ", " + unit + ")" + + def <(other : Duration) = { + if (other.finite_?) { + toNanos < other.asInstanceOf[FiniteDuration].toNanos + } else { + other > this + } + } + + def <=(other : Duration) = { + if (other.finite_?) { + toNanos <= other.asInstanceOf[FiniteDuration].toNanos + } else { + other >= this + } + } + + def >(other : Duration) = { + if (other.finite_?) { + toNanos > other.asInstanceOf[FiniteDuration].toNanos + } else { + other < this + } + } + + def >=(other : Duration) = { + if (other.finite_?) { + toNanos >= other.asInstanceOf[FiniteDuration].toNanos + } else { + other <= this + } + } + + private def fromNanos(nanos : Long) : Duration = { + if (nanos % 1000000000L == 0) { + Duration(nanos / 1000000000L, TimeUnit.SECONDS) + } else if (nanos % 1000000L == 0) { + Duration(nanos / 1000000L, TimeUnit.MILLISECONDS) + } else if (nanos % 1000L == 0) { + Duration(nanos / 1000L, TimeUnit.MICROSECONDS) + } else { + Duration(nanos, TimeUnit.NANOSECONDS) + } + } + + private def fromNanos(nanos : Double) : Duration = fromNanos(nanos.asInstanceOf[Long]) + + def +(other : Duration) = { + if (!other.finite_?) { + other + } else { + val nanos = toNanos + other.asInstanceOf[FiniteDuration].toNanos + fromNanos(nanos) + } + } + + def -(other : Duration) = { + if (!other.finite_?) { + other + } else { + val nanos = toNanos - other.asInstanceOf[FiniteDuration].toNanos + fromNanos(nanos) + } + } + + def *(factor : Double) = fromNanos(long2double(toNanos) * factor) + + def /(factor : Double) = fromNanos(long2double(toNanos) / factor) + + def unary_- = Duration(-length, unit) + + def finite_? = true + + override def equals(other : Any) = + other.isInstanceOf[FiniteDuration] && + toNanos == other.asInstanceOf[FiniteDuration].toNanos + + override def hashCode = toNanos.asInstanceOf[Int] +} + +object Inf extends Duration { + override def toString = "Duration.Inf" + override def equals(other : Any) = false + + def >(other : Duration) = true + def >=(other : Duration) = true + def <(other : Duration) = false + def <=(other : Duration) = false + + def +(other : Duration) : Duration = this + def -(other : Duration) : Duration = this + def *(other : Double) : Duration = this + def /(other : Double) : Duration = this + + def unary_- : Duration = MinusInf + + def finite_? = false + + def length : Long = error("length not allowed on Inf") + def unit : TimeUnit = error("unit not allowed on Inf") + def toNanos : Long = error("toNanos not allowed on Inf") + def toMicros : Long = error("toMicros not allowed on Inf") + def toMillis : Long = error("toMillis not allowed on Inf") + def toSeconds : Long = error("toSeconds not allowed on Inf") +} + +object MinusInf extends Duration { + override def toString = "Duration.MinusInf" + override def equals(other : Any) = false + + def >(other : Duration) = false + def >=(other : Duration) = false + def <(other : Duration) = true + def <=(other : Duration) = true + + def +(other : Duration) : Duration = this + def -(other : Duration) : Duration = this + def *(other : Double) : Duration = this + def /(other : Double) : Duration = this + + def unary_- : Duration = Inf + + def finite_? = false + + def length : Long = error("length not allowed on MinusInf") + def unit : TimeUnit = error("unit not allowed on MinusInf") + def toNanos : Long = error("toNanos not allowed on MinusInf") + def toMicros : Long = error("toMicros not allowed on MinusInf") + def toMillis : Long = error("toMillis not allowed on MinusInf") + def toSeconds : Long = error("toSeconds not allowed on MinusInf") } package object duration { implicit def intToDurationInt(n: Int) = new DurationInt(n) implicit def longToDurationLong(n: Long) = new DurationLong(n) + implicit def pairIntToDuration(p : (Int, TimeUnit)) = Duration(p._1, p._2) + implicit def pairLongToDuration(p : (Long, TimeUnit)) = Duration(p._1, p._2) + implicit def durationToPair(d : Duration) = (d.length, d.unit) + + implicit def intMult(i : Int) = new { + def *(d : Duration) = d * i + } + implicit def longMult(l : Long) = new { + def *(d : Duration) = d * l + } } class DurationInt(n: Int) { From 6a8b0e1d98d7553612ed8deeafce641119f3c84a Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Sun, 26 Dec 2010 22:49:40 +0100 Subject: [PATCH 09/31] first sketch of basic TestKit architecture --- .../src/main/scala/akka/util/TestKit.scala | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 akka-actor/src/main/scala/akka/util/TestKit.scala diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala new file mode 100644 index 0000000000..1f411e020a --- /dev/null +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -0,0 +1,71 @@ +package akka.util + +import akka.actor.{Actor, FSM} +import Actor._ +import duration._ + +import java.util.concurrent.{BlockingQueue, LinkedBlockingQueue} + +class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Null] { + import FSM._ + + startWith(0, null) + when(0, stateTimeout = 5 seconds) { + case Event(StateTimeout, _) => stop + case Event(x : AnyRef, _) => + queue offer x + stay + } + initialize +} + +trait TestKit extends Logging { + + private val queue = new LinkedBlockingQueue[AnyRef]() + + val sender = actorOf(new TestActor(queue)).start + implicit val senderOption = Some(sender) + + def within[T](max : Duration)(f : => T) : T = { + val start = now + val ret = f + val stop = now + val diff = stop - start + assert (diff <= max, "block took "+diff+" instead of "+max) + ret + } + + def within[T](min : Duration, max : Duration)(f : => T) : T = { + val start = now + val ret = f + val stop = now + val diff = stop - start + assert (diff >= min && diff <= max, "block took "+diff+", which is not in ("+min+","+max+")") + ret + } + + def expect(max : Duration, obj : Any) = { + val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + assert (obj == o, "expected "+obj+", found "+o) + o + } + + def expectAnyOf(max : Duration, obj : Any*) = { + val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + assert (obj exists (_ == o), "found unexpected "+o) + o + } + + def expectAllOf(max : Duration, obj : Any*) { + val end = now + max + val recv = for { + x <- 1 to obj.size + timeout = end - now + } yield queue.poll(timeout.length, timeout.unit) + assert (obj forall (x => recv exists (x == _)), "not found all") + } + + def now = System.currentTimeMillis.millis +} + +// vim: set ts=4 sw=4 et: From 4838121d0e134e84fc92c1531c8f905df5da5062 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Tue, 28 Dec 2010 17:30:13 +0100 Subject: [PATCH 10/31] add facility for changing stateTimeout dynamically --- akka-actor/src/main/scala/akka/actor/FSM.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index a77e64dfeb..065fb9d399 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -226,6 +226,14 @@ trait FSM[S, D] { */ protected final def timerActive_?(name: String) = timers contains name + /** + * Set state timeout explicitly. This method can safely be used from within a + * state handler. + */ + protected final def setStateTimeout(state : S, timeout : Timeout) { + stateTimeouts(state) = timeout + } + /** * Set handler which is called upon each state transition, i.e. not when * staying in the same state. From b868c909e0b474ab90498608d5e35482eaeeb3b7 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Tue, 28 Dec 2010 23:50:08 +0100 Subject: [PATCH 11/31] Improve Duration classes - add more factories and implicits (for Double) - add extractors for deconstruction and string parsing - fix bug in infinite durations (comparisons were inverted) --- .../src/main/scala/akka/util/Duration.scala | 297 +++++++++++------- 1 file changed, 180 insertions(+), 117 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/Duration.scala b/akka-actor/src/main/scala/akka/util/Duration.scala index f694839fb3..d279249b30 100644 --- a/akka-actor/src/main/scala/akka/util/Duration.scala +++ b/akka-actor/src/main/scala/akka/util/Duration.scala @@ -5,16 +5,70 @@ package akka.util import java.util.concurrent.TimeUnit +import TimeUnit._ +import java.lang.{Long => JLong, Double => JDouble} object Duration { def apply(length: Long, unit: TimeUnit) : Duration = new FiniteDuration(length, unit) + def apply(length: Double, unit: TimeUnit) : Duration = new FiniteDuration(length, unit) def apply(length: Long, unit: String) : Duration = new FiniteDuration(length, timeUnit(unit)) + /** + * Construct a Duration by parsing a String. In case of a format error, a + * RuntimeException is thrown. See `unapply(String)` for more information. + */ + def apply(s : String) : Duration = unapply(s) getOrElse error("format error") + + /** + * Deconstruct a Duration into length and unit if it is finite. + */ + def unapply(d : Duration) : Option[(Long, TimeUnit)] = { + if (d.finite_?) { + Some((d.length, d.unit)) + } else { + None + } + } + + private val RE = ("""^\s*(\d+(?:\.\d+)?)\s*"""+ // length part + "(?:"+ // units are distinguished in separate match groups + "(h|hour|hours)|"+ + "(min|minute|minutes)|"+ + "(s|sec|second|seconds)|"+ + "(ms|milli|millis|millisecond|milliseconds)|"+ + "(µs|micro|micros|microsecond|microseconds)|"+ + "(ns|nano|nanos|nanosecond|nanoseconds)"+ + """)\s*$""").r // close the non-capturing group + private val REinf = """^\s*Inf\s*$""".r + private val REminf = """^\s*(?:-\s*|Minus)Inf\s*""".r + + /** + * Parse String, return None if no match. Format is `""`, where + * whitespace is allowed before, between and after the parts. Infinities are + * designated by `"Inf"` and `"-Inf"` or `"MinusInf"`. + */ + def unapply(s : String) : Option[Duration] = s match { + case RE(length, h, m, s, ms, mus, ns) => + if (h ne null) Some(Duration(3600 * JDouble.parseDouble(length), SECONDS)) else + if (m ne null) Some(Duration(60 * JDouble.parseDouble(length), SECONDS)) else + if (s ne null) Some(Duration(1 * JDouble.parseDouble(length), SECONDS)) else + if (ms ne null) Some(Duration(1 * JDouble.parseDouble(length), MILLISECONDS)) else + if (mus ne null) Some(Duration(1 * JDouble.parseDouble(length), MICROSECONDS)) else + if (ns ne null) Some(Duration(1 * JDouble.parseDouble(length), NANOSECONDS)) else + error("made some error in regex (should not be possible)") + case REinf() => Some(Inf) + case REminf() => Some(MinusInf) + case _ => None + } + + /** + * Parse TimeUnit from string representation. + */ def timeUnit(unit: String) = unit.toLowerCase match { - case "nanoseconds" | "nanos" | "nanosecond" | "nano" => TimeUnit.NANOSECONDS - case "microseconds" | "micros" | "microsecond" | "micro" => TimeUnit.MICROSECONDS - case "milliseconds" | "millis" | "millisecond" | "milli" => TimeUnit.MILLISECONDS - case _ => TimeUnit.SECONDS + case "nanoseconds" | "nanos" | "nanosecond" | "nano" => NANOSECONDS + case "microseconds" | "micros" | "microsecond" | "micro" => MICROSECONDS + case "milliseconds" | "millis" | "millisecond" | "milli" => MILLISECONDS + case _ => SECONDS } trait Infinite { @@ -29,30 +83,38 @@ object Duration { def finite_? = false - def length : Long = error("length not allowed on infinite Durations") - def unit : TimeUnit = error("unit not allowed on infinite Durations") - def toNanos : Long = error("toNanos not allowed on infinite Durations") - def toMicros : Long = error("toMicros not allowed on infinite Durations") - def toMillis : Long = error("toMillis not allowed on infinite Durations") - def toSeconds : Long = error("toSeconds not allowed on infinite Durations") + def length : Long = throw new IllegalArgumentException("length not allowed on infinite Durations") + def unit : TimeUnit = throw new IllegalArgumentException("unit not allowed on infinite Durations") + def toNanos : Long = throw new IllegalArgumentException("toNanos not allowed on infinite Durations") + def toMicros : Long = throw new IllegalArgumentException("toMicros not allowed on infinite Durations") + def toMillis : Long = throw new IllegalArgumentException("toMillis not allowed on infinite Durations") + def toSeconds : Long = throw new IllegalArgumentException("toSeconds not allowed on infinite Durations") } + /** + * Infinite duration: greater than any other and not equal to any other, + * including itself. + */ object Inf extends Duration with Infinite { override def toString = "Duration.Inf" - def >(other : Duration) = false - def >=(other : Duration) = false - def <(other : Duration) = true - def <=(other : Duration) = true - def unary_- : Duration = MinusInf - } - - object MinusInf extends Duration with Infinite { - override def toString = "Duration.MinusInf" def >(other : Duration) = true def >=(other : Duration) = true def <(other : Duration) = false def <=(other : Duration) = false def unary_- : Duration = MinusInf + } + + /** + * Infinite negative duration: lesser than any other and not equal to any other, + * including itself. + */ + object MinusInf extends Duration with Infinite { + override def toString = "Duration.MinusInf" + def >(other : Duration) = false + def >=(other : Duration) = false + def <(other : Duration) = true + def <=(other : Duration) = true + def unary_- : Duration = Inf } } @@ -66,7 +128,7 @@ object Duration { * import akka.util.Duration; * import java.util.concurrent.TimeUnit; * - * Duration duration = new Duration(100, TimeUnit.MILLISECONDS); + * Duration duration = new Duration(100, MILLISECONDS); * Duration duration = new Duration(5, "seconds"); * * duration.toNanos(); @@ -78,18 +140,28 @@ object Duration { * import akka.util.Duration * import java.util.concurrent.TimeUnit * - * val duration = Duration(100, TimeUnit.MILLISECONDS) + * val duration = Duration(100, MILLISECONDS) * val duration = Duration(100, "millis") * * duration.toNanos + * duration < 1.second + * duration <= Duration.Inf * * *

- * Implicits are also provided for Int and Long. Example usage: + * Implicits are also provided for Int, Long and Double. Example usage: *

  * import akka.util.duration._
  *
- * val duration = 100.millis
+ * val duration = 100 millis
+ * 
+ * + * Extractors, parsing and arithmetic are also included: + *
+ * val d = Duration("1.2 µs")
+ * val Duration(length, unit) = 5 millis
+ * val d2 = d * 2.5
+ * val d3 = d2 + 1.millisecond
  * 
*/ trait Duration { @@ -113,11 +185,26 @@ trait Duration { class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { def this(length: Long, unit: String) = this(length, Duration.timeUnit(unit)) + def this(length: Double, unit: TimeUnit) = { + this(1, unit) + this * length + } + def toNanos = unit.toNanos(length) def toMicros = unit.toMicros(length) def toMillis = unit.toMillis(length) def toSeconds = unit.toSeconds(length) - override def toString = "Duration(" + length + ", " + unit + ")" + + override def toString = this match { + case Duration(1, SECONDS) => "1 second" + case Duration(x, SECONDS) => x+" seconds" + case Duration(1, MILLISECONDS) => "1 millisecond" + case Duration(x, MILLISECONDS) => x+" milliseconds" + case Duration(1, MICROSECONDS) => "1 microsecond" + case Duration(x, MICROSECONDS) => x+" microseconds" + case Duration(1, NANOSECONDS) => "1 nanosecond" + case Duration(x, NANOSECONDS) => x+" nanoseconds" + } def <(other : Duration) = { if (other.finite_?) { @@ -153,17 +240,17 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { private def fromNanos(nanos : Long) : Duration = { if (nanos % 1000000000L == 0) { - Duration(nanos / 1000000000L, TimeUnit.SECONDS) + Duration(nanos / 1000000000L, SECONDS) } else if (nanos % 1000000L == 0) { - Duration(nanos / 1000000L, TimeUnit.MILLISECONDS) + Duration(nanos / 1000000L, MILLISECONDS) } else if (nanos % 1000L == 0) { - Duration(nanos / 1000L, TimeUnit.MICROSECONDS) + Duration(nanos / 1000L, MICROSECONDS) } else { - Duration(nanos, TimeUnit.NANOSECONDS) + Duration(nanos, NANOSECONDS) } } - private def fromNanos(nanos : Double) : Duration = fromNanos(nanos.asInstanceOf[Long]) + private def fromNanos(nanos : Double) : Duration = fromNanos((nanos + 0.5).asInstanceOf[Long]) def +(other : Duration) = { if (!other.finite_?) { @@ -198,61 +285,11 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { override def hashCode = toNanos.asInstanceOf[Int] } -object Inf extends Duration { - override def toString = "Duration.Inf" - override def equals(other : Any) = false - - def >(other : Duration) = true - def >=(other : Duration) = true - def <(other : Duration) = false - def <=(other : Duration) = false - - def +(other : Duration) : Duration = this - def -(other : Duration) : Duration = this - def *(other : Double) : Duration = this - def /(other : Double) : Duration = this - - def unary_- : Duration = MinusInf - - def finite_? = false - - def length : Long = error("length not allowed on Inf") - def unit : TimeUnit = error("unit not allowed on Inf") - def toNanos : Long = error("toNanos not allowed on Inf") - def toMicros : Long = error("toMicros not allowed on Inf") - def toMillis : Long = error("toMillis not allowed on Inf") - def toSeconds : Long = error("toSeconds not allowed on Inf") -} - -object MinusInf extends Duration { - override def toString = "Duration.MinusInf" - override def equals(other : Any) = false - - def >(other : Duration) = false - def >=(other : Duration) = false - def <(other : Duration) = true - def <=(other : Duration) = true - - def +(other : Duration) : Duration = this - def -(other : Duration) : Duration = this - def *(other : Double) : Duration = this - def /(other : Double) : Duration = this - - def unary_- : Duration = Inf - - def finite_? = false - - def length : Long = error("length not allowed on MinusInf") - def unit : TimeUnit = error("unit not allowed on MinusInf") - def toNanos : Long = error("toNanos not allowed on MinusInf") - def toMicros : Long = error("toMicros not allowed on MinusInf") - def toMillis : Long = error("toMillis not allowed on MinusInf") - def toSeconds : Long = error("toSeconds not allowed on MinusInf") -} - package object duration { implicit def intToDurationInt(n: Int) = new DurationInt(n) implicit def longToDurationLong(n: Long) = new DurationLong(n) + implicit def doubleToDurationDouble(d: Double) = new DurationDouble(d) + implicit def pairIntToDuration(p : (Int, TimeUnit)) = Duration(p._1, p._2) implicit def pairLongToDuration(p : (Long, TimeUnit)) = Duration(p._1, p._2) implicit def durationToPair(d : Duration) = (d.length, d.unit) @@ -266,53 +303,79 @@ package object duration { } class DurationInt(n: Int) { - def nanoseconds = Duration(n, TimeUnit.NANOSECONDS) - def nanos = Duration(n, TimeUnit.NANOSECONDS) - def nanosecond = Duration(n, TimeUnit.NANOSECONDS) - def nano = Duration(n, TimeUnit.NANOSECONDS) + def nanoseconds = Duration(n, NANOSECONDS) + def nanos = Duration(n, NANOSECONDS) + def nanosecond = Duration(n, NANOSECONDS) + def nano = Duration(n, NANOSECONDS) - def microseconds = Duration(n, TimeUnit.MICROSECONDS) - def micros = Duration(n, TimeUnit.MICROSECONDS) - def microsecond = Duration(n, TimeUnit.MICROSECONDS) - def micro = Duration(n, TimeUnit.MICROSECONDS) + def microseconds = Duration(n, MICROSECONDS) + def micros = Duration(n, MICROSECONDS) + def microsecond = Duration(n, MICROSECONDS) + def micro = Duration(n, MICROSECONDS) - def milliseconds = Duration(n, TimeUnit.MILLISECONDS) - def millis = Duration(n, TimeUnit.MILLISECONDS) - def millisecond = Duration(n, TimeUnit.MILLISECONDS) - def milli = Duration(n, TimeUnit.MILLISECONDS) + def milliseconds = Duration(n, MILLISECONDS) + def millis = Duration(n, MILLISECONDS) + def millisecond = Duration(n, MILLISECONDS) + def milli = Duration(n, MILLISECONDS) - def seconds = Duration(n, TimeUnit.SECONDS) - def second = Duration(n, TimeUnit.SECONDS) + def seconds = Duration(n, SECONDS) + def second = Duration(n, SECONDS) - def minutes = Duration(60 * n, TimeUnit.SECONDS) - def minute = Duration(60 * n, TimeUnit.SECONDS) + def minutes = Duration(60 * n, SECONDS) + def minute = Duration(60 * n, SECONDS) - def hours = Duration(3600 * n, TimeUnit.SECONDS) - def hour = Duration(3600 * n, TimeUnit.SECONDS) + def hours = Duration(3600 * n, SECONDS) + def hour = Duration(3600 * n, SECONDS) } class DurationLong(n: Long) { - def nanoseconds = Duration(n, TimeUnit.NANOSECONDS) - def nanos = Duration(n, TimeUnit.NANOSECONDS) - def nanosecond = Duration(n, TimeUnit.NANOSECONDS) - def nano = Duration(n, TimeUnit.NANOSECONDS) + def nanoseconds = Duration(n, NANOSECONDS) + def nanos = Duration(n, NANOSECONDS) + def nanosecond = Duration(n, NANOSECONDS) + def nano = Duration(n, NANOSECONDS) - def microseconds = Duration(n, TimeUnit.MICROSECONDS) - def micros = Duration(n, TimeUnit.MICROSECONDS) - def microsecond = Duration(n, TimeUnit.MICROSECONDS) - def micro = Duration(n, TimeUnit.MICROSECONDS) + def microseconds = Duration(n, MICROSECONDS) + def micros = Duration(n, MICROSECONDS) + def microsecond = Duration(n, MICROSECONDS) + def micro = Duration(n, MICROSECONDS) - def milliseconds = Duration(n, TimeUnit.MILLISECONDS) - def millis = Duration(n, TimeUnit.MILLISECONDS) - def millisecond = Duration(n, TimeUnit.MILLISECONDS) - def milli = Duration(n, TimeUnit.MILLISECONDS) + def milliseconds = Duration(n, MILLISECONDS) + def millis = Duration(n, MILLISECONDS) + def millisecond = Duration(n, MILLISECONDS) + def milli = Duration(n, MILLISECONDS) - def seconds = Duration(n, TimeUnit.SECONDS) - def second = Duration(n, TimeUnit.SECONDS) + def seconds = Duration(n, SECONDS) + def second = Duration(n, SECONDS) - def minutes = Duration(60 * n, TimeUnit.SECONDS) - def minute = Duration(60 * n, TimeUnit.SECONDS) + def minutes = Duration(60 * n, SECONDS) + def minute = Duration(60 * n, SECONDS) - def hours = Duration(3600 * n, TimeUnit.SECONDS) - def hour = Duration(3600 * n, TimeUnit.SECONDS) + def hours = Duration(3600 * n, SECONDS) + def hour = Duration(3600 * n, SECONDS) +} + +class DurationDouble(d: Double) { + def nanoseconds = Duration(d, NANOSECONDS) + def nanos = Duration(d, NANOSECONDS) + def nanosecond = Duration(d, NANOSECONDS) + def nano = Duration(d, NANOSECONDS) + + def microseconds = Duration(d, MICROSECONDS) + def micros = Duration(d, MICROSECONDS) + def microsecond = Duration(d, MICROSECONDS) + def micro = Duration(d, MICROSECONDS) + + def milliseconds = Duration(d, MILLISECONDS) + def millis = Duration(d, MILLISECONDS) + def millisecond = Duration(d, MILLISECONDS) + def milli = Duration(d, MILLISECONDS) + + def seconds = Duration(d, SECONDS) + def second = Duration(d, SECONDS) + + def minutes = Duration(60 * d, SECONDS) + def minute = Duration(60 * d, SECONDS) + + def hours = Duration(3600 * d, SECONDS) + def hour = Duration(3600 * d, SECONDS) } From ab10f6c71072c6ca10fb9d0badcc18d94801760a Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Tue, 28 Dec 2010 23:51:41 +0100 Subject: [PATCH 12/31] revamp TestKit (with documentation) - add implicit wait bounding from within() blocks - add class matching - add test actor configuration and stop facilities --- .../src/main/scala/akka/util/TestKit.scala | 299 ++++++++++++++++-- 1 file changed, 276 insertions(+), 23 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala index 1f411e020a..17a663e535 100644 --- a/akka-actor/src/main/scala/akka/util/TestKit.scala +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -6,12 +6,21 @@ import duration._ import java.util.concurrent.{BlockingQueue, LinkedBlockingQueue} +object TestActor { + case class SetTimeout(d : Duration) +} + class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Null] { import FSM._ + import TestActor._ startWith(0, null) when(0, stateTimeout = 5 seconds) { - case Event(StateTimeout, _) => stop + case Event(SetTimeout(d), _) => + setStateTimeout(0, if (d.finite_?) d else None) + stay + case Event(StateTimeout, _) => + stop case Event(x : AnyRef, _) => queue offer x stay @@ -19,53 +28,297 @@ class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Null] initialize } -trait TestKit extends Logging { +/** + * Test kit for testing actors. Inheriting from this trait enables reception of + * replies from actors, which are queued by an internal actor and can be + * examined using the `expect...` methods. Assertions and bounds concerning + * timing are available in the form of `within` blocks. + * + *
+ * class Test extends TestKit {
+ *     val test = actorOf[SomeActor].start
+ *
+ *     within (1 second) {
+ *         test ! SomeWork
+ *         expect(Result1) // bounded to 1 second
+ *         expect(Result2) // bounded to the remainder of the 1 second
+ *     }
+ * }
+ * 
+ * + * Beware of two points: + * + * - the internal test actor needs to be stopped, either explicitly using + * `stopTestActor` or implicitly by using its internal inactivity timeout, + * see `setTestActorTimeout` + * - this trait is not thread-safe (only one actor with one queue, one stack + * of `within` blocks); it is expected that the code is executed from a + * constructor as shown above, which makes this a non-issue, otherwise take + * care not to run tests within a single test class in parallel. + */ +trait TestKit { private val queue = new LinkedBlockingQueue[AnyRef]() - val sender = actorOf(new TestActor(queue)).start - implicit val senderOption = Some(sender) + private val sender = actorOf(new TestActor(queue)).start + protected implicit val senderOption = Some(sender) - def within[T](max : Duration)(f : => T) : T = { - val start = now - val ret = f - val stop = now - val diff = stop - start - assert (diff <= max, "block took "+diff+" instead of "+max) - ret - } + private var end : Duration = Duration.Inf + /** + * Stop test actor. Should be done at the end of the test unless relying on + * test actor timeout. + */ + def stopTestActor { sender.stop } + + /** + * Set test actor timeout. By default, the test actor shuts itself down + * after 5 seconds of inactivity. Set this to Duration.Inf to disable this + * behavior, but make sure that someone will then call `stopTestActor`, + * unless you want to leak actors, e.g. wrap test in + * + *
+     *   try {
+     *     ...
+     *   } finally { stopTestActor }
+     * 
+ */ + def setTestActorTimeout(d : Duration) { sender ! TestActor.SetTimeout(d) } + + /** + * Obtain current time (`System.currentTimeMillis`) as Duration. + */ + def now : Duration = System.currentTimeMillis.millis + + /** + * Obtain time remaining for execution of the innermost enclosing `within` block. + */ + def remaining : Duration = end - now + + /** + * Execute code block while bounding its execution time between `min` and + * `max`. `within` blocks may be nested. All methods in this trait which + * take maximum wait times are available in a version which implicitly uses + * the remaining time governed by the innermost enclosing `within` block. + * + *
+     * val ret = within(50 millis) {
+     *             test ! "ping"
+     *             expectClass(classOf[String])
+     *           }
+     * 
+ */ def within[T](min : Duration, max : Duration)(f : => T) : T = { val start = now + val rem = end - start + assert (rem >= min, "required min time "+min+" not possible, only "+rem+" left") + + val max_diff = if (max < rem) max else rem + val prev_end = end + end = start + max_diff + val ret = f - val stop = now - val diff = stop - start - assert (diff >= min && diff <= max, "block took "+diff+", which is not in ("+min+","+max+")") + + val diff = now - start + assert (min <= diff, "block took "+diff+", should at least have been "+min) + assert (diff <= max_diff, "block took "+diff+", exceeding "+max_diff) + + end = prev_end ret } - def expect(max : Duration, obj : Any) = { + /** + * Same as calling `within(0 seconds, max)(f)`. + */ + def within[T](max : Duration)(f : => T) : T = within(0 seconds, max)(f) + + /** + * Same as `expect`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expect(obj : Any) : AnyRef = expect(remaining, obj) + + /** + * Receive one message from the test actor and assert that it equals the + * given object. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expect(max : Duration, obj : Any) : AnyRef = { val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + assert (o ne null, "timeout during expect") assert (obj == o, "expected "+obj+", found "+o) o } - def expectAnyOf(max : Duration, obj : Any*) = { + /** + * Same as `expect`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expect[T](f : PartialFunction[Any, T]) : T = expect(remaining)(f) + + /** + * Receive one message from the test actor and assert that the given + * partial function accepts it. Wait time is bounded by the given duration, + * with an AssertionFailure being thrown in case of timeout. + * + * Use this variant to implement more complicated or conditional + * processing. + * + * @return the received object as transformed by the partial function + */ + def expect[T](max : Duration)(f : PartialFunction[Any, T]) : T = { val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + assert (o ne null, "timeout during expect") + assert (f.isDefinedAt(o), "does not match: "+o) + f(o) + } + + /** + * Same as `expectClass`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectClass[C](c : Class[C]) : C = expectClass(remaining, c) + + /** + * Receive one message from the test actor and assert that it conforms to + * the given class. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectClass[C](max : Duration, c : Class[C]) : C = { + val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + assert (o ne null, "timeout during expectClass") + assert (c isInstance o, "expected "+c+", found "+o.getClass) + o.asInstanceOf[C] + } + + /** + * Same as `expectAnyOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectAnyOf(obj : Any*) : AnyRef = expectAnyOf(remaining, obj : _*) + + /** + * Receive one message from the test actor and assert that it equals one of + * the given objects. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectAnyOf(max : Duration, obj : Any*) : AnyRef = { + val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + assert (o ne null, "timeout during expectAnyOf") assert (obj exists (_ == o), "found unexpected "+o) o } + /** + * Same as `expectAnyClassOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectAnyClassOf(obj : Class[_]*) : AnyRef = expectAnyClassOf(remaining, obj : _*) + + /** + * Receive one message from the test actor and assert that it conforms to + * one of the given classes. Wait time is bounded by the given duration, + * with an AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { + val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + assert (o ne null, "timeout during expectAnyClassOf") + assert (obj exists (_ isInstance o), "found unexpected "+o) + o + } + + /** + * Same as `expectAllOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectAllOf(obj : Any*) { expectAllOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of objects and assert that for each given object one is received + * which equals it. This construct is useful when the order in which the + * objects are received is not fixed. Wait time is bounded by the given + * duration, with an AssertionFailure being thrown in case of timeout. + * + *
+     * within(1 second) {
+     *   dispatcher ! SomeWork1()
+     *   dispatcher ! SomeWork2()
+     *   expectAllOf(Result1(), Result2())
+     * }
+     * 
+ */ def expectAllOf(max : Duration, obj : Any*) { - val end = now + max - val recv = for { - x <- 1 to obj.size - timeout = end - now - } yield queue.poll(timeout.length, timeout.unit) + val stop = now + max + val recv = for { x <- 1 to obj.size } yield { + val timeout = stop - now + val o = queue.poll(timeout.length, timeout.unit) + assert (o ne null, "timeout during expectAllClassOf") + o + } assert (obj forall (x => recv exists (x == _)), "not found all") } - def now = System.currentTimeMillis.millis + /** + * Same as `expectAllClassOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectAllClassOf(obj : Class[_]*) { expectAllClassOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of classes and assert that for each given class one is received + * which is of that class (equality, not conformance). This construct is + * useful when the order in which the objects are received is not fixed. + * Wait time is bounded by the given duration, with an AssertionFailure + * being thrown in case of timeout. + */ + def expectAllClassOf(max : Duration, obj : Class[_]*) { + val stop = now + max + val recv = for { x <- 1 to obj.size } yield { + val timeout = stop - now + val o = queue.poll(timeout.length, timeout.unit) + assert (o ne null, "timeout during expectAllClassOf") + o + } + assert (obj forall (x => recv exists (_.getClass eq x)), "not found all") + } + + /** + * Same as `expectAllConformingOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectAllConformingOf(obj : Class[_]*) { expectAllClassOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of classes and assert that for each given class one is received + * which conforms to that class. This construct is useful when the order in + * which the objects are received is not fixed. Wait time is bounded by + * the given duration, with an AssertionFailure being thrown in case of + * timeout. + * + * Beware that one object may satisfy all given class constraints, which + * may be counter-intuitive. + */ + def expectAllConformingOf(max : Duration, obj : Class[_]*) { + val stop = now + max + val recv = for { x <- 1 to obj.size } yield { + val timeout = stop - now + val o = queue.poll(timeout.length, timeout.unit) + assert (o ne null, "timeout during expectAllClassOf") + o + } + assert (obj forall (x => recv exists (x isInstance _)), "not found all") + } } // vim: set ts=4 sw=4 et: From e83ef89aad52da6e50f3bec4be1c70077a1f2f92 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Wed, 29 Dec 2010 11:23:24 +0100 Subject: [PATCH 13/31] code cleanup (thanks, Viktor and Irmo) --- .../src/main/scala/akka/util/TestKit.scala | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala index 17a663e535..2a3e136756 100644 --- a/akka-actor/src/main/scala/akka/util/TestKit.scala +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -10,11 +10,11 @@ object TestActor { case class SetTimeout(d : Duration) } -class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Null] { +class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Unit] { import FSM._ import TestActor._ - startWith(0, null) + startWith(0, ()) when(0, stateTimeout = 5 seconds) { case Event(SetTimeout(d), _) => setStateTimeout(0, if (d.finite_?) d else None) @@ -146,7 +146,7 @@ trait TestKit { * @return the received object */ def expect(max : Duration, obj : Any) : AnyRef = { - val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + val o = receiveOne(max) assert (o ne null, "timeout during expect") assert (obj == o, "expected "+obj+", found "+o) o @@ -169,7 +169,7 @@ trait TestKit { * @return the received object as transformed by the partial function */ def expect[T](max : Duration)(f : PartialFunction[Any, T]) : T = { - val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + val o = receiveOne(max) assert (o ne null, "timeout during expect") assert (f.isDefinedAt(o), "does not match: "+o) f(o) @@ -189,7 +189,7 @@ trait TestKit { * @return the received object */ def expectClass[C](max : Duration, c : Class[C]) : C = { - val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + val o = receiveOne(max) assert (o ne null, "timeout during expectClass") assert (c isInstance o, "expected "+c+", found "+o.getClass) o.asInstanceOf[C] @@ -209,7 +209,7 @@ trait TestKit { * @return the received object */ def expectAnyOf(max : Duration, obj : Any*) : AnyRef = { - val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + val o = receiveOne(max) assert (o ne null, "timeout during expectAnyOf") assert (obj exists (_ == o), "found unexpected "+o) o @@ -229,7 +229,7 @@ trait TestKit { * @return the received object */ def expectAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { - val o = if (max.finite_?) queue.poll(max.length, max.unit) else queue.take + val o = receiveOne(max) assert (o ne null, "timeout during expectAnyClassOf") assert (obj exists (_ isInstance o), "found unexpected "+o) o @@ -257,13 +257,7 @@ trait TestKit { * */ def expectAllOf(max : Duration, obj : Any*) { - val stop = now + max - val recv = for { x <- 1 to obj.size } yield { - val timeout = stop - now - val o = queue.poll(timeout.length, timeout.unit) - assert (o ne null, "timeout during expectAllClassOf") - o - } + val recv = receiveN(obj.size, now + max) assert (obj forall (x => recv exists (x == _)), "not found all") } @@ -282,13 +276,7 @@ trait TestKit { * being thrown in case of timeout. */ def expectAllClassOf(max : Duration, obj : Class[_]*) { - val stop = now + max - val recv = for { x <- 1 to obj.size } yield { - val timeout = stop - now - val o = queue.poll(timeout.length, timeout.unit) - assert (o ne null, "timeout during expectAllClassOf") - o - } + val recv = receiveN(obj.size, now + max) assert (obj forall (x => recv exists (_.getClass eq x)), "not found all") } @@ -310,14 +298,25 @@ trait TestKit { * may be counter-intuitive. */ def expectAllConformingOf(max : Duration, obj : Class[_]*) { - val stop = now + max - val recv = for { x <- 1 to obj.size } yield { + val recv = receiveN(obj.size, now + max) + assert (obj forall (x => recv exists (x isInstance _)), "not found all") + } + + private def receiveN(n : Int, stop : Duration) = { + for { x <- 1 to n } yield { val timeout = stop - now - val o = queue.poll(timeout.length, timeout.unit) - assert (o ne null, "timeout during expectAllClassOf") + val o = receiveOne(timeout) + assert (o ne null, "timeout while expecting "+n+" messages") o } - assert (obj forall (x => recv exists (x isInstance _)), "not found all") + } + + private def receiveOne(max : Duration) = { + if (max.finite_?) { + queue.poll(max.length, max.unit) + } else { + queue.take + } } } From 68c0f7c8e9ef0ba1ca0d21afba231462310034ec Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Wed, 29 Dec 2010 21:47:05 +0100 Subject: [PATCH 14/31] add first usage of TestKit - rename expect... methods to expectMsg... to avoid clash with scalatest - add access to testActor, e.g. for registration of that actor with others - add facility for ignoring messages in testActor, which would only clutter the test spec --- .../src/main/scala/akka/util/TestKit.scala | 119 +++++++++++------- .../akka/actor/actor/FSMTimingSpec.scala | 61 +++++++++ 2 files changed, 137 insertions(+), 43 deletions(-) create mode 100644 akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala index 2a3e136756..77d3f77ca8 100644 --- a/akka-actor/src/main/scala/akka/util/TestKit.scala +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -7,22 +7,29 @@ import duration._ import java.util.concurrent.{BlockingQueue, LinkedBlockingQueue} object TestActor { + type Ignore = Option[PartialFunction[AnyRef, Boolean]] + case class SetTimeout(d : Duration) + case class SetIgnore(i : Ignore) } -class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Unit] { +class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, TestActor.Ignore] { import FSM._ import TestActor._ - startWith(0, ()) + startWith(0, None) when(0, stateTimeout = 5 seconds) { case Event(SetTimeout(d), _) => setStateTimeout(0, if (d.finite_?) d else None) stay + case Event(SetIgnore(ign), _) => stay using ign case Event(StateTimeout, _) => stop - case Event(x : AnyRef, _) => - queue offer x + case Event(x : AnyRef, ign) => + val ignore = ign map (z => if (z isDefinedAt x) z(x) else false) getOrElse false + if (!ignore) { + queue offer x + } stay } initialize @@ -31,7 +38,7 @@ class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Unit] /** * Test kit for testing actors. Inheriting from this trait enables reception of * replies from actors, which are queued by an internal actor and can be - * examined using the `expect...` methods. Assertions and bounds concerning + * examined using the `expectMsg...` methods. Assertions and bounds concerning * timing are available in the form of `within` blocks. * *
@@ -40,8 +47,8 @@ class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Unit]
  *
  *     within (1 second) {
  *         test ! SomeWork
- *         expect(Result1) // bounded to 1 second
- *         expect(Result2) // bounded to the remainder of the 1 second
+ *         expectMsg(Result1) // bounded to 1 second
+ *         expectMsg(Result2) // bounded to the remainder of the 1 second
  *     }
  * }
  * 
@@ -54,14 +61,29 @@ class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, Unit] * - this trait is not thread-safe (only one actor with one queue, one stack * of `within` blocks); it is expected that the code is executed from a * constructor as shown above, which makes this a non-issue, otherwise take - * care not to run tests within a single test class in parallel. + * care not to run tests within a single test class instance in parallel. + * + * TODO: add receiveWhile for receiving a series of messages (single timeout, + * overall maximum time, partial function for selection, ...) + * + * @author Roland Kuhn + * @since 1.1 */ trait TestKit { private val queue = new LinkedBlockingQueue[AnyRef]() - private val sender = actorOf(new TestActor(queue)).start - protected implicit val senderOption = Some(sender) + /** + * ActorRef of the test actor. Access is provided to enable e.g. + * registration as message target. + */ + protected val testActor = actorOf(new TestActor(queue)).start + + /** + * Implicit sender reference so that replies are possible for messages sent + * from the test class. + */ + protected implicit val senderOption = Some(testActor) private var end : Duration = Duration.Inf @@ -69,7 +91,7 @@ trait TestKit { * Stop test actor. Should be done at the end of the test unless relying on * test actor timeout. */ - def stopTestActor { sender.stop } + def stopTestActor { testActor.stop } /** * Set test actor timeout. By default, the test actor shuts itself down @@ -83,7 +105,18 @@ trait TestKit { * } finally { stopTestActor } * */ - def setTestActorTimeout(d : Duration) { sender ! TestActor.SetTimeout(d) } + def setTestActorTimeout(d : Duration) { testActor ! TestActor.SetTimeout(d) } + + /** + * Ignore all messages in the test actor for which the given partial + * function returns true. + */ + def ignoreMsg(f : PartialFunction[AnyRef, Boolean]) { testActor ! TestActor.SetIgnore(Some(f)) } + + /** + * Stop ignoring messages in the test actor. + */ + def ignoreNoMsg { testActor ! TestActor.SetIgnore(None) } /** * Obtain current time (`System.currentTimeMillis`) as Duration. @@ -104,7 +137,7 @@ trait TestKit { *
      * val ret = within(50 millis) {
      *             test ! "ping"
-     *             expectClass(classOf[String])
+     *             expectMsgClass(classOf[String])
      *           }
      * 
*/ @@ -133,10 +166,10 @@ trait TestKit { def within[T](max : Duration)(f : => T) : T = within(0 seconds, max)(f) /** - * Same as `expect`, but takes the maximum wait time from the innermost + * Same as `expectMsg`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expect(obj : Any) : AnyRef = expect(remaining, obj) + def expectMsg(obj : Any) : AnyRef = expectMsg(remaining, obj) /** * Receive one message from the test actor and assert that it equals the @@ -145,18 +178,18 @@ trait TestKit { * * @return the received object */ - def expect(max : Duration, obj : Any) : AnyRef = { + def expectMsg(max : Duration, obj : Any) : AnyRef = { val o = receiveOne(max) - assert (o ne null, "timeout during expect") + assert (o ne null, "timeout during expectMsg") assert (obj == o, "expected "+obj+", found "+o) o } /** - * Same as `expect`, but takes the maximum wait time from the innermost + * Same as `expectMsg`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expect[T](f : PartialFunction[Any, T]) : T = expect(remaining)(f) + def expectMsg[T](f : PartialFunction[Any, T]) : T = expectMsg(remaining)(f) /** * Receive one message from the test actor and assert that the given @@ -168,18 +201,18 @@ trait TestKit { * * @return the received object as transformed by the partial function */ - def expect[T](max : Duration)(f : PartialFunction[Any, T]) : T = { + def expectMsg[T](max : Duration)(f : PartialFunction[Any, T]) : T = { val o = receiveOne(max) - assert (o ne null, "timeout during expect") + assert (o ne null, "timeout during expectMsg") assert (f.isDefinedAt(o), "does not match: "+o) f(o) } /** - * Same as `expectClass`, but takes the maximum wait time from the innermost + * Same as `expectMsgClass`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expectClass[C](c : Class[C]) : C = expectClass(remaining, c) + def expectMsgClass[C](c : Class[C]) : C = expectMsgClass(remaining, c) /** * Receive one message from the test actor and assert that it conforms to @@ -188,18 +221,18 @@ trait TestKit { * * @return the received object */ - def expectClass[C](max : Duration, c : Class[C]) : C = { + def expectMsgClass[C](max : Duration, c : Class[C]) : C = { val o = receiveOne(max) - assert (o ne null, "timeout during expectClass") + assert (o ne null, "timeout during expectMsgClass") assert (c isInstance o, "expected "+c+", found "+o.getClass) o.asInstanceOf[C] } /** - * Same as `expectAnyOf`, but takes the maximum wait time from the innermost + * Same as `expectMsgAnyOf`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expectAnyOf(obj : Any*) : AnyRef = expectAnyOf(remaining, obj : _*) + def expectMsgAnyOf(obj : Any*) : AnyRef = expectMsgAnyOf(remaining, obj : _*) /** * Receive one message from the test actor and assert that it equals one of @@ -208,18 +241,18 @@ trait TestKit { * * @return the received object */ - def expectAnyOf(max : Duration, obj : Any*) : AnyRef = { + def expectMsgAnyOf(max : Duration, obj : Any*) : AnyRef = { val o = receiveOne(max) - assert (o ne null, "timeout during expectAnyOf") + assert (o ne null, "timeout during expectMsgAnyOf") assert (obj exists (_ == o), "found unexpected "+o) o } /** - * Same as `expectAnyClassOf`, but takes the maximum wait time from the innermost + * Same as `expectMsgAnyClassOf`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expectAnyClassOf(obj : Class[_]*) : AnyRef = expectAnyClassOf(remaining, obj : _*) + def expectMsgAnyClassOf(obj : Class[_]*) : AnyRef = expectMsgAnyClassOf(remaining, obj : _*) /** * Receive one message from the test actor and assert that it conforms to @@ -228,18 +261,18 @@ trait TestKit { * * @return the received object */ - def expectAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { + def expectMsgAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { val o = receiveOne(max) - assert (o ne null, "timeout during expectAnyClassOf") + assert (o ne null, "timeout during expectMsgAnyClassOf") assert (obj exists (_ isInstance o), "found unexpected "+o) o } /** - * Same as `expectAllOf`, but takes the maximum wait time from the innermost + * Same as `expectMsgAllOf`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expectAllOf(obj : Any*) { expectAllOf(remaining, obj : _*) } + def expectMsgAllOf(obj : Any*) { expectMsgAllOf(remaining, obj : _*) } /** * Receive a number of messages from the test actor matching the given @@ -252,20 +285,20 @@ trait TestKit { * within(1 second) { * dispatcher ! SomeWork1() * dispatcher ! SomeWork2() - * expectAllOf(Result1(), Result2()) + * expectMsgAllOf(Result1(), Result2()) * } * */ - def expectAllOf(max : Duration, obj : Any*) { + def expectMsgAllOf(max : Duration, obj : Any*) { val recv = receiveN(obj.size, now + max) assert (obj forall (x => recv exists (x == _)), "not found all") } /** - * Same as `expectAllClassOf`, but takes the maximum wait time from the innermost + * Same as `expectMsgAllClassOf`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expectAllClassOf(obj : Class[_]*) { expectAllClassOf(remaining, obj : _*) } + def expectMsgAllClassOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } /** * Receive a number of messages from the test actor matching the given @@ -275,16 +308,16 @@ trait TestKit { * Wait time is bounded by the given duration, with an AssertionFailure * being thrown in case of timeout. */ - def expectAllClassOf(max : Duration, obj : Class[_]*) { + def expectMsgAllClassOf(max : Duration, obj : Class[_]*) { val recv = receiveN(obj.size, now + max) assert (obj forall (x => recv exists (_.getClass eq x)), "not found all") } /** - * Same as `expectAllConformingOf`, but takes the maximum wait time from the innermost + * Same as `expectMsgAllConformingOf`, but takes the maximum wait time from the innermost * enclosing `within` block. */ - def expectAllConformingOf(obj : Class[_]*) { expectAllClassOf(remaining, obj : _*) } + def expectMsgAllConformingOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } /** * Receive a number of messages from the test actor matching the given @@ -297,7 +330,7 @@ trait TestKit { * Beware that one object may satisfy all given class constraints, which * may be counter-intuitive. */ - def expectAllConformingOf(max : Duration, obj : Class[_]*) { + def expectMsgAllConformingOf(max : Duration, obj : Class[_]*) { val recv = receiveN(obj.size, now + max) assert (obj forall (x => recv exists (x isInstance _)), "not found all") } diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala new file mode 100644 index 0000000000..d02c343fe7 --- /dev/null +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -0,0 +1,61 @@ +package akka.actor + +import akka.util.TestKit +import akka.util.duration._ + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith + +@RunWith(classOf[JUnitRunner]) +class FSMTimingSpec + extends Spec + with ShouldMatchers + with TestKit { + + import FSMTimingSpec._ + import FSM._ + + val fsm = Actor.actorOf[StateMachine].start + fsm ! SubscribeTransitionCallBack(testActor) + expectMsg(Initial) + + ignoreMsg { + case Transition(Initial, _) => true + } + + describe("A Finite State Machine") { + + it("should receive StateTimeout") { + within (50 millis, 150 millis) { + fsm ! TestStateTimeout + expectMsg(Transition(TestStateTimeout, Initial)) + } + } + + } + +} + +object FSMTimingSpec { + + trait State + case object Initial extends State + case object TestStateTimeout extends State + + class StateMachine extends Actor with FSM[State, Unit] { + import FSM._ + + startWith(Initial, ()) + when(Initial) { + case Event(TestStateTimeout, _) => goto(TestStateTimeout) + } + when(TestStateTimeout, stateTimeout = 100 millis) { + case Event(StateTimeout, _) => goto(Initial) + } + } + +} + +// vim: set ts=4 sw=4 et: From a45fc955e97da46ab5bfee74797a277750a4ca83 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Thu, 30 Dec 2010 22:43:24 +0100 Subject: [PATCH 15/31] flesh out FSMTimingSpec - add expectNoMsg and receiveWhile to TestKit - add Ev(...) convenience extractor for FSM.Event - switch to System.nanoTime for TestKit --- .../src/main/scala/akka/actor/FSM.scala | 8 +- .../src/main/scala/akka/util/TestKit.scala | 104 +++++++++++++++--- .../akka/actor/actor/FSMTimingSpec.scala | 70 +++++++++++- 3 files changed, 162 insertions(+), 20 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 065fb9d399..66d5635f7a 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -11,6 +11,9 @@ import java.util.concurrent.{ScheduledFuture, TimeUnit} object FSM { case class Event[D](event: Any, stateData: D) + object Ev { + def unapply[D](e : Event[D]) : Option[Any] = Some(e.event) + } case class Transition[S](from: S, to: S) case class SubscribeTransitionCallBack(actorRef: ActorRef) @@ -67,7 +70,10 @@ object FSM { * import A._ * * startWith(One, Data(42)) - * when(One) { [some partial function] } + * when(One) { + * case Event(SomeMsg, Data(x)) => ... + * case Ev(SomeMsg) => ... // convenience when data not needed + * } * when(Two, stateTimeout = 5 seconds) { ... } * initialize * } diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala index 77d3f77ca8..8822bbfb75 100644 --- a/akka-actor/src/main/scala/akka/util/TestKit.scala +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -4,7 +4,9 @@ import akka.actor.{Actor, FSM} import Actor._ import duration._ -import java.util.concurrent.{BlockingQueue, LinkedBlockingQueue} +import java.util.concurrent.{BlockingDeque, LinkedBlockingDeque} + +import scala.annotation.tailrec object TestActor { type Ignore = Option[PartialFunction[AnyRef, Boolean]] @@ -13,7 +15,7 @@ object TestActor { case class SetIgnore(i : Ignore) } -class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, TestActor.Ignore] { +class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestActor.Ignore] { import FSM._ import TestActor._ @@ -28,7 +30,7 @@ class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, TestA case Event(x : AnyRef, ign) => val ignore = ign map (z => if (z isDefinedAt x) z(x) else false) getOrElse false if (!ignore) { - queue offer x + queue.offerLast(x) } stay } @@ -63,15 +65,12 @@ class TestActor(queue : BlockingQueue[AnyRef]) extends Actor with FSM[Int, TestA * constructor as shown above, which makes this a non-issue, otherwise take * care not to run tests within a single test class instance in parallel. * - * TODO: add receiveWhile for receiving a series of messages (single timeout, - * overall maximum time, partial function for selection, ...) - * * @author Roland Kuhn * @since 1.1 */ trait TestKit { - private val queue = new LinkedBlockingQueue[AnyRef]() + private val queue = new LinkedBlockingDeque[AnyRef]() /** * ActorRef of the test actor. Access is provided to enable e.g. @@ -86,6 +85,13 @@ trait TestKit { protected implicit val senderOption = Some(testActor) private var end : Duration = Duration.Inf + /* + * THIS IS A HACK: expectNoMsg and receiveWhile are bounded by `end`, but + * running them should not trigger an AssertionError, so mark their end + * time here and do not fail at the end of `within` if that time is not + * long gone. + */ + private var lastSoftTimeout : Duration = now - 5.millis /** * Stop test actor. Should be done at the end of the test unless relying on @@ -121,7 +127,7 @@ trait TestKit { /** * Obtain current time (`System.currentTimeMillis`) as Duration. */ - def now : Duration = System.currentTimeMillis.millis + def now : Duration = System.nanoTime.nanos /** * Obtain time remaining for execution of the innermost enclosing `within` block. @@ -154,7 +160,14 @@ trait TestKit { val diff = now - start assert (min <= diff, "block took "+diff+", should at least have been "+min) - assert (diff <= max_diff, "block took "+diff+", exceeding "+max_diff) + /* + * caution: HACK AHEAD + */ + if (now - lastSoftTimeout > 5.millis) { + assert (diff <= max_diff, "block took "+diff+", exceeding "+max_diff) + } else { + lastSoftTimeout -= 5.millis + } end = prev_end ret @@ -335,7 +348,68 @@ trait TestKit { assert (obj forall (x => recv exists (x isInstance _)), "not found all") } - private def receiveN(n : Int, stop : Duration) = { + /** + * Same as `expectNoMsg`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectNoMsg { expectNoMsg(remaining) } + + /** + * Assert that no message is received for the specified time. + */ + def expectNoMsg(max : Duration) { + val o = receiveOne(max) + assert (o eq null, "received unexpected message "+o) + lastSoftTimeout = now + } + + /** + * Same as `receiveWhile`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def receiveWhile[T](f : PartialFunction[AnyRef, T]) : Seq[T] = receiveWhile(remaining)(f) + + /** + * Receive a series of messages as long as the given partial function + * accepts them or the idle timeout is met or the overall maximum duration + * is elapsed. Returns the sequence of messages. + * + * Beware that the maximum duration is not implicitly bounded by or taken + * from the innermost enclosing `within` block, as it is not an error to + * hit the `max` duration in this case. + * + * One possible use of this method is for testing whether messages of + * certain characteristics are generated at a certain rate: + * + *
+     * test ! ScheduleTicks(100 millis)
+     * val series = receiveWhile(750 millis) {
+     *     case Tick(count) => count
+     * }
+     * assert(series == (1 to 7).toList)
+     * 
+ */ + def receiveWhile[T](max : Duration)(f : PartialFunction[AnyRef, T]) : Seq[T] = { + val stop = now + max + + @tailrec def doit(acc : List[T]) : List[T] = { + receiveOne(stop - now) match { + case null => + acc.reverse + case o if (f isDefinedAt o) => + doit(f(o) :: acc) + case o => + queue.offerFirst(o) + acc.reverse + } + } + + val ret = doit(Nil) + lastSoftTimeout = now + ret + } + + private def receiveN(n : Int, stop : Duration) : Seq[AnyRef] = { for { x <- 1 to n } yield { val timeout = stop - now val o = receiveOne(timeout) @@ -344,11 +418,13 @@ trait TestKit { } } - private def receiveOne(max : Duration) = { - if (max.finite_?) { - queue.poll(max.length, max.unit) + private def receiveOne(max : Duration) : AnyRef = { + if (max == 0.seconds) { + queue.pollFirst + } else if (max.finite_?) { + queue.pollFirst(max.length, max.unit) } else { - queue.take + queue.takeFirst } } } diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala index d02c343fe7..cde157ec90 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -17,9 +17,9 @@ class FSMTimingSpec import FSMTimingSpec._ import FSM._ - val fsm = Actor.actorOf[StateMachine].start + val fsm = Actor.actorOf(new StateMachine(testActor)).start fsm ! SubscribeTransitionCallBack(testActor) - expectMsg(Initial) + expectMsg(50 millis, Initial) ignoreMsg { case Transition(Initial, _) => true @@ -31,9 +31,37 @@ class FSMTimingSpec within (50 millis, 150 millis) { fsm ! TestStateTimeout expectMsg(Transition(TestStateTimeout, Initial)) + expectNoMsg } } + it("should receive single-shot timer") { + within (50 millis, 150 millis) { + fsm ! TestSingleTimer + expectMsg(Tick) + expectMsg(Transition(TestSingleTimer, Initial)) + expectNoMsg + } + } + + it("should receive and cancel a repeated timer") { + fsm ! TestRepeatedTimer + val seq = receiveWhile(550 millis) { + case Tick => Tick + } + seq should have length (5) + within(250 millis) { + fsm ! Cancel + expectMsg(Transition(TestRepeatedTimer, Initial)) + expectNoMsg + } + } + + it("should notify unhandled messages") { + fsm ! Cancel + expectMsg(Unhandled(Cancel)) + } + } } @@ -43,16 +71,48 @@ object FSMTimingSpec { trait State case object Initial extends State case object TestStateTimeout extends State + case object TestSingleTimer extends State + case object TestRepeatedTimer extends State - class StateMachine extends Actor with FSM[State, Unit] { + case object Tick + case object Cancel + + case class Unhandled(msg : AnyRef) + + class StateMachine(tester : ActorRef) extends Actor with FSM[State, Unit] { import FSM._ startWith(Initial, ()) when(Initial) { - case Event(TestStateTimeout, _) => goto(TestStateTimeout) + case Ev(TestStateTimeout) => goto(TestStateTimeout) + case Ev(TestSingleTimer) => + setTimer("tester", Tick, 100 millis, false) + goto(TestSingleTimer) + case Ev(TestRepeatedTimer) => + setTimer("tester", Tick, 100 millis, true) + goto(TestRepeatedTimer) } when(TestStateTimeout, stateTimeout = 100 millis) { - case Event(StateTimeout, _) => goto(Initial) + case Ev(StateTimeout) => goto(Initial) + } + when(TestSingleTimer) { + case Ev(Tick) => + tester ! Tick + goto(Initial) + } + when(TestRepeatedTimer) { + case Ev(Tick) => + tester ! Tick + stay + case Ev(Cancel) => + cancelTimer("tester") + goto(Initial) + } + + whenUnhandled { + case Ev(msg : AnyRef) => + tester ! Unhandled(msg) + stay } } From da03c0552a0fb5cc83692729bd12a0675e8373bb Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Fri, 31 Dec 2010 15:53:32 +0100 Subject: [PATCH 16/31] remove unnecessary allocations in hot paths --- .../src/main/scala/akka/actor/FSM.scala | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 66d5635f7a..10a0944147 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -40,8 +40,9 @@ object FSM { } def cancel { - ref = ref flatMap { - t => t.cancel(true); None + if (ref.isDefined) { + ref.get.cancel(true) + ref = None } } } @@ -259,7 +260,7 @@ trait FSM[S, D] { * Set handler which is called upon reception of unhandled messages. */ protected final def whenUnhandled(stateFunction: StateFunction) = { - handleEvent = stateFunction + handleEvent = stateFunction orElse handleEventDefault } /** @@ -292,7 +293,8 @@ trait FSM[S, D] { } } - private var handleEvent: StateFunction = { + private var handleEvent: StateFunction = handleEventDefault + private val handleEventDefault: StateFunction = { case Event(value, stateData) => log.slf4j.warn("Event {} not handled in state {}, staying at current state", value, currentState.stateName) stay @@ -327,8 +329,9 @@ trait FSM[S, D] { case UnsubscribeTransitionCallBack(actorRef) => transitionCallBackList = transitionCallBackList.filterNot(_ == actorRef) case value => { - timeoutFuture = timeoutFuture.flatMap{ - ref => ref.cancel(true); None + if (timeoutFuture.isDefined) { + timeoutFuture.get.cancel(true) + timeoutFuture = None } generation += 1 processEvent(value) @@ -337,7 +340,13 @@ trait FSM[S, D] { private def processEvent(value: Any) = { val event = Event(value, currentState.stateData) - val nextState = (stateFunctions(currentState.stateName) orElse handleEvent).apply(event) + val stateFunc = stateFunctions(currentState.stateName) + val nextState = if (stateFunc isDefinedAt event) { + stateFunc(event) + } else { + // handleEventDefault ensures that this is always defined + handleEvent(event) + } nextState.stopReason match { case Some(reason) => terminate(reason) case None => makeTransition(nextState) @@ -359,11 +368,12 @@ trait FSM[S, D] { private def applyState(nextState: State) = { currentState = nextState - currentState.timeout orElse stateTimeouts(currentState.stateName) foreach { - t => - if (t.length >= 0) { - timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t.length, t.unit)) - } + val timeout = currentState.timeout orElse stateTimeouts(currentState.stateName) + if (timeout.isDefined) { + val t = timeout.get + if (t.length >= 0) { + timeoutFuture = Some(Scheduler.scheduleOnce(self, TimeoutMarker(generation), t.length, t.unit)) + } } } From 6a672747b9bd0958960661bf487ed002faffd9dd Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Fri, 31 Dec 2010 17:57:08 +0100 Subject: [PATCH 17/31] make TestKit assertions nicer / improve Duration --- .../src/main/scala/akka/util/Duration.scala | 171 +++++++++++------- .../src/main/scala/akka/util/TestKit.scala | 16 +- 2 files changed, 115 insertions(+), 72 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/Duration.scala b/akka-actor/src/main/scala/akka/util/Duration.scala index d279249b30..3a727d2531 100644 --- a/akka-actor/src/main/scala/akka/util/Duration.scala +++ b/akka-actor/src/main/scala/akka/util/Duration.scala @@ -10,9 +10,29 @@ import java.lang.{Long => JLong, Double => JDouble} object Duration { def apply(length: Long, unit: TimeUnit) : Duration = new FiniteDuration(length, unit) - def apply(length: Double, unit: TimeUnit) : Duration = new FiniteDuration(length, unit) + def apply(length: Double, unit: TimeUnit) : Duration = fromNanos(unit.toNanos(1) * length) def apply(length: Long, unit: String) : Duration = new FiniteDuration(length, timeUnit(unit)) + def fromNanos(nanos : Long) : Duration = { + if (nanos % 86400000000000L == 0) { + Duration(nanos / 86400000000000L, DAYS) + } else if (nanos % 3600000000000L == 0) { + Duration(nanos / 3600000000000L, HOURS) + } else if (nanos % 60000000000L == 0) { + Duration(nanos / 60000000000L, MINUTES) + } else if (nanos % 1000000000L == 0) { + Duration(nanos / 1000000000L, SECONDS) + } else if (nanos % 1000000L == 0) { + Duration(nanos / 1000000L, MILLISECONDS) + } else if (nanos % 1000L == 0) { + Duration(nanos / 1000L, MICROSECONDS) + } else { + Duration(nanos, NANOSECONDS) + } + } + + def fromNanos(nanos : Double) : Duration = fromNanos((nanos + 0.5).asInstanceOf[Long]) + /** * Construct a Duration by parsing a String. In case of a format error, a * RuntimeException is thrown. See `unapply(String)` for more information. @@ -32,6 +52,7 @@ object Duration { private val RE = ("""^\s*(\d+(?:\.\d+)?)\s*"""+ // length part "(?:"+ // units are distinguished in separate match groups + "(d|day|days)|"+ "(h|hour|hours)|"+ "(min|minute|minutes)|"+ "(s|sec|second|seconds)|"+ @@ -48,13 +69,14 @@ object Duration { * designated by `"Inf"` and `"-Inf"` or `"MinusInf"`. */ def unapply(s : String) : Option[Duration] = s match { - case RE(length, h, m, s, ms, mus, ns) => - if (h ne null) Some(Duration(3600 * JDouble.parseDouble(length), SECONDS)) else - if (m ne null) Some(Duration(60 * JDouble.parseDouble(length), SECONDS)) else - if (s ne null) Some(Duration(1 * JDouble.parseDouble(length), SECONDS)) else - if (ms ne null) Some(Duration(1 * JDouble.parseDouble(length), MILLISECONDS)) else - if (mus ne null) Some(Duration(1 * JDouble.parseDouble(length), MICROSECONDS)) else - if (ns ne null) Some(Duration(1 * JDouble.parseDouble(length), NANOSECONDS)) else + case RE(length, d, h, m, s, ms, mus, ns) => + if ( d ne null) Some(Duration(JDouble.parseDouble(length), DAYS)) else + if ( h ne null) Some(Duration(JDouble.parseDouble(length), HOURS)) else + if ( m ne null) Some(Duration(JDouble.parseDouble(length), MINUTES)) else + if ( s ne null) Some(Duration(JDouble.parseDouble(length), SECONDS)) else + if ( ms ne null) Some(Duration(JDouble.parseDouble(length), MILLISECONDS)) else + if (mus ne null) Some(Duration(JDouble.parseDouble(length), MICROSECONDS)) else + if ( ns ne null) Some(Duration(JDouble.parseDouble(length), NANOSECONDS)) else error("made some error in regex (should not be possible)") case REinf() => Some(Inf) case REminf() => Some(MinusInf) @@ -74,35 +96,41 @@ object Duration { trait Infinite { this : Duration => - override def equals(other : Any) = false + override def equals(other : Any) = false - def +(other : Duration) : Duration = this - def -(other : Duration) : Duration = this - def *(other : Double) : Duration = this - def /(other : Double) : Duration = this - - def finite_? = false - - def length : Long = throw new IllegalArgumentException("length not allowed on infinite Durations") - def unit : TimeUnit = throw new IllegalArgumentException("unit not allowed on infinite Durations") - def toNanos : Long = throw new IllegalArgumentException("toNanos not allowed on infinite Durations") - def toMicros : Long = throw new IllegalArgumentException("toMicros not allowed on infinite Durations") - def toMillis : Long = throw new IllegalArgumentException("toMillis not allowed on infinite Durations") - def toSeconds : Long = throw new IllegalArgumentException("toSeconds not allowed on infinite Durations") + def +(other : Duration) : Duration = this + def -(other : Duration) : Duration = this + def *(other : Double) : Duration = this + def /(other : Double) : Duration = this + + def finite_? = false + + def length : Long = throw new IllegalArgumentException("length not allowed on infinite Durations") + def unit : TimeUnit = throw new IllegalArgumentException("unit not allowed on infinite Durations") + def toNanos : Long = throw new IllegalArgumentException("toNanos not allowed on infinite Durations") + def toMicros : Long = throw new IllegalArgumentException("toMicros not allowed on infinite Durations") + def toMillis : Long = throw new IllegalArgumentException("toMillis not allowed on infinite Durations") + def toSeconds : Long = throw new IllegalArgumentException("toSeconds not allowed on infinite Durations") + def toMinutes : Long = throw new IllegalArgumentException("toMinutes not allowed on infinite Durations") + def toHours : Long = throw new IllegalArgumentException("toHours not allowed on infinite Durations") + def toDays : Long = throw new IllegalArgumentException("toDays not allowed on infinite Durations") + def toUnit(unit : TimeUnit) : Double = throw new IllegalArgumentException("toUnit not allowed on infinite Durations") + + def printHMS = toString } /** * Infinite duration: greater than any other and not equal to any other, * including itself. */ - object Inf extends Duration with Infinite { - override def toString = "Duration.Inf" - def >(other : Duration) = true - def >=(other : Duration) = true - def <(other : Duration) = false - def <=(other : Duration) = false - def unary_- : Duration = MinusInf - } + object Inf extends Duration with Infinite { + override def toString = "Duration.Inf" + def >(other : Duration) = true + def >=(other : Duration) = true + def <(other : Duration) = false + def <=(other : Duration) = false + def unary_- : Duration = MinusInf + } /** * Infinite negative duration: lesser than any other and not equal to any other, @@ -110,11 +138,11 @@ object Duration { */ object MinusInf extends Duration with Infinite { override def toString = "Duration.MinusInf" - def >(other : Duration) = false - def >=(other : Duration) = false - def <(other : Duration) = true - def <=(other : Duration) = true - def unary_- : Duration = Inf + def >(other : Duration) = false + def >=(other : Duration) = false + def <(other : Duration) = true + def <=(other : Duration) = true + def unary_- : Duration = Inf } } @@ -171,6 +199,11 @@ trait Duration { def toMicros : Long def toMillis : Long def toSeconds : Long + def toMinutes : Long + def toHours : Long + def toDays : Long + def toUnit(unit : TimeUnit) : Double + def printHMS : String def <(other : Duration) : Boolean def <=(other : Duration) : Boolean def >(other : Duration) : Boolean @@ -184,18 +217,26 @@ trait Duration { } class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { + import Duration._ + def this(length: Long, unit: String) = this(length, Duration.timeUnit(unit)) - def this(length: Double, unit: TimeUnit) = { - this(1, unit) - this * length - } def toNanos = unit.toNanos(length) def toMicros = unit.toMicros(length) def toMillis = unit.toMillis(length) def toSeconds = unit.toSeconds(length) + def toMinutes = unit.toMinutes(length) + def toHours = unit.toHours(length) + def toDays = unit.toDays(length) + def toUnit(u : TimeUnit) = long2double(toNanos) / NANOSECONDS.convert(1, u) override def toString = this match { + case Duration(1, DAYS) => "1 day" + case Duration(x, DAYS) => x+" days" + case Duration(1, HOURS) => "1 hour" + case Duration(x, HOURS) => x+" hours" + case Duration(1, MINUTES) => "1 minute" + case Duration(x, MINUTES) => x+" minutes" case Duration(1, SECONDS) => "1 second" case Duration(x, SECONDS) => x+" seconds" case Duration(1, MILLISECONDS) => "1 millisecond" @@ -206,6 +247,8 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { case Duration(x, NANOSECONDS) => x+" nanoseconds" } + def printHMS = "%02d:%02d:%06.3f".format(toHours, toMinutes % 60, toMillis / 1000. % 60) + def <(other : Duration) = { if (other.finite_?) { toNanos < other.asInstanceOf[FiniteDuration].toNanos @@ -238,20 +281,6 @@ class FiniteDuration(val length: Long, val unit: TimeUnit) extends Duration { } } - private def fromNanos(nanos : Long) : Duration = { - if (nanos % 1000000000L == 0) { - Duration(nanos / 1000000000L, SECONDS) - } else if (nanos % 1000000L == 0) { - Duration(nanos / 1000000L, MILLISECONDS) - } else if (nanos % 1000L == 0) { - Duration(nanos / 1000L, MICROSECONDS) - } else { - Duration(nanos, NANOSECONDS) - } - } - - private def fromNanos(nanos : Double) : Duration = fromNanos((nanos + 0.5).asInstanceOf[Long]) - def +(other : Duration) = { if (!other.finite_?) { other @@ -300,6 +329,9 @@ package object duration { implicit def longMult(l : Long) = new { def *(d : Duration) = d * l } + implicit def doubleMult(f : Double) = new { + def *(d : Duration) = d * f + } } class DurationInt(n: Int) { @@ -321,11 +353,14 @@ class DurationInt(n: Int) { def seconds = Duration(n, SECONDS) def second = Duration(n, SECONDS) - def minutes = Duration(60 * n, SECONDS) - def minute = Duration(60 * n, SECONDS) + def minutes = Duration(n, MINUTES) + def minute = Duration(n, MINUTES) - def hours = Duration(3600 * n, SECONDS) - def hour = Duration(3600 * n, SECONDS) + def hours = Duration(n, HOURS) + def hour = Duration(n, HOURS) + + def days = Duration(n, DAYS) + def day = Duration(n, DAYS) } class DurationLong(n: Long) { @@ -347,11 +382,14 @@ class DurationLong(n: Long) { def seconds = Duration(n, SECONDS) def second = Duration(n, SECONDS) - def minutes = Duration(60 * n, SECONDS) - def minute = Duration(60 * n, SECONDS) + def minutes = Duration(n, MINUTES) + def minute = Duration(n, MINUTES) - def hours = Duration(3600 * n, SECONDS) - def hour = Duration(3600 * n, SECONDS) + def hours = Duration(n, HOURS) + def hour = Duration(n, HOURS) + + def days = Duration(n, DAYS) + def day = Duration(n, DAYS) } class DurationDouble(d: Double) { @@ -373,9 +411,12 @@ class DurationDouble(d: Double) { def seconds = Duration(d, SECONDS) def second = Duration(d, SECONDS) - def minutes = Duration(60 * d, SECONDS) - def minute = Duration(60 * d, SECONDS) + def minutes = Duration(d, MINUTES) + def minute = Duration(d, MINUTES) - def hours = Duration(3600 * d, SECONDS) - def hour = Duration(3600 * d, SECONDS) + def hours = Duration(d, HOURS) + def hour = Duration(d, HOURS) + + def days = Duration(d, DAYS) + def day = Duration(d, DAYS) } diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala index 8822bbfb75..c79c58db98 100644 --- a/akka-actor/src/main/scala/akka/util/TestKit.scala +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -4,7 +4,7 @@ import akka.actor.{Actor, FSM} import Actor._ import duration._ -import java.util.concurrent.{BlockingDeque, LinkedBlockingDeque} +import java.util.concurrent.{BlockingDeque, LinkedBlockingDeque, TimeUnit} import scala.annotation.tailrec @@ -21,11 +21,11 @@ class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestA startWith(0, None) when(0, stateTimeout = 5 seconds) { - case Event(SetTimeout(d), _) => + case Ev(SetTimeout(d)) => setStateTimeout(0, if (d.finite_?) d else None) stay - case Event(SetIgnore(ign), _) => stay using ign - case Event(StateTimeout, _) => + case Ev(SetIgnore(ign)) => stay using ign + case Ev(StateTimeout) => stop case Event(x : AnyRef, ign) => val ignore = ign map (z => if (z isDefinedAt x) z(x) else false) getOrElse false @@ -150,7 +150,7 @@ trait TestKit { def within[T](min : Duration, max : Duration)(f : => T) : T = { val start = now val rem = end - start - assert (rem >= min, "required min time "+min+" not possible, only "+rem+" left") + assert (rem >= min, "required min time "+min+" not possible, only "+format(min.unit, rem)+" left") val max_diff = if (max < rem) max else rem val prev_end = end @@ -159,12 +159,12 @@ trait TestKit { val ret = f val diff = now - start - assert (min <= diff, "block took "+diff+", should at least have been "+min) + assert (min <= diff, "block took "+format(min.unit, diff)+", should at least have been "+min) /* * caution: HACK AHEAD */ if (now - lastSoftTimeout > 5.millis) { - assert (diff <= max_diff, "block took "+diff+", exceeding "+max_diff) + assert (diff <= max_diff, "block took "+format(max.unit, diff)+", exceeding "+format(max.unit, max_diff)) } else { lastSoftTimeout -= 5.millis } @@ -427,6 +427,8 @@ trait TestKit { queue.takeFirst } } + + private def format(u : TimeUnit, d : Duration) = "%.3f %s".format(d.toUnit(u), u.toString.toLowerCase) } // vim: set ts=4 sw=4 et: From 91e210ebbb010ea2f968ba52794f792c3a7d622c Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Sat, 1 Jan 2011 21:12:12 +0100 Subject: [PATCH 18/31] fix fallout of Duration changes in STM tests - document change in Java API --- .../src/main/scala/akka/util/Duration.scala | 17 ++++++++++------- .../test/UntypedCoordinatedCounter.java | 4 ++-- .../akka/transactor/test/UntypedCounter.java | 6 +++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/Duration.scala b/akka-actor/src/main/scala/akka/util/Duration.scala index 3a727d2531..743ce0fc4c 100644 --- a/akka-actor/src/main/scala/akka/util/Duration.scala +++ b/akka-actor/src/main/scala/akka/util/Duration.scala @@ -87,10 +87,13 @@ object Duration { * Parse TimeUnit from string representation. */ def timeUnit(unit: String) = unit.toLowerCase match { - case "nanoseconds" | "nanos" | "nanosecond" | "nano" => NANOSECONDS - case "microseconds" | "micros" | "microsecond" | "micro" => MICROSECONDS - case "milliseconds" | "millis" | "millisecond" | "milli" => MILLISECONDS - case _ => SECONDS + case "d" | "day" | "days" => DAYS + case "h" | "hour" | "hours" => HOURS + case "min" | "minute" | "minutes" => MINUTES + case "s" | "sec" | "second" | "seconds" => SECONDS + case "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => MILLISECONDS + case "µs" | "micro" | "micros" | "microsecond" | "microseconds" => MICROSECONDS + case "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => NANOSECONDS } trait Infinite { @@ -153,11 +156,11 @@ object Duration { *

* Examples of usage from Java: *

- * import akka.util.Duration;
+ * import akka.util.FiniteDuration;
  * import java.util.concurrent.TimeUnit;
  *
- * Duration duration = new Duration(100, MILLISECONDS);
- * Duration duration = new Duration(5, "seconds");
+ * Duration duration = new FiniteDuration(100, MILLISECONDS);
+ * Duration duration = new FiniteDuration(5, "seconds");
  *
  * duration.toNanos();
  * 
diff --git a/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedCounter.java b/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedCounter.java index b1030106de..9e36409728 100644 --- a/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedCounter.java +++ b/akka-stm/src/test/java/akka/transactor/test/UntypedCoordinatedCounter.java @@ -5,7 +5,7 @@ import akka.transactor.Atomically; import akka.actor.ActorRef; import akka.actor.UntypedActor; import akka.stm.*; -import akka.util.Duration; +import akka.util.FiniteDuration; import org.multiverse.api.StmUtils; @@ -17,7 +17,7 @@ public class UntypedCoordinatedCounter extends UntypedActor { private String name; private Ref count = new Ref(0); private TransactionFactory txFactory = new TransactionFactoryBuilder() - .setTimeout(new Duration(3, TimeUnit.SECONDS)) + .setTimeout(new FiniteDuration(3, TimeUnit.SECONDS)) .build(); public UntypedCoordinatedCounter(String name) { diff --git a/akka-stm/src/test/java/akka/transactor/test/UntypedCounter.java b/akka-stm/src/test/java/akka/transactor/test/UntypedCounter.java index d343ceea31..325b06ba73 100644 --- a/akka-stm/src/test/java/akka/transactor/test/UntypedCounter.java +++ b/akka-stm/src/test/java/akka/transactor/test/UntypedCounter.java @@ -4,7 +4,7 @@ import akka.transactor.UntypedTransactor; import akka.transactor.SendTo; import akka.actor.ActorRef; import akka.stm.*; -import akka.util.Duration; +import akka.util.FiniteDuration; import org.multiverse.api.StmUtils; @@ -23,7 +23,7 @@ public class UntypedCounter extends UntypedTransactor { @Override public TransactionFactory transactionFactory() { return new TransactionFactoryBuilder() - .setTimeout(new Duration(3, TimeUnit.SECONDS)) + .setTimeout(new FiniteDuration(3, TimeUnit.SECONDS)) .build(); } @@ -74,4 +74,4 @@ public class UntypedCounter extends UntypedTransactor { return true; } else return false; } -} \ No newline at end of file +} From 227f2bb7c9a9e304d3a0d979452e4fc3e63e8ad4 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Sat, 1 Jan 2011 21:15:10 +0100 Subject: [PATCH 19/31] convert test to WordSpec with MustMatchers --- .../akka/actor/actor/FSMTimingSpec.scala | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala index cde157ec90..3bb3353da5 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -3,15 +3,12 @@ package akka.actor import akka.util.TestKit import akka.util.duration._ -import org.scalatest.Spec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner -import org.junit.runner.RunWith +import org.scalatest.WordSpec +import org.scalatest.matchers.MustMatchers -@RunWith(classOf[JUnitRunner]) class FSMTimingSpec - extends Spec - with ShouldMatchers + extends WordSpec + with MustMatchers with TestKit { import FSMTimingSpec._ @@ -25,9 +22,9 @@ class FSMTimingSpec case Transition(Initial, _) => true } - describe("A Finite State Machine") { + "A Finite State Machine" must { - it("should receive StateTimeout") { + "receive StateTimeout" in { within (50 millis, 150 millis) { fsm ! TestStateTimeout expectMsg(Transition(TestStateTimeout, Initial)) @@ -35,7 +32,7 @@ class FSMTimingSpec } } - it("should receive single-shot timer") { + "receive single-shot timer" in { within (50 millis, 150 millis) { fsm ! TestSingleTimer expectMsg(Tick) @@ -44,12 +41,12 @@ class FSMTimingSpec } } - it("should receive and cancel a repeated timer") { + "receive and cancel a repeated timer" in { fsm ! TestRepeatedTimer val seq = receiveWhile(550 millis) { case Tick => Tick } - seq should have length (5) + seq must have length (5) within(250 millis) { fsm ! Cancel expectMsg(Transition(TestRepeatedTimer, Initial)) @@ -57,9 +54,12 @@ class FSMTimingSpec } } - it("should notify unhandled messages") { - fsm ! Cancel - expectMsg(Unhandled(Cancel)) + "notify unhandled messages" in { + within(200 millis) { + fsm ! Cancel + expectMsg(Unhandled(Cancel)) + expectNoMsg + } } } From 33a628a0d11681da80625d37dc44b8d4f08f252c Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 3 Jan 2011 11:12:40 +0100 Subject: [PATCH 20/31] stop the timers (if any) while terminating --- akka-actor/src/main/scala/akka/actor/FSM.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 10a0944147..81078f59be 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -378,6 +378,7 @@ trait FSM[S, D] { } private def terminate(reason: Reason) = { + timers.foreach{ case (timer, t) => log.slf4j.info("Canceling timer {}", timer); t.cancel} terminateEvent.apply(StopEvent(reason, currentState.stateName, currentState.stateData)) self.stop } From 9f664711c03a69d21737a9fad96dee38253457fb Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 3 Jan 2011 11:35:27 +0100 Subject: [PATCH 21/31] - make transition handler a function taking old and new state avoiding the default use of the transition class - only create transition class when transition listeners are subscribed --- akka-actor/src/main/scala/akka/actor/FSM.scala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 81078f59be..00bdf75d6e 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -6,7 +6,7 @@ package akka.actor import akka.util._ import scala.collection.mutable -import java.util.concurrent.{ScheduledFuture, TimeUnit} +import java.util.concurrent.ScheduledFuture object FSM { @@ -124,6 +124,7 @@ trait FSM[S, D] { type StateFunction = scala.PartialFunction[Event[D], State] type Timeout = Option[Duration] + type TransitionHandler = (S, S) => Unit /* DSL */ @@ -245,7 +246,7 @@ trait FSM[S, D] { * Set handler which is called upon each state transition, i.e. not when * staying in the same state. */ - protected final def onTransition(transitionHandler: PartialFunction[Transition[S], Unit]) = { + protected final def onTransition(transitionHandler: TransitionHandler) = { transitionEvent = transitionHandler } @@ -306,8 +307,8 @@ trait FSM[S, D] { case StopEvent(reason, _, _) => log.slf4j.info("Stopping because of reason: {}", reason) } - private var transitionEvent: PartialFunction[Transition[S], Unit] = { - case Transition(from, to) => log.slf4j.debug("Transitioning from state {} to {}", from, to) + private var transitionEvent: TransitionHandler = (from, to) => { + log.slf4j.debug("Transitioning from state {} to {}", from, to) } override final protected def receive: Receive = { @@ -358,9 +359,11 @@ trait FSM[S, D] { terminate(Failure("Next state %s does not exist".format(nextState.stateName))) } else { if (currentState.stateName != nextState.stateName) { - val transition = Transition(currentState.stateName, nextState.stateName) - transitionEvent.apply(transition) - transitionCallBackList.foreach(_ ! transition) + transitionEvent.apply(currentState.stateName, nextState.stateName) + if (!transitionCallBackList.isEmpty) { + val transition = Transition(currentState.stateName, nextState.stateName) + transitionCallBackList.foreach(_ ! transition) + } } applyState(nextState) } From 80ac75ac38a041bc0bdc07b5423d2ad9011544a7 Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 3 Jan 2011 11:40:04 +0100 Subject: [PATCH 22/31] fix tests --- akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala index 397517e05e..f9dc185597 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala @@ -67,10 +67,10 @@ object FSMActorSpec { } } - onTransition { + onTransition((oldState, newState) => Transition(oldState, newState) match { case Transition(Locked, Open) => transitionLatch.open case Transition(_, _) => () - } + }) onTermination { case StopEvent(Shutdown, Locked, _) => From 61b4ded1bda00cbd23666898b3f6c1ddd7661736 Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 3 Jan 2011 12:03:38 +0100 Subject: [PATCH 23/31] Removed generic typed classes that are only used in the FSM itself from the companion object and put back in the typed FSM again so they will take same types. --- akka-actor/src/main/scala/akka/actor/FSM.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 00bdf75d6e..f25d4ea164 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -10,11 +10,6 @@ import java.util.concurrent.ScheduledFuture object FSM { - case class Event[D](event: Any, stateData: D) - object Ev { - def unapply[D](e : Event[D]) : Option[Any] = Some(e.event) - } - case class Transition[S](from: S, to: S) case class SubscribeTransitionCallBack(actorRef: ActorRef) case class UnsubscribeTransitionCallBack(actorRef: ActorRef) @@ -23,7 +18,6 @@ object FSM { case object Normal extends Reason case object Shutdown extends Reason case class Failure(cause: Any) extends Reason - case class StopEvent[S, D](reason: Reason, currentState: S, stateData: D) case object StateTimeout case class TimeoutMarker(generation: Long) @@ -386,6 +380,10 @@ trait FSM[S, D] { self.stop } + case class Event[D](event: Any, stateData: D) + object Ev { + def unapply[D](e : Event[D]) : Option[Any] = Some(e.event) + } case class State(stateName: S, stateData: D, timeout: Timeout = None) { @@ -427,4 +425,5 @@ trait FSM[S, D] { } } + case class StopEvent[S, D](reason: Reason, currentState: S, stateData: D) } From 817395f4543bede39ca78f9ea0e17f95ed15d408 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Mon, 3 Jan 2011 18:50:42 +0100 Subject: [PATCH 24/31] change indentation to 2 spaces --- .../src/main/scala/akka/util/TestKit.scala | 738 +++++++++--------- .../akka/actor/actor/FSMTimingSpec.scala | 184 ++--- .../src/main/scala/Buncher.scala | 104 +-- 3 files changed, 513 insertions(+), 513 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala index c79c58db98..bb400ff992 100644 --- a/akka-actor/src/main/scala/akka/util/TestKit.scala +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -9,32 +9,32 @@ import java.util.concurrent.{BlockingDeque, LinkedBlockingDeque, TimeUnit} import scala.annotation.tailrec object TestActor { - type Ignore = Option[PartialFunction[AnyRef, Boolean]] + type Ignore = Option[PartialFunction[AnyRef, Boolean]] - case class SetTimeout(d : Duration) - case class SetIgnore(i : Ignore) + case class SetTimeout(d : Duration) + case class SetIgnore(i : Ignore) } class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestActor.Ignore] { - import FSM._ - import TestActor._ + import FSM._ + import TestActor._ - startWith(0, None) - when(0, stateTimeout = 5 seconds) { - case Ev(SetTimeout(d)) => - setStateTimeout(0, if (d.finite_?) d else None) - stay - case Ev(SetIgnore(ign)) => stay using ign - case Ev(StateTimeout) => - stop - case Event(x : AnyRef, ign) => - val ignore = ign map (z => if (z isDefinedAt x) z(x) else false) getOrElse false - if (!ignore) { - queue.offerLast(x) - } - stay - } - initialize + startWith(0, None) + when(0, stateTimeout = 5 seconds) { + case Ev(SetTimeout(d)) => + setStateTimeout(0, if (d.finite_?) d else None) + stay + case Ev(SetIgnore(ign)) => stay using ign + case Ev(StateTimeout) => + stop + case Event(x : AnyRef, ign) => + val ignore = ign map (z => if (z isDefinedAt x) z(x) else false) getOrElse false + if (!ignore) { + queue.offerLast(x) + } + stay + } + initialize } /** @@ -48,9 +48,9 @@ class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestA * val test = actorOf[SomeActor].start * * within (1 second) { - * test ! SomeWork - * expectMsg(Result1) // bounded to 1 second - * expectMsg(Result2) // bounded to the remainder of the 1 second + * test ! SomeWork + * expectMsg(Result1) // bounded to 1 second + * expectMsg(Result2) // bounded to the remainder of the 1 second * } * } * @@ -70,365 +70,365 @@ class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestA */ trait TestKit { - private val queue = new LinkedBlockingDeque[AnyRef]() - - /** - * ActorRef of the test actor. Access is provided to enable e.g. - * registration as message target. - */ - protected val testActor = actorOf(new TestActor(queue)).start + private val queue = new LinkedBlockingDeque[AnyRef]() + + /** + * ActorRef of the test actor. Access is provided to enable e.g. + * registration as message target. + */ + protected val testActor = actorOf(new TestActor(queue)).start - /** - * Implicit sender reference so that replies are possible for messages sent - * from the test class. - */ - protected implicit val senderOption = Some(testActor) + /** + * Implicit sender reference so that replies are possible for messages sent + * from the test class. + */ + protected implicit val senderOption = Some(testActor) - private var end : Duration = Duration.Inf + private var end : Duration = Duration.Inf + /* + * THIS IS A HACK: expectNoMsg and receiveWhile are bounded by `end`, but + * running them should not trigger an AssertionError, so mark their end + * time here and do not fail at the end of `within` if that time is not + * long gone. + */ + private var lastSoftTimeout : Duration = now - 5.millis + + /** + * Stop test actor. Should be done at the end of the test unless relying on + * test actor timeout. + */ + def stopTestActor { testActor.stop } + + /** + * Set test actor timeout. By default, the test actor shuts itself down + * after 5 seconds of inactivity. Set this to Duration.Inf to disable this + * behavior, but make sure that someone will then call `stopTestActor`, + * unless you want to leak actors, e.g. wrap test in + * + *
+   *   try {
+   *     ...
+   *   } finally { stopTestActor }
+   * 
+ */ + def setTestActorTimeout(d : Duration) { testActor ! TestActor.SetTimeout(d) } + + /** + * Ignore all messages in the test actor for which the given partial + * function returns true. + */ + def ignoreMsg(f : PartialFunction[AnyRef, Boolean]) { testActor ! TestActor.SetIgnore(Some(f)) } + + /** + * Stop ignoring messages in the test actor. + */ + def ignoreNoMsg { testActor ! TestActor.SetIgnore(None) } + + /** + * Obtain current time (`System.currentTimeMillis`) as Duration. + */ + def now : Duration = System.nanoTime.nanos + + /** + * Obtain time remaining for execution of the innermost enclosing `within` block. + */ + def remaining : Duration = end - now + + /** + * Execute code block while bounding its execution time between `min` and + * `max`. `within` blocks may be nested. All methods in this trait which + * take maximum wait times are available in a version which implicitly uses + * the remaining time governed by the innermost enclosing `within` block. + * + *
+   * val ret = within(50 millis) {
+   *         test ! "ping"
+   *         expectMsgClass(classOf[String])
+   *       }
+   * 
+ */ + def within[T](min : Duration, max : Duration)(f : => T) : T = { + val start = now + val rem = end - start + assert (rem >= min, "required min time "+min+" not possible, only "+format(min.unit, rem)+" left") + + val max_diff = if (max < rem) max else rem + val prev_end = end + end = start + max_diff + + val ret = f + + val diff = now - start + assert (min <= diff, "block took "+format(min.unit, diff)+", should at least have been "+min) /* - * THIS IS A HACK: expectNoMsg and receiveWhile are bounded by `end`, but - * running them should not trigger an AssertionError, so mark their end - * time here and do not fail at the end of `within` if that time is not - * long gone. + * caution: HACK AHEAD */ - private var lastSoftTimeout : Duration = now - 5.millis - - /** - * Stop test actor. Should be done at the end of the test unless relying on - * test actor timeout. - */ - def stopTestActor { testActor.stop } - - /** - * Set test actor timeout. By default, the test actor shuts itself down - * after 5 seconds of inactivity. Set this to Duration.Inf to disable this - * behavior, but make sure that someone will then call `stopTestActor`, - * unless you want to leak actors, e.g. wrap test in - * - *
-     *   try {
-     *     ...
-     *   } finally { stopTestActor }
-     * 
- */ - def setTestActorTimeout(d : Duration) { testActor ! TestActor.SetTimeout(d) } - - /** - * Ignore all messages in the test actor for which the given partial - * function returns true. - */ - def ignoreMsg(f : PartialFunction[AnyRef, Boolean]) { testActor ! TestActor.SetIgnore(Some(f)) } - - /** - * Stop ignoring messages in the test actor. - */ - def ignoreNoMsg { testActor ! TestActor.SetIgnore(None) } - - /** - * Obtain current time (`System.currentTimeMillis`) as Duration. - */ - def now : Duration = System.nanoTime.nanos - - /** - * Obtain time remaining for execution of the innermost enclosing `within` block. - */ - def remaining : Duration = end - now - - /** - * Execute code block while bounding its execution time between `min` and - * `max`. `within` blocks may be nested. All methods in this trait which - * take maximum wait times are available in a version which implicitly uses - * the remaining time governed by the innermost enclosing `within` block. - * - *
-     * val ret = within(50 millis) {
-     *             test ! "ping"
-     *             expectMsgClass(classOf[String])
-     *           }
-     * 
- */ - def within[T](min : Duration, max : Duration)(f : => T) : T = { - val start = now - val rem = end - start - assert (rem >= min, "required min time "+min+" not possible, only "+format(min.unit, rem)+" left") - - val max_diff = if (max < rem) max else rem - val prev_end = end - end = start + max_diff - - val ret = f - - val diff = now - start - assert (min <= diff, "block took "+format(min.unit, diff)+", should at least have been "+min) - /* - * caution: HACK AHEAD - */ - if (now - lastSoftTimeout > 5.millis) { - assert (diff <= max_diff, "block took "+format(max.unit, diff)+", exceeding "+format(max.unit, max_diff)) - } else { - lastSoftTimeout -= 5.millis - } - - end = prev_end - ret + if (now - lastSoftTimeout > 5.millis) { + assert (diff <= max_diff, "block took "+format(max.unit, diff)+", exceeding "+format(max.unit, max_diff)) + } else { + lastSoftTimeout -= 5.millis } - /** - * Same as calling `within(0 seconds, max)(f)`. - */ - def within[T](max : Duration)(f : => T) : T = within(0 seconds, max)(f) + end = prev_end + ret + } - /** - * Same as `expectMsg`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsg(obj : Any) : AnyRef = expectMsg(remaining, obj) + /** + * Same as calling `within(0 seconds, max)(f)`. + */ + def within[T](max : Duration)(f : => T) : T = within(0 seconds, max)(f) - /** - * Receive one message from the test actor and assert that it equals the - * given object. Wait time is bounded by the given duration, with an - * AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsg(max : Duration, obj : Any) : AnyRef = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsg") - assert (obj == o, "expected "+obj+", found "+o) - o + /** + * Same as `expectMsg`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsg(obj : Any) : AnyRef = expectMsg(remaining, obj) + + /** + * Receive one message from the test actor and assert that it equals the + * given object. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsg(max : Duration, obj : Any) : AnyRef = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsg") + assert (obj == o, "expected "+obj+", found "+o) + o + } + + /** + * Same as `expectMsg`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsg[T](f : PartialFunction[Any, T]) : T = expectMsg(remaining)(f) + + /** + * Receive one message from the test actor and assert that the given + * partial function accepts it. Wait time is bounded by the given duration, + * with an AssertionFailure being thrown in case of timeout. + * + * Use this variant to implement more complicated or conditional + * processing. + * + * @return the received object as transformed by the partial function + */ + def expectMsg[T](max : Duration)(f : PartialFunction[Any, T]) : T = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsg") + assert (f.isDefinedAt(o), "does not match: "+o) + f(o) + } + + /** + * Same as `expectMsgClass`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgClass[C](c : Class[C]) : C = expectMsgClass(remaining, c) + + /** + * Receive one message from the test actor and assert that it conforms to + * the given class. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsgClass[C](max : Duration, c : Class[C]) : C = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsgClass") + assert (c isInstance o, "expected "+c+", found "+o.getClass) + o.asInstanceOf[C] + } + + /** + * Same as `expectMsgAnyOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAnyOf(obj : Any*) : AnyRef = expectMsgAnyOf(remaining, obj : _*) + + /** + * Receive one message from the test actor and assert that it equals one of + * the given objects. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsgAnyOf(max : Duration, obj : Any*) : AnyRef = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsgAnyOf") + assert (obj exists (_ == o), "found unexpected "+o) + o + } + + /** + * Same as `expectMsgAnyClassOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAnyClassOf(obj : Class[_]*) : AnyRef = expectMsgAnyClassOf(remaining, obj : _*) + + /** + * Receive one message from the test actor and assert that it conforms to + * one of the given classes. Wait time is bounded by the given duration, + * with an AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsgAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsgAnyClassOf") + assert (obj exists (_ isInstance o), "found unexpected "+o) + o + } + + /** + * Same as `expectMsgAllOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAllOf(obj : Any*) { expectMsgAllOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of objects and assert that for each given object one is received + * which equals it. This construct is useful when the order in which the + * objects are received is not fixed. Wait time is bounded by the given + * duration, with an AssertionFailure being thrown in case of timeout. + * + *
+   * within(1 second) {
+   *   dispatcher ! SomeWork1()
+   *   dispatcher ! SomeWork2()
+   *   expectMsgAllOf(Result1(), Result2())
+   * }
+   * 
+ */ + def expectMsgAllOf(max : Duration, obj : Any*) { + val recv = receiveN(obj.size, now + max) + assert (obj forall (x => recv exists (x == _)), "not found all") + } + + /** + * Same as `expectMsgAllClassOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAllClassOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of classes and assert that for each given class one is received + * which is of that class (equality, not conformance). This construct is + * useful when the order in which the objects are received is not fixed. + * Wait time is bounded by the given duration, with an AssertionFailure + * being thrown in case of timeout. + */ + def expectMsgAllClassOf(max : Duration, obj : Class[_]*) { + val recv = receiveN(obj.size, now + max) + assert (obj forall (x => recv exists (_.getClass eq x)), "not found all") + } + + /** + * Same as `expectMsgAllConformingOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAllConformingOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of classes and assert that for each given class one is received + * which conforms to that class. This construct is useful when the order in + * which the objects are received is not fixed. Wait time is bounded by + * the given duration, with an AssertionFailure being thrown in case of + * timeout. + * + * Beware that one object may satisfy all given class constraints, which + * may be counter-intuitive. + */ + def expectMsgAllConformingOf(max : Duration, obj : Class[_]*) { + val recv = receiveN(obj.size, now + max) + assert (obj forall (x => recv exists (x isInstance _)), "not found all") + } + + /** + * Same as `expectNoMsg`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectNoMsg { expectNoMsg(remaining) } + + /** + * Assert that no message is received for the specified time. + */ + def expectNoMsg(max : Duration) { + val o = receiveOne(max) + assert (o eq null, "received unexpected message "+o) + lastSoftTimeout = now + } + + /** + * Same as `receiveWhile`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def receiveWhile[T](f : PartialFunction[AnyRef, T]) : Seq[T] = receiveWhile(remaining)(f) + + /** + * Receive a series of messages as long as the given partial function + * accepts them or the idle timeout is met or the overall maximum duration + * is elapsed. Returns the sequence of messages. + * + * Beware that the maximum duration is not implicitly bounded by or taken + * from the innermost enclosing `within` block, as it is not an error to + * hit the `max` duration in this case. + * + * One possible use of this method is for testing whether messages of + * certain characteristics are generated at a certain rate: + * + *
+   * test ! ScheduleTicks(100 millis)
+   * val series = receiveWhile(750 millis) {
+   *     case Tick(count) => count
+   * }
+   * assert(series == (1 to 7).toList)
+   * 
+ */ + def receiveWhile[T](max : Duration)(f : PartialFunction[AnyRef, T]) : Seq[T] = { + val stop = now + max + + @tailrec def doit(acc : List[T]) : List[T] = { + receiveOne(stop - now) match { + case null => + acc.reverse + case o if (f isDefinedAt o) => + doit(f(o) :: acc) + case o => + queue.offerFirst(o) + acc.reverse + } } - /** - * Same as `expectMsg`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsg[T](f : PartialFunction[Any, T]) : T = expectMsg(remaining)(f) + val ret = doit(Nil) + lastSoftTimeout = now + ret + } - /** - * Receive one message from the test actor and assert that the given - * partial function accepts it. Wait time is bounded by the given duration, - * with an AssertionFailure being thrown in case of timeout. - * - * Use this variant to implement more complicated or conditional - * processing. - * - * @return the received object as transformed by the partial function - */ - def expectMsg[T](max : Duration)(f : PartialFunction[Any, T]) : T = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsg") - assert (f.isDefinedAt(o), "does not match: "+o) - f(o) + private def receiveN(n : Int, stop : Duration) : Seq[AnyRef] = { + for { x <- 1 to n } yield { + val timeout = stop - now + val o = receiveOne(timeout) + assert (o ne null, "timeout while expecting "+n+" messages") + o } + } - /** - * Same as `expectMsgClass`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgClass[C](c : Class[C]) : C = expectMsgClass(remaining, c) - - /** - * Receive one message from the test actor and assert that it conforms to - * the given class. Wait time is bounded by the given duration, with an - * AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsgClass[C](max : Duration, c : Class[C]) : C = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsgClass") - assert (c isInstance o, "expected "+c+", found "+o.getClass) - o.asInstanceOf[C] + private def receiveOne(max : Duration) : AnyRef = { + if (max == 0.seconds) { + queue.pollFirst + } else if (max.finite_?) { + queue.pollFirst(max.length, max.unit) + } else { + queue.takeFirst } + } - /** - * Same as `expectMsgAnyOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAnyOf(obj : Any*) : AnyRef = expectMsgAnyOf(remaining, obj : _*) - - /** - * Receive one message from the test actor and assert that it equals one of - * the given objects. Wait time is bounded by the given duration, with an - * AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsgAnyOf(max : Duration, obj : Any*) : AnyRef = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsgAnyOf") - assert (obj exists (_ == o), "found unexpected "+o) - o - } - - /** - * Same as `expectMsgAnyClassOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAnyClassOf(obj : Class[_]*) : AnyRef = expectMsgAnyClassOf(remaining, obj : _*) - - /** - * Receive one message from the test actor and assert that it conforms to - * one of the given classes. Wait time is bounded by the given duration, - * with an AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsgAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsgAnyClassOf") - assert (obj exists (_ isInstance o), "found unexpected "+o) - o - } - - /** - * Same as `expectMsgAllOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAllOf(obj : Any*) { expectMsgAllOf(remaining, obj : _*) } - - /** - * Receive a number of messages from the test actor matching the given - * number of objects and assert that for each given object one is received - * which equals it. This construct is useful when the order in which the - * objects are received is not fixed. Wait time is bounded by the given - * duration, with an AssertionFailure being thrown in case of timeout. - * - *
-     * within(1 second) {
-     *   dispatcher ! SomeWork1()
-     *   dispatcher ! SomeWork2()
-     *   expectMsgAllOf(Result1(), Result2())
-     * }
-     * 
- */ - def expectMsgAllOf(max : Duration, obj : Any*) { - val recv = receiveN(obj.size, now + max) - assert (obj forall (x => recv exists (x == _)), "not found all") - } - - /** - * Same as `expectMsgAllClassOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAllClassOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } - - /** - * Receive a number of messages from the test actor matching the given - * number of classes and assert that for each given class one is received - * which is of that class (equality, not conformance). This construct is - * useful when the order in which the objects are received is not fixed. - * Wait time is bounded by the given duration, with an AssertionFailure - * being thrown in case of timeout. - */ - def expectMsgAllClassOf(max : Duration, obj : Class[_]*) { - val recv = receiveN(obj.size, now + max) - assert (obj forall (x => recv exists (_.getClass eq x)), "not found all") - } - - /** - * Same as `expectMsgAllConformingOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAllConformingOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } - - /** - * Receive a number of messages from the test actor matching the given - * number of classes and assert that for each given class one is received - * which conforms to that class. This construct is useful when the order in - * which the objects are received is not fixed. Wait time is bounded by - * the given duration, with an AssertionFailure being thrown in case of - * timeout. - * - * Beware that one object may satisfy all given class constraints, which - * may be counter-intuitive. - */ - def expectMsgAllConformingOf(max : Duration, obj : Class[_]*) { - val recv = receiveN(obj.size, now + max) - assert (obj forall (x => recv exists (x isInstance _)), "not found all") - } - - /** - * Same as `expectNoMsg`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectNoMsg { expectNoMsg(remaining) } - - /** - * Assert that no message is received for the specified time. - */ - def expectNoMsg(max : Duration) { - val o = receiveOne(max) - assert (o eq null, "received unexpected message "+o) - lastSoftTimeout = now - } - - /** - * Same as `receiveWhile`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def receiveWhile[T](f : PartialFunction[AnyRef, T]) : Seq[T] = receiveWhile(remaining)(f) - - /** - * Receive a series of messages as long as the given partial function - * accepts them or the idle timeout is met or the overall maximum duration - * is elapsed. Returns the sequence of messages. - * - * Beware that the maximum duration is not implicitly bounded by or taken - * from the innermost enclosing `within` block, as it is not an error to - * hit the `max` duration in this case. - * - * One possible use of this method is for testing whether messages of - * certain characteristics are generated at a certain rate: - * - *
-     * test ! ScheduleTicks(100 millis)
-     * val series = receiveWhile(750 millis) {
-     *     case Tick(count) => count
-     * }
-     * assert(series == (1 to 7).toList)
-     * 
- */ - def receiveWhile[T](max : Duration)(f : PartialFunction[AnyRef, T]) : Seq[T] = { - val stop = now + max - - @tailrec def doit(acc : List[T]) : List[T] = { - receiveOne(stop - now) match { - case null => - acc.reverse - case o if (f isDefinedAt o) => - doit(f(o) :: acc) - case o => - queue.offerFirst(o) - acc.reverse - } - } - - val ret = doit(Nil) - lastSoftTimeout = now - ret - } - - private def receiveN(n : Int, stop : Duration) : Seq[AnyRef] = { - for { x <- 1 to n } yield { - val timeout = stop - now - val o = receiveOne(timeout) - assert (o ne null, "timeout while expecting "+n+" messages") - o - } - } - - private def receiveOne(max : Duration) : AnyRef = { - if (max == 0.seconds) { - queue.pollFirst - } else if (max.finite_?) { - queue.pollFirst(max.length, max.unit) - } else { - queue.takeFirst - } - } - - private def format(u : TimeUnit, d : Duration) = "%.3f %s".format(d.toUnit(u), u.toString.toLowerCase) + private def format(u : TimeUnit, d : Duration) = "%.3f %s".format(d.toUnit(u), u.toString.toLowerCase) } -// vim: set ts=4 sw=4 et: +// vim: set ts=2 sw=2 et: diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala index 3bb3353da5..b0521bbc1b 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -7,114 +7,114 @@ import org.scalatest.WordSpec import org.scalatest.matchers.MustMatchers class FSMTimingSpec - extends WordSpec - with MustMatchers - with TestKit { + extends WordSpec + with MustMatchers + with TestKit { - import FSMTimingSpec._ - import FSM._ + import FSMTimingSpec._ + import FSM._ - val fsm = Actor.actorOf(new StateMachine(testActor)).start - fsm ! SubscribeTransitionCallBack(testActor) - expectMsg(50 millis, Initial) + val fsm = Actor.actorOf(new StateMachine(testActor)).start + fsm ! SubscribeTransitionCallBack(testActor) + expectMsg(50 millis, Initial) - ignoreMsg { - case Transition(Initial, _) => true + ignoreMsg { + case Transition(Initial, _) => true + } + + "A Finite State Machine" must { + + "receive StateTimeout" in { + within (50 millis, 150 millis) { + fsm ! TestStateTimeout + expectMsg(Transition(TestStateTimeout, Initial)) + expectNoMsg + } } - "A Finite State Machine" must { - - "receive StateTimeout" in { - within (50 millis, 150 millis) { - fsm ! TestStateTimeout - expectMsg(Transition(TestStateTimeout, Initial)) - expectNoMsg - } - } - - "receive single-shot timer" in { - within (50 millis, 150 millis) { - fsm ! TestSingleTimer - expectMsg(Tick) - expectMsg(Transition(TestSingleTimer, Initial)) - expectNoMsg - } - } - - "receive and cancel a repeated timer" in { - fsm ! TestRepeatedTimer - val seq = receiveWhile(550 millis) { - case Tick => Tick - } - seq must have length (5) - within(250 millis) { - fsm ! Cancel - expectMsg(Transition(TestRepeatedTimer, Initial)) - expectNoMsg - } - } - - "notify unhandled messages" in { - within(200 millis) { - fsm ! Cancel - expectMsg(Unhandled(Cancel)) - expectNoMsg - } - } - + "receive single-shot timer" in { + within (50 millis, 150 millis) { + fsm ! TestSingleTimer + expectMsg(Tick) + expectMsg(Transition(TestSingleTimer, Initial)) + expectNoMsg + } } + "receive and cancel a repeated timer" in { + fsm ! TestRepeatedTimer + val seq = receiveWhile(550 millis) { + case Tick => Tick + } + seq must have length (5) + within(250 millis) { + fsm ! Cancel + expectMsg(Transition(TestRepeatedTimer, Initial)) + expectNoMsg + } + } + + "notify unhandled messages" in { + within(200 millis) { + fsm ! Cancel + expectMsg(Unhandled(Cancel)) + expectNoMsg + } + } + + } + } object FSMTimingSpec { - trait State - case object Initial extends State - case object TestStateTimeout extends State - case object TestSingleTimer extends State - case object TestRepeatedTimer extends State + trait State + case object Initial extends State + case object TestStateTimeout extends State + case object TestSingleTimer extends State + case object TestRepeatedTimer extends State - case object Tick - case object Cancel + case object Tick + case object Cancel - case class Unhandled(msg : AnyRef) + case class Unhandled(msg : AnyRef) - class StateMachine(tester : ActorRef) extends Actor with FSM[State, Unit] { - import FSM._ + class StateMachine(tester : ActorRef) extends Actor with FSM[State, Unit] { + import FSM._ - startWith(Initial, ()) - when(Initial) { - case Ev(TestStateTimeout) => goto(TestStateTimeout) - case Ev(TestSingleTimer) => - setTimer("tester", Tick, 100 millis, false) - goto(TestSingleTimer) - case Ev(TestRepeatedTimer) => - setTimer("tester", Tick, 100 millis, true) - goto(TestRepeatedTimer) - } - when(TestStateTimeout, stateTimeout = 100 millis) { - case Ev(StateTimeout) => goto(Initial) - } - when(TestSingleTimer) { - case Ev(Tick) => - tester ! Tick - goto(Initial) - } - when(TestRepeatedTimer) { - case Ev(Tick) => - tester ! Tick - stay - case Ev(Cancel) => - cancelTimer("tester") - goto(Initial) - } - - whenUnhandled { - case Ev(msg : AnyRef) => - tester ! Unhandled(msg) - stay - } + startWith(Initial, ()) + when(Initial) { + case Ev(TestStateTimeout) => goto(TestStateTimeout) + case Ev(TestSingleTimer) => + setTimer("tester", Tick, 100 millis, false) + goto(TestSingleTimer) + case Ev(TestRepeatedTimer) => + setTimer("tester", Tick, 100 millis, true) + goto(TestRepeatedTimer) } + when(TestStateTimeout, stateTimeout = 100 millis) { + case Ev(StateTimeout) => goto(Initial) + } + when(TestSingleTimer) { + case Ev(Tick) => + tester ! Tick + goto(Initial) + } + when(TestRepeatedTimer) { + case Ev(Tick) => + tester ! Tick + stay + case Ev(Cancel) => + cancelTimer("tester") + goto(Initial) + } + + whenUnhandled { + case Ev(msg : AnyRef) => + tester ! Unhandled(msg) + stay + } + } } diff --git a/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala index e9d54cccbe..8c232255d0 100644 --- a/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala @@ -15,61 +15,61 @@ import akka.actor.{FSM, Actor} * possibly logged). */ object Buncher { - trait State - case object Idle extends State - case object Active extends State + trait State + case object Idle extends State + case object Active extends State - case object Flush // send out current queue immediately - case object Stop // poison pill + case object Flush // send out current queue immediately + case object Stop // poison pill - case class Data[A](start : Long, xs : List[A]) - - def apply[A : Manifest](singleTimeout : Duration, - multiTimeout : Duration)(f : List[A] => Unit) = - Actor.actorOf(new Buncher[A](singleTimeout, multiTimeout).deliver(f)) + case class Data[A](start : Long, xs : List[A]) + + def apply[A : Manifest](singleTimeout : Duration, + multiTimeout : Duration)(f : List[A] => Unit) = + Actor.actorOf(new Buncher[A](singleTimeout, multiTimeout).deliver(f)) } - + class Buncher[A : Manifest] private (val singleTimeout : Duration, val multiTimeout : Duration) - extends Actor with FSM[Buncher.State, Buncher.Data[A]] { - import Buncher._ - import FSM._ - - private val manifestA = manifest[A] - - private var send : List[A] => Unit = _ - private def deliver(f : List[A] => Unit) = { send = f; this } - - private def now = System.currentTimeMillis - private def check(m : AnyRef) = ClassManifest.fromClass(m.getClass) <:< manifestA + extends Actor with FSM[Buncher.State, Buncher.Data[A]] { + import Buncher._ + import FSM._ + + private val manifestA = manifest[A] + + private var send : List[A] => Unit = _ + private def deliver(f : List[A] => Unit) = { send = f; this } + + private def now = System.currentTimeMillis + private def check(m : AnyRef) = ClassManifest.fromClass(m.getClass) <:< manifestA - startWith(Idle, Data(0, Nil)) - - when(Idle) { - case Event(m : AnyRef, _) if check(m) => - goto(Active) using Data(now, m.asInstanceOf[A] :: Nil) - case Event(Flush, _) => stay - case Event(Stop, _) => stop - } - - when(Active, stateTimeout = Some(singleTimeout)) { - case Event(m : AnyRef, Data(start, xs)) if check(m) => - val l = m.asInstanceOf[A] :: xs - if (now - start > multiTimeout.toMillis) { - send(l.reverse) - goto(Idle) using Data(0, Nil) - } else { - stay using Data(start, l) - } - case Event(StateTimeout, Data(_, xs)) => - send(xs.reverse) - goto(Idle) using Data(0, Nil) - case Event(Flush, Data(_, xs)) => - send(xs.reverse) - goto(Idle) using Data(0, Nil) - case Event(Stop, Data(_, xs)) => - send(xs.reverse) - stop - } - - initialize + startWith(Idle, Data(0, Nil)) + + when(Idle) { + case Event(m : AnyRef, _) if check(m) => + goto(Active) using Data(now, m.asInstanceOf[A] :: Nil) + case Event(Flush, _) => stay + case Event(Stop, _) => stop + } + + when(Active, stateTimeout = Some(singleTimeout)) { + case Event(m : AnyRef, Data(start, xs)) if check(m) => + val l = m.asInstanceOf[A] :: xs + if (now - start > multiTimeout.toMillis) { + send(l.reverse) + goto(Idle) using Data(0, Nil) + } else { + stay using Data(start, l) + } + case Event(StateTimeout, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Flush, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Stop, Data(_, xs)) => + send(xs.reverse) + stop + } + + initialize } From 5094e87df78226fe7555c976c628951520ae8308 Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 3 Jan 2011 19:37:23 +0100 Subject: [PATCH 25/31] Move handleEvent var declaration _after_ handleEventDefault val declaration. Using a val before defining it causes nullpointer exceptions... --- akka-actor/src/main/scala/akka/actor/FSM.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index f25d4ea164..3140fd9cdf 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -288,12 +288,12 @@ trait FSM[S, D] { } } - private var handleEvent: StateFunction = handleEventDefault private val handleEventDefault: StateFunction = { case Event(value, stateData) => log.slf4j.warn("Event {} not handled in state {}, staying at current state", value, currentState.stateName) stay } + private var handleEvent: StateFunction = handleEventDefault private var terminateEvent: PartialFunction[StopEvent[S,D], Unit] = { case StopEvent(Failure(cause), _, _) => From c66bbb566d70b85534045397d4fa0b4c60c5a7a1 Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 3 Jan 2011 19:50:53 +0100 Subject: [PATCH 26/31] add fsm self actor ref to external transition message --- akka-actor/src/main/scala/akka/actor/FSM.scala | 4 ++-- .../test/scala/akka/actor/actor/FSMActorSpec.scala | 11 ++++++----- .../test/scala/akka/actor/actor/FSMTimingSpec.scala | 8 ++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 3140fd9cdf..80c0fba49f 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -10,7 +10,7 @@ import java.util.concurrent.ScheduledFuture object FSM { - case class Transition[S](from: S, to: S) + case class Transition[S](fsmRef: ActorRef, from: S, to: S) case class SubscribeTransitionCallBack(actorRef: ActorRef) case class UnsubscribeTransitionCallBack(actorRef: ActorRef) @@ -355,7 +355,7 @@ trait FSM[S, D] { if (currentState.stateName != nextState.stateName) { transitionEvent.apply(currentState.stateName, nextState.stateName) if (!transitionCallBackList.isEmpty) { - val transition = Transition(currentState.stateName, nextState.stateName) + val transition = Transition(self, currentState.stateName, nextState.stateName) transitionCallBackList.foreach(_ ! transition) } } diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala index f9dc185597..47378c24a6 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala @@ -67,10 +67,11 @@ object FSMActorSpec { } } - onTransition((oldState, newState) => Transition(oldState, newState) match { - case Transition(Locked, Open) => transitionLatch.open - case Transition(_, _) => () - }) + onTransition(transitionHandler) + + def transitionHandler(from: LockState, to: LockState) = { + if (from == Locked && to == Open) transitionLatch.open + } onTermination { case StopEvent(Shutdown, Locked, _) => @@ -106,7 +107,7 @@ class FSMActorSpec extends JUnitSuite { val lock = Actor.actorOf(new Lock("33221", (1, TimeUnit.SECONDS))).start val transitionTester = Actor.actorOf(new Actor { def receive = { - case Transition(_, _) => transitionCallBackLatch.open + case Transition(_, _, _) => transitionCallBackLatch.open case Locked => initialStateLatch.open }}).start diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala index 3bb3353da5..6d9df1c388 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -19,7 +19,7 @@ class FSMTimingSpec expectMsg(50 millis, Initial) ignoreMsg { - case Transition(Initial, _) => true + case Transition(_, Initial, _) => true } "A Finite State Machine" must { @@ -27,7 +27,7 @@ class FSMTimingSpec "receive StateTimeout" in { within (50 millis, 150 millis) { fsm ! TestStateTimeout - expectMsg(Transition(TestStateTimeout, Initial)) + expectMsg(Transition(fsm, TestStateTimeout, Initial)) expectNoMsg } } @@ -36,7 +36,7 @@ class FSMTimingSpec within (50 millis, 150 millis) { fsm ! TestSingleTimer expectMsg(Tick) - expectMsg(Transition(TestSingleTimer, Initial)) + expectMsg(Transition(fsm, TestSingleTimer, Initial)) expectNoMsg } } @@ -49,7 +49,7 @@ class FSMTimingSpec seq must have length (5) within(250 millis) { fsm ! Cancel - expectMsg(Transition(TestRepeatedTimer, Initial)) + expectMsg(Transition(fsm, TestRepeatedTimer, Initial)) expectNoMsg } } From dfd18968c8126162c326de5cae4924a9ec867426 Mon Sep 17 00:00:00 2001 From: momania Date: Mon, 3 Jan 2011 20:02:03 +0100 Subject: [PATCH 27/31] wrap initial sending of state to transition listener in CurrentState object with fsm actor ref --- akka-actor/src/main/scala/akka/actor/FSM.scala | 3 ++- akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala | 2 +- akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 80c0fba49f..2763e1c0bb 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -10,6 +10,7 @@ import java.util.concurrent.ScheduledFuture object FSM { + case class CurrentState[S](fsmRef: ActorRef, state: S) case class Transition[S](fsmRef: ActorRef, from: S, to: S) case class SubscribeTransitionCallBack(actorRef: ActorRef) case class UnsubscribeTransitionCallBack(actorRef: ActorRef) @@ -319,7 +320,7 @@ trait FSM[S, D] { } case SubscribeTransitionCallBack(actorRef) => // send current state back as reference point - actorRef ! currentState.stateName + actorRef ! CurrentState(self, currentState.stateName) transitionCallBackList ::= actorRef case UnsubscribeTransitionCallBack(actorRef) => transitionCallBackList = transitionCallBackList.filterNot(_ == actorRef) diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala index 47378c24a6..b25e96e87c 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMActorSpec.scala @@ -108,7 +108,7 @@ class FSMActorSpec extends JUnitSuite { val transitionTester = Actor.actorOf(new Actor { def receive = { case Transition(_, _, _) => transitionCallBackLatch.open - case Locked => initialStateLatch.open + case CurrentState(_, Locked) => initialStateLatch.open }}).start lock ! SubscribeTransitionCallBack(transitionTester) diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala index 6d9df1c388..c565f07447 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -16,7 +16,7 @@ class FSMTimingSpec val fsm = Actor.actorOf(new StateMachine(testActor)).start fsm ! SubscribeTransitionCallBack(testActor) - expectMsg(50 millis, Initial) + expectMsg(50 millis, CurrentState(fsm, Initial)) ignoreMsg { case Transition(_, Initial, _) => true From 094b11cae3eafff591915a9dc07f4389185e9f08 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Mon, 3 Jan 2011 20:04:02 +0100 Subject: [PATCH 28/31] remove one more allocation in hot path --- akka-actor/src/main/scala/akka/actor/FSM.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-actor/src/main/scala/akka/actor/FSM.scala b/akka-actor/src/main/scala/akka/actor/FSM.scala index 10a0944147..683b86b09e 100755 --- a/akka-actor/src/main/scala/akka/actor/FSM.scala +++ b/akka-actor/src/main/scala/akka/actor/FSM.scala @@ -368,7 +368,7 @@ trait FSM[S, D] { private def applyState(nextState: State) = { currentState = nextState - val timeout = currentState.timeout orElse stateTimeouts(currentState.stateName) + val timeout = if (currentState.timeout.isDefined) currentState.timeout else stateTimeouts(currentState.stateName) if (timeout.isDefined) { val t = timeout.get if (t.length >= 0) { From 639b141cdc861d945719864bf7832530853e3e43 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Mon, 3 Jan 2011 23:06:28 +0100 Subject: [PATCH 29/31] also test custom whenUnhandled fall-through --- .../test/scala/akka/actor/actor/FSMTimingSpec.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala index c0ed57c47c..2d39a4ee14 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -16,7 +16,7 @@ class FSMTimingSpec val fsm = Actor.actorOf(new StateMachine(testActor)).start fsm ! SubscribeTransitionCallBack(testActor) - expectMsg(50 millis, Initial) + expectMsg(100 millis, Initial) ignoreMsg { case Transition(Initial, _) => true @@ -66,6 +66,10 @@ class FSMTimingSpec expectMsg(Unhandled(Tick)) expectNoMsg } + within(100 millis) { + fsm ! Unhandled("test") + expectNoMsg + } within(100 millis) { fsm ! Cancel expectMsg(Transition(TestUnhandled, Initial)) @@ -123,8 +127,8 @@ object FSMTimingSpec { when(TestUnhandled) { case Ev(SetHandler) => whenUnhandled { - case Ev(msg : AnyRef) => - tester ! Unhandled(msg) + case Ev(Tick) => + tester ! Unhandled(Tick) stay } stay From dc1fe991bb12fd7366f89d06faf3daa183792dd7 Mon Sep 17 00:00:00 2001 From: momania Date: Tue, 4 Jan 2011 09:32:13 +0100 Subject: [PATCH 30/31] small adjustment to the example, showing the correct use of the startWith and initialize --- .../src/main/scala/DiningHakkersOnFsm.scala | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala index 1147e6d0bd..7a4641c35e 100644 --- a/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/DiningHakkersOnFsm.scala @@ -33,6 +33,9 @@ case class TakenBy(hakker: Option[ActorRef]) class Chopstick(name: String) extends Actor with FSM[ChopstickState, TakenBy] { self.id = name + // A chopstick begins its existence as available and taken by no one + startWith(Available, TakenBy(None)) + // When a chopstick is available, it can be taken by a some hakker when(Available) { case Event(Take, _) => @@ -49,8 +52,8 @@ class Chopstick(name: String) extends Actor with FSM[ChopstickState, TakenBy] { goto(Available) using TakenBy(None) } - // A chopstick begins its existence as available and taken by no one - startWith(Available, TakenBy(None)) + // Initialze the chopstick + initialize } /** @@ -81,6 +84,9 @@ case class TakenChopsticks(left: Option[ActorRef], right: Option[ActorRef]) class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor with FSM[FSMHakkerState, TakenChopsticks] { self.id = name + //All hakkers start waiting + startWith(Waiting, TakenChopsticks(None, None)) + when(Waiting) { case Event(Think, _) => log.info("%s starts to think", name) @@ -147,12 +153,12 @@ class FSMHakker(name: String, left: ActorRef, right: ActorRef) extends Actor wit startThinking(5 seconds) } + // Initialize the hakker + initialize + private def startThinking(duration: Duration): State = { goto(Thinking) using TakenChopsticks(None, None) forMax duration } - - //All hakkers start waiting - startWith(Waiting, TakenChopsticks(None, None)) } /* From be655aa3235c3b5b56c9e0b5205c51ab05a57ae0 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Tue, 4 Jan 2011 13:50:50 +0100 Subject: [PATCH 31/31] fix up indentation --- .../src/main/scala/akka/util/TestKit.scala | 738 +++++++++--------- .../akka/actor/actor/FSMTimingSpec.scala | 80 +- .../src/main/scala/Buncher.scala | 104 +-- 3 files changed, 461 insertions(+), 461 deletions(-) diff --git a/akka-actor/src/main/scala/akka/util/TestKit.scala b/akka-actor/src/main/scala/akka/util/TestKit.scala index c79c58db98..bb400ff992 100644 --- a/akka-actor/src/main/scala/akka/util/TestKit.scala +++ b/akka-actor/src/main/scala/akka/util/TestKit.scala @@ -9,32 +9,32 @@ import java.util.concurrent.{BlockingDeque, LinkedBlockingDeque, TimeUnit} import scala.annotation.tailrec object TestActor { - type Ignore = Option[PartialFunction[AnyRef, Boolean]] + type Ignore = Option[PartialFunction[AnyRef, Boolean]] - case class SetTimeout(d : Duration) - case class SetIgnore(i : Ignore) + case class SetTimeout(d : Duration) + case class SetIgnore(i : Ignore) } class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestActor.Ignore] { - import FSM._ - import TestActor._ + import FSM._ + import TestActor._ - startWith(0, None) - when(0, stateTimeout = 5 seconds) { - case Ev(SetTimeout(d)) => - setStateTimeout(0, if (d.finite_?) d else None) - stay - case Ev(SetIgnore(ign)) => stay using ign - case Ev(StateTimeout) => - stop - case Event(x : AnyRef, ign) => - val ignore = ign map (z => if (z isDefinedAt x) z(x) else false) getOrElse false - if (!ignore) { - queue.offerLast(x) - } - stay - } - initialize + startWith(0, None) + when(0, stateTimeout = 5 seconds) { + case Ev(SetTimeout(d)) => + setStateTimeout(0, if (d.finite_?) d else None) + stay + case Ev(SetIgnore(ign)) => stay using ign + case Ev(StateTimeout) => + stop + case Event(x : AnyRef, ign) => + val ignore = ign map (z => if (z isDefinedAt x) z(x) else false) getOrElse false + if (!ignore) { + queue.offerLast(x) + } + stay + } + initialize } /** @@ -48,9 +48,9 @@ class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestA * val test = actorOf[SomeActor].start * * within (1 second) { - * test ! SomeWork - * expectMsg(Result1) // bounded to 1 second - * expectMsg(Result2) // bounded to the remainder of the 1 second + * test ! SomeWork + * expectMsg(Result1) // bounded to 1 second + * expectMsg(Result2) // bounded to the remainder of the 1 second * } * } * @@ -70,365 +70,365 @@ class TestActor(queue : BlockingDeque[AnyRef]) extends Actor with FSM[Int, TestA */ trait TestKit { - private val queue = new LinkedBlockingDeque[AnyRef]() - - /** - * ActorRef of the test actor. Access is provided to enable e.g. - * registration as message target. - */ - protected val testActor = actorOf(new TestActor(queue)).start + private val queue = new LinkedBlockingDeque[AnyRef]() + + /** + * ActorRef of the test actor. Access is provided to enable e.g. + * registration as message target. + */ + protected val testActor = actorOf(new TestActor(queue)).start - /** - * Implicit sender reference so that replies are possible for messages sent - * from the test class. - */ - protected implicit val senderOption = Some(testActor) + /** + * Implicit sender reference so that replies are possible for messages sent + * from the test class. + */ + protected implicit val senderOption = Some(testActor) - private var end : Duration = Duration.Inf + private var end : Duration = Duration.Inf + /* + * THIS IS A HACK: expectNoMsg and receiveWhile are bounded by `end`, but + * running them should not trigger an AssertionError, so mark their end + * time here and do not fail at the end of `within` if that time is not + * long gone. + */ + private var lastSoftTimeout : Duration = now - 5.millis + + /** + * Stop test actor. Should be done at the end of the test unless relying on + * test actor timeout. + */ + def stopTestActor { testActor.stop } + + /** + * Set test actor timeout. By default, the test actor shuts itself down + * after 5 seconds of inactivity. Set this to Duration.Inf to disable this + * behavior, but make sure that someone will then call `stopTestActor`, + * unless you want to leak actors, e.g. wrap test in + * + *
+   *   try {
+   *     ...
+   *   } finally { stopTestActor }
+   * 
+ */ + def setTestActorTimeout(d : Duration) { testActor ! TestActor.SetTimeout(d) } + + /** + * Ignore all messages in the test actor for which the given partial + * function returns true. + */ + def ignoreMsg(f : PartialFunction[AnyRef, Boolean]) { testActor ! TestActor.SetIgnore(Some(f)) } + + /** + * Stop ignoring messages in the test actor. + */ + def ignoreNoMsg { testActor ! TestActor.SetIgnore(None) } + + /** + * Obtain current time (`System.currentTimeMillis`) as Duration. + */ + def now : Duration = System.nanoTime.nanos + + /** + * Obtain time remaining for execution of the innermost enclosing `within` block. + */ + def remaining : Duration = end - now + + /** + * Execute code block while bounding its execution time between `min` and + * `max`. `within` blocks may be nested. All methods in this trait which + * take maximum wait times are available in a version which implicitly uses + * the remaining time governed by the innermost enclosing `within` block. + * + *
+   * val ret = within(50 millis) {
+   *         test ! "ping"
+   *         expectMsgClass(classOf[String])
+   *       }
+   * 
+ */ + def within[T](min : Duration, max : Duration)(f : => T) : T = { + val start = now + val rem = end - start + assert (rem >= min, "required min time "+min+" not possible, only "+format(min.unit, rem)+" left") + + val max_diff = if (max < rem) max else rem + val prev_end = end + end = start + max_diff + + val ret = f + + val diff = now - start + assert (min <= diff, "block took "+format(min.unit, diff)+", should at least have been "+min) /* - * THIS IS A HACK: expectNoMsg and receiveWhile are bounded by `end`, but - * running them should not trigger an AssertionError, so mark their end - * time here and do not fail at the end of `within` if that time is not - * long gone. + * caution: HACK AHEAD */ - private var lastSoftTimeout : Duration = now - 5.millis - - /** - * Stop test actor. Should be done at the end of the test unless relying on - * test actor timeout. - */ - def stopTestActor { testActor.stop } - - /** - * Set test actor timeout. By default, the test actor shuts itself down - * after 5 seconds of inactivity. Set this to Duration.Inf to disable this - * behavior, but make sure that someone will then call `stopTestActor`, - * unless you want to leak actors, e.g. wrap test in - * - *
-     *   try {
-     *     ...
-     *   } finally { stopTestActor }
-     * 
- */ - def setTestActorTimeout(d : Duration) { testActor ! TestActor.SetTimeout(d) } - - /** - * Ignore all messages in the test actor for which the given partial - * function returns true. - */ - def ignoreMsg(f : PartialFunction[AnyRef, Boolean]) { testActor ! TestActor.SetIgnore(Some(f)) } - - /** - * Stop ignoring messages in the test actor. - */ - def ignoreNoMsg { testActor ! TestActor.SetIgnore(None) } - - /** - * Obtain current time (`System.currentTimeMillis`) as Duration. - */ - def now : Duration = System.nanoTime.nanos - - /** - * Obtain time remaining for execution of the innermost enclosing `within` block. - */ - def remaining : Duration = end - now - - /** - * Execute code block while bounding its execution time between `min` and - * `max`. `within` blocks may be nested. All methods in this trait which - * take maximum wait times are available in a version which implicitly uses - * the remaining time governed by the innermost enclosing `within` block. - * - *
-     * val ret = within(50 millis) {
-     *             test ! "ping"
-     *             expectMsgClass(classOf[String])
-     *           }
-     * 
- */ - def within[T](min : Duration, max : Duration)(f : => T) : T = { - val start = now - val rem = end - start - assert (rem >= min, "required min time "+min+" not possible, only "+format(min.unit, rem)+" left") - - val max_diff = if (max < rem) max else rem - val prev_end = end - end = start + max_diff - - val ret = f - - val diff = now - start - assert (min <= diff, "block took "+format(min.unit, diff)+", should at least have been "+min) - /* - * caution: HACK AHEAD - */ - if (now - lastSoftTimeout > 5.millis) { - assert (diff <= max_diff, "block took "+format(max.unit, diff)+", exceeding "+format(max.unit, max_diff)) - } else { - lastSoftTimeout -= 5.millis - } - - end = prev_end - ret + if (now - lastSoftTimeout > 5.millis) { + assert (diff <= max_diff, "block took "+format(max.unit, diff)+", exceeding "+format(max.unit, max_diff)) + } else { + lastSoftTimeout -= 5.millis } - /** - * Same as calling `within(0 seconds, max)(f)`. - */ - def within[T](max : Duration)(f : => T) : T = within(0 seconds, max)(f) + end = prev_end + ret + } - /** - * Same as `expectMsg`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsg(obj : Any) : AnyRef = expectMsg(remaining, obj) + /** + * Same as calling `within(0 seconds, max)(f)`. + */ + def within[T](max : Duration)(f : => T) : T = within(0 seconds, max)(f) - /** - * Receive one message from the test actor and assert that it equals the - * given object. Wait time is bounded by the given duration, with an - * AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsg(max : Duration, obj : Any) : AnyRef = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsg") - assert (obj == o, "expected "+obj+", found "+o) - o + /** + * Same as `expectMsg`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsg(obj : Any) : AnyRef = expectMsg(remaining, obj) + + /** + * Receive one message from the test actor and assert that it equals the + * given object. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsg(max : Duration, obj : Any) : AnyRef = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsg") + assert (obj == o, "expected "+obj+", found "+o) + o + } + + /** + * Same as `expectMsg`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsg[T](f : PartialFunction[Any, T]) : T = expectMsg(remaining)(f) + + /** + * Receive one message from the test actor and assert that the given + * partial function accepts it. Wait time is bounded by the given duration, + * with an AssertionFailure being thrown in case of timeout. + * + * Use this variant to implement more complicated or conditional + * processing. + * + * @return the received object as transformed by the partial function + */ + def expectMsg[T](max : Duration)(f : PartialFunction[Any, T]) : T = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsg") + assert (f.isDefinedAt(o), "does not match: "+o) + f(o) + } + + /** + * Same as `expectMsgClass`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgClass[C](c : Class[C]) : C = expectMsgClass(remaining, c) + + /** + * Receive one message from the test actor and assert that it conforms to + * the given class. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsgClass[C](max : Duration, c : Class[C]) : C = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsgClass") + assert (c isInstance o, "expected "+c+", found "+o.getClass) + o.asInstanceOf[C] + } + + /** + * Same as `expectMsgAnyOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAnyOf(obj : Any*) : AnyRef = expectMsgAnyOf(remaining, obj : _*) + + /** + * Receive one message from the test actor and assert that it equals one of + * the given objects. Wait time is bounded by the given duration, with an + * AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsgAnyOf(max : Duration, obj : Any*) : AnyRef = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsgAnyOf") + assert (obj exists (_ == o), "found unexpected "+o) + o + } + + /** + * Same as `expectMsgAnyClassOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAnyClassOf(obj : Class[_]*) : AnyRef = expectMsgAnyClassOf(remaining, obj : _*) + + /** + * Receive one message from the test actor and assert that it conforms to + * one of the given classes. Wait time is bounded by the given duration, + * with an AssertionFailure being thrown in case of timeout. + * + * @return the received object + */ + def expectMsgAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { + val o = receiveOne(max) + assert (o ne null, "timeout during expectMsgAnyClassOf") + assert (obj exists (_ isInstance o), "found unexpected "+o) + o + } + + /** + * Same as `expectMsgAllOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAllOf(obj : Any*) { expectMsgAllOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of objects and assert that for each given object one is received + * which equals it. This construct is useful when the order in which the + * objects are received is not fixed. Wait time is bounded by the given + * duration, with an AssertionFailure being thrown in case of timeout. + * + *
+   * within(1 second) {
+   *   dispatcher ! SomeWork1()
+   *   dispatcher ! SomeWork2()
+   *   expectMsgAllOf(Result1(), Result2())
+   * }
+   * 
+ */ + def expectMsgAllOf(max : Duration, obj : Any*) { + val recv = receiveN(obj.size, now + max) + assert (obj forall (x => recv exists (x == _)), "not found all") + } + + /** + * Same as `expectMsgAllClassOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAllClassOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of classes and assert that for each given class one is received + * which is of that class (equality, not conformance). This construct is + * useful when the order in which the objects are received is not fixed. + * Wait time is bounded by the given duration, with an AssertionFailure + * being thrown in case of timeout. + */ + def expectMsgAllClassOf(max : Duration, obj : Class[_]*) { + val recv = receiveN(obj.size, now + max) + assert (obj forall (x => recv exists (_.getClass eq x)), "not found all") + } + + /** + * Same as `expectMsgAllConformingOf`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectMsgAllConformingOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } + + /** + * Receive a number of messages from the test actor matching the given + * number of classes and assert that for each given class one is received + * which conforms to that class. This construct is useful when the order in + * which the objects are received is not fixed. Wait time is bounded by + * the given duration, with an AssertionFailure being thrown in case of + * timeout. + * + * Beware that one object may satisfy all given class constraints, which + * may be counter-intuitive. + */ + def expectMsgAllConformingOf(max : Duration, obj : Class[_]*) { + val recv = receiveN(obj.size, now + max) + assert (obj forall (x => recv exists (x isInstance _)), "not found all") + } + + /** + * Same as `expectNoMsg`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def expectNoMsg { expectNoMsg(remaining) } + + /** + * Assert that no message is received for the specified time. + */ + def expectNoMsg(max : Duration) { + val o = receiveOne(max) + assert (o eq null, "received unexpected message "+o) + lastSoftTimeout = now + } + + /** + * Same as `receiveWhile`, but takes the maximum wait time from the innermost + * enclosing `within` block. + */ + def receiveWhile[T](f : PartialFunction[AnyRef, T]) : Seq[T] = receiveWhile(remaining)(f) + + /** + * Receive a series of messages as long as the given partial function + * accepts them or the idle timeout is met or the overall maximum duration + * is elapsed. Returns the sequence of messages. + * + * Beware that the maximum duration is not implicitly bounded by or taken + * from the innermost enclosing `within` block, as it is not an error to + * hit the `max` duration in this case. + * + * One possible use of this method is for testing whether messages of + * certain characteristics are generated at a certain rate: + * + *
+   * test ! ScheduleTicks(100 millis)
+   * val series = receiveWhile(750 millis) {
+   *     case Tick(count) => count
+   * }
+   * assert(series == (1 to 7).toList)
+   * 
+ */ + def receiveWhile[T](max : Duration)(f : PartialFunction[AnyRef, T]) : Seq[T] = { + val stop = now + max + + @tailrec def doit(acc : List[T]) : List[T] = { + receiveOne(stop - now) match { + case null => + acc.reverse + case o if (f isDefinedAt o) => + doit(f(o) :: acc) + case o => + queue.offerFirst(o) + acc.reverse + } } - /** - * Same as `expectMsg`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsg[T](f : PartialFunction[Any, T]) : T = expectMsg(remaining)(f) + val ret = doit(Nil) + lastSoftTimeout = now + ret + } - /** - * Receive one message from the test actor and assert that the given - * partial function accepts it. Wait time is bounded by the given duration, - * with an AssertionFailure being thrown in case of timeout. - * - * Use this variant to implement more complicated or conditional - * processing. - * - * @return the received object as transformed by the partial function - */ - def expectMsg[T](max : Duration)(f : PartialFunction[Any, T]) : T = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsg") - assert (f.isDefinedAt(o), "does not match: "+o) - f(o) + private def receiveN(n : Int, stop : Duration) : Seq[AnyRef] = { + for { x <- 1 to n } yield { + val timeout = stop - now + val o = receiveOne(timeout) + assert (o ne null, "timeout while expecting "+n+" messages") + o } + } - /** - * Same as `expectMsgClass`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgClass[C](c : Class[C]) : C = expectMsgClass(remaining, c) - - /** - * Receive one message from the test actor and assert that it conforms to - * the given class. Wait time is bounded by the given duration, with an - * AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsgClass[C](max : Duration, c : Class[C]) : C = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsgClass") - assert (c isInstance o, "expected "+c+", found "+o.getClass) - o.asInstanceOf[C] + private def receiveOne(max : Duration) : AnyRef = { + if (max == 0.seconds) { + queue.pollFirst + } else if (max.finite_?) { + queue.pollFirst(max.length, max.unit) + } else { + queue.takeFirst } + } - /** - * Same as `expectMsgAnyOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAnyOf(obj : Any*) : AnyRef = expectMsgAnyOf(remaining, obj : _*) - - /** - * Receive one message from the test actor and assert that it equals one of - * the given objects. Wait time is bounded by the given duration, with an - * AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsgAnyOf(max : Duration, obj : Any*) : AnyRef = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsgAnyOf") - assert (obj exists (_ == o), "found unexpected "+o) - o - } - - /** - * Same as `expectMsgAnyClassOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAnyClassOf(obj : Class[_]*) : AnyRef = expectMsgAnyClassOf(remaining, obj : _*) - - /** - * Receive one message from the test actor and assert that it conforms to - * one of the given classes. Wait time is bounded by the given duration, - * with an AssertionFailure being thrown in case of timeout. - * - * @return the received object - */ - def expectMsgAnyClassOf(max : Duration, obj : Class[_]*) : AnyRef = { - val o = receiveOne(max) - assert (o ne null, "timeout during expectMsgAnyClassOf") - assert (obj exists (_ isInstance o), "found unexpected "+o) - o - } - - /** - * Same as `expectMsgAllOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAllOf(obj : Any*) { expectMsgAllOf(remaining, obj : _*) } - - /** - * Receive a number of messages from the test actor matching the given - * number of objects and assert that for each given object one is received - * which equals it. This construct is useful when the order in which the - * objects are received is not fixed. Wait time is bounded by the given - * duration, with an AssertionFailure being thrown in case of timeout. - * - *
-     * within(1 second) {
-     *   dispatcher ! SomeWork1()
-     *   dispatcher ! SomeWork2()
-     *   expectMsgAllOf(Result1(), Result2())
-     * }
-     * 
- */ - def expectMsgAllOf(max : Duration, obj : Any*) { - val recv = receiveN(obj.size, now + max) - assert (obj forall (x => recv exists (x == _)), "not found all") - } - - /** - * Same as `expectMsgAllClassOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAllClassOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } - - /** - * Receive a number of messages from the test actor matching the given - * number of classes and assert that for each given class one is received - * which is of that class (equality, not conformance). This construct is - * useful when the order in which the objects are received is not fixed. - * Wait time is bounded by the given duration, with an AssertionFailure - * being thrown in case of timeout. - */ - def expectMsgAllClassOf(max : Duration, obj : Class[_]*) { - val recv = receiveN(obj.size, now + max) - assert (obj forall (x => recv exists (_.getClass eq x)), "not found all") - } - - /** - * Same as `expectMsgAllConformingOf`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectMsgAllConformingOf(obj : Class[_]*) { expectMsgAllClassOf(remaining, obj : _*) } - - /** - * Receive a number of messages from the test actor matching the given - * number of classes and assert that for each given class one is received - * which conforms to that class. This construct is useful when the order in - * which the objects are received is not fixed. Wait time is bounded by - * the given duration, with an AssertionFailure being thrown in case of - * timeout. - * - * Beware that one object may satisfy all given class constraints, which - * may be counter-intuitive. - */ - def expectMsgAllConformingOf(max : Duration, obj : Class[_]*) { - val recv = receiveN(obj.size, now + max) - assert (obj forall (x => recv exists (x isInstance _)), "not found all") - } - - /** - * Same as `expectNoMsg`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def expectNoMsg { expectNoMsg(remaining) } - - /** - * Assert that no message is received for the specified time. - */ - def expectNoMsg(max : Duration) { - val o = receiveOne(max) - assert (o eq null, "received unexpected message "+o) - lastSoftTimeout = now - } - - /** - * Same as `receiveWhile`, but takes the maximum wait time from the innermost - * enclosing `within` block. - */ - def receiveWhile[T](f : PartialFunction[AnyRef, T]) : Seq[T] = receiveWhile(remaining)(f) - - /** - * Receive a series of messages as long as the given partial function - * accepts them or the idle timeout is met or the overall maximum duration - * is elapsed. Returns the sequence of messages. - * - * Beware that the maximum duration is not implicitly bounded by or taken - * from the innermost enclosing `within` block, as it is not an error to - * hit the `max` duration in this case. - * - * One possible use of this method is for testing whether messages of - * certain characteristics are generated at a certain rate: - * - *
-     * test ! ScheduleTicks(100 millis)
-     * val series = receiveWhile(750 millis) {
-     *     case Tick(count) => count
-     * }
-     * assert(series == (1 to 7).toList)
-     * 
- */ - def receiveWhile[T](max : Duration)(f : PartialFunction[AnyRef, T]) : Seq[T] = { - val stop = now + max - - @tailrec def doit(acc : List[T]) : List[T] = { - receiveOne(stop - now) match { - case null => - acc.reverse - case o if (f isDefinedAt o) => - doit(f(o) :: acc) - case o => - queue.offerFirst(o) - acc.reverse - } - } - - val ret = doit(Nil) - lastSoftTimeout = now - ret - } - - private def receiveN(n : Int, stop : Duration) : Seq[AnyRef] = { - for { x <- 1 to n } yield { - val timeout = stop - now - val o = receiveOne(timeout) - assert (o ne null, "timeout while expecting "+n+" messages") - o - } - } - - private def receiveOne(max : Duration) : AnyRef = { - if (max == 0.seconds) { - queue.pollFirst - } else if (max.finite_?) { - queue.pollFirst(max.length, max.unit) - } else { - queue.takeFirst - } - } - - private def format(u : TimeUnit, d : Duration) = "%.3f %s".format(d.toUnit(u), u.toString.toLowerCase) + private def format(u : TimeUnit, d : Duration) = "%.3f %s".format(d.toUnit(u), u.toString.toLowerCase) } -// vim: set ts=4 sw=4 et: +// vim: set ts=2 sw=2 et: diff --git a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala index 77d8e82313..b13a61b82f 100644 --- a/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala +++ b/akka-actor/src/test/scala/akka/actor/actor/FSMTimingSpec.scala @@ -7,52 +7,52 @@ import org.scalatest.WordSpec import org.scalatest.matchers.MustMatchers class FSMTimingSpec - extends WordSpec - with MustMatchers - with TestKit { + extends WordSpec + with MustMatchers + with TestKit { - import FSMTimingSpec._ - import FSM._ + import FSMTimingSpec._ + import FSM._ - val fsm = Actor.actorOf(new StateMachine(testActor)).start - fsm ! SubscribeTransitionCallBack(testActor) - expectMsg(100 millis, CurrentState(fsm, Initial)) + val fsm = Actor.actorOf(new StateMachine(testActor)).start + fsm ! SubscribeTransitionCallBack(testActor) + expectMsg(100 millis, CurrentState(fsm, Initial)) - ignoreMsg { - case Transition(_, Initial, _) => true + ignoreMsg { + case Transition(_, Initial, _) => true + } + + "A Finite State Machine" must { + + "receive StateTimeout" in { + within (50 millis, 150 millis) { + fsm ! TestStateTimeout + expectMsg(Transition(fsm, TestStateTimeout, Initial)) + expectNoMsg + } } - "A Finite State Machine" must { + "receive single-shot timer" in { + within (50 millis, 150 millis) { + fsm ! TestSingleTimer + expectMsg(Tick) + expectMsg(Transition(fsm, TestSingleTimer, Initial)) + expectNoMsg + } + } - "receive StateTimeout" in { - within (50 millis, 150 millis) { - fsm ! TestStateTimeout - expectMsg(Transition(fsm, TestStateTimeout, Initial)) - expectNoMsg - } - } - - "receive single-shot timer" in { - within (50 millis, 150 millis) { - fsm ! TestSingleTimer - expectMsg(Tick) - expectMsg(Transition(fsm, TestSingleTimer, Initial)) - expectNoMsg - } - } - - "receive and cancel a repeated timer" in { - fsm ! TestRepeatedTimer - val seq = receiveWhile(550 millis) { - case Tick => Tick - } - seq must have length (5) - within(250 millis) { - fsm ! Cancel - expectMsg(Transition(fsm, TestRepeatedTimer, Initial)) - expectNoMsg - } - } + "receive and cancel a repeated timer" in { + fsm ! TestRepeatedTimer + val seq = receiveWhile(550 millis) { + case Tick => Tick + } + seq must have length (5) + within(250 millis) { + fsm ! Cancel + expectMsg(Transition(fsm, TestRepeatedTimer, Initial)) + expectNoMsg + } + } "notify unhandled messages" in { fsm ! TestUnhandled diff --git a/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala index e9d54cccbe..8c232255d0 100644 --- a/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala +++ b/akka-samples/akka-sample-fsm/src/main/scala/Buncher.scala @@ -15,61 +15,61 @@ import akka.actor.{FSM, Actor} * possibly logged). */ object Buncher { - trait State - case object Idle extends State - case object Active extends State + trait State + case object Idle extends State + case object Active extends State - case object Flush // send out current queue immediately - case object Stop // poison pill + case object Flush // send out current queue immediately + case object Stop // poison pill - case class Data[A](start : Long, xs : List[A]) - - def apply[A : Manifest](singleTimeout : Duration, - multiTimeout : Duration)(f : List[A] => Unit) = - Actor.actorOf(new Buncher[A](singleTimeout, multiTimeout).deliver(f)) + case class Data[A](start : Long, xs : List[A]) + + def apply[A : Manifest](singleTimeout : Duration, + multiTimeout : Duration)(f : List[A] => Unit) = + Actor.actorOf(new Buncher[A](singleTimeout, multiTimeout).deliver(f)) } - + class Buncher[A : Manifest] private (val singleTimeout : Duration, val multiTimeout : Duration) - extends Actor with FSM[Buncher.State, Buncher.Data[A]] { - import Buncher._ - import FSM._ - - private val manifestA = manifest[A] - - private var send : List[A] => Unit = _ - private def deliver(f : List[A] => Unit) = { send = f; this } - - private def now = System.currentTimeMillis - private def check(m : AnyRef) = ClassManifest.fromClass(m.getClass) <:< manifestA + extends Actor with FSM[Buncher.State, Buncher.Data[A]] { + import Buncher._ + import FSM._ + + private val manifestA = manifest[A] + + private var send : List[A] => Unit = _ + private def deliver(f : List[A] => Unit) = { send = f; this } + + private def now = System.currentTimeMillis + private def check(m : AnyRef) = ClassManifest.fromClass(m.getClass) <:< manifestA - startWith(Idle, Data(0, Nil)) - - when(Idle) { - case Event(m : AnyRef, _) if check(m) => - goto(Active) using Data(now, m.asInstanceOf[A] :: Nil) - case Event(Flush, _) => stay - case Event(Stop, _) => stop - } - - when(Active, stateTimeout = Some(singleTimeout)) { - case Event(m : AnyRef, Data(start, xs)) if check(m) => - val l = m.asInstanceOf[A] :: xs - if (now - start > multiTimeout.toMillis) { - send(l.reverse) - goto(Idle) using Data(0, Nil) - } else { - stay using Data(start, l) - } - case Event(StateTimeout, Data(_, xs)) => - send(xs.reverse) - goto(Idle) using Data(0, Nil) - case Event(Flush, Data(_, xs)) => - send(xs.reverse) - goto(Idle) using Data(0, Nil) - case Event(Stop, Data(_, xs)) => - send(xs.reverse) - stop - } - - initialize + startWith(Idle, Data(0, Nil)) + + when(Idle) { + case Event(m : AnyRef, _) if check(m) => + goto(Active) using Data(now, m.asInstanceOf[A] :: Nil) + case Event(Flush, _) => stay + case Event(Stop, _) => stop + } + + when(Active, stateTimeout = Some(singleTimeout)) { + case Event(m : AnyRef, Data(start, xs)) if check(m) => + val l = m.asInstanceOf[A] :: xs + if (now - start > multiTimeout.toMillis) { + send(l.reverse) + goto(Idle) using Data(0, Nil) + } else { + stay using Data(start, l) + } + case Event(StateTimeout, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Flush, Data(_, xs)) => + send(xs.reverse) + goto(Idle) using Data(0, Nil) + case Event(Stop, Data(_, xs)) => + send(xs.reverse) + stop + } + + initialize }