I have to fill in template to implement timeout on a shopping cart. So far I have:
import akka.actor.{Actor, ActorRef, Cancellable, Props, Timers}
import akka.event.{Logging, LoggingReceive}
import scala.concurrent.duration._
import scala.language.postfixOps
object CartActor {
sealed trait Command
case class AddItem(item: Any) extends Command
case class RemoveItem(item: Any) extends Command
case object ExpireCart extends Command
case object StartCheckout extends Command
case object ConfirmCheckoutCancelled extends Command
case object ConfirmCheckoutClosed extends Command
sealed trait Event
case class CheckoutStarted(checkoutRef: ActorRef) extends Event
def props = Props(new CartActor())
}
class CartActor extends Actor with Timers {
import context._
import CartActor._
private val log = Logging(context.system, this)
val cartTimerDuration: FiniteDuration = 5 seconds
var cart: Cart = Cart.empty
private def scheduleTimer: Cancellable =
system.scheduler.scheduleOnce(cartTimerDuration, self, ExpireCart)
def receive: Receive = empty
def empty: Receive = {
case AddItem(item) =>
this.cart = cart.addItem(item)
scheduleTimer
context become nonEmpty(cart, scheduleTimer)
case _ =>
}
def nonEmpty(cart: Cart, timer: Cancellable): Receive = {
case AddItem(item) =>
this.cart = cart.addItem(item)
timer.cancel()
scheduleTimer
case RemoveItem(item) =>
this.cart = this.cart.removeItem(item)
if (this.cart.size != 0) {
timer.cancel()
scheduleTimer
}
else
context become empty
case StartCheckout =>
context become inCheckout(this.cart)
case ExpireCart =>
this.cart = Cart.empty
println("Cart expired")
context become empty
}
def inCheckout(cart: Cart): Receive = {
case ConfirmCheckoutCancelled =>
context become nonEmpty(cart, scheduleTimer)
case ConfirmCheckoutClosed =>
println("Cart closed after checkout")
context become empty
case _ =>
}
}
Method signatures were provided, so e.g. I can't change def nonEmpty(cart: Cart, timer: Cancellable)
. While adding or removing item, the timer should be reset, so user has again 5 seconds to do something. The problem is I have no idea how to do it properly - the method above clearly does not reset the timer, as it always timeouts after 5 seconds.
How can I achieve that? Should I use timers instead of scheduler, e.g. timers.startSingleTimer("ExpireCart", ExpireCart, cartTimerDuration)
? How should I pass this between methods? Should a timer be an attribute of the CartActor instead and I should ignore the scheduler? On a side note, when I have def nonEmpty(cart: Cart, timer: Cancellable)
, is the timer called anywhere implicitly, or just passed?
There are two problems.
Firstly, in empty
you are starting two timers:
scheduleTimer // Creates a timer, reference lost so can't be cancelled
context become nonEmpty(cart, scheduleTimer) // Creates a second timer
More generally, you are using both mutable state and parameters to the receive method. The mutable state is not required, so delete this line:
var cart: Cart = Cart.empty
Now fix nonEmpty
to pass the updated state to the new receive
method rather than using this
:
def nonEmpty(cart: Cart, timer: Cancellable): Receive = {
case AddItem(item) =>
timer.cancel()
context become nonEmpty(cart.addItem(item), scheduleTimer)
case RemoveItem(item) =>
timer.cancel()
if (this.cart.size > 1) {
context become nonEmpty(cart.remoteItem(item), scheduleTimer)
} else {
context become empty
}
case StartCheckout =>
timer.cancel()
context become inCheckout(cart)
case ExpireCart =>
timer.cancel() // Just in case :)
println("Cart expired")
context become empty
}