All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.kodeworks.kvarg.actor.CometActor.scala Maven / Gradle / Ivy

There is a newer version: 0.7
Show newest version
package no.kodeworks.kvarg.actor

import java.util.UUID

import akka.actor.{Actor, ActorRef, FSM}
import no.kodeworks.kvarg.actor.CometActor._
import no.kodeworks.kvarg.model.Page
import no.kodeworks.kvarg.patch.Patch
import io.circe.{Encoder, Json, JsonObject}
import io.circe.syntax._

import scala.concurrent.duration._
import scala.language.postfixOps

class CometActor[Response](
                            burstTimeout: Long,
                            gateTimeout: Long,
                            listenerTimeout: Long,
                            reconnectTimeout: Long,
                            useAck: Boolean = true
                          ) extends Actor with FSM[State, Data[Response]] {
  require(0L < burstTimeout, "burst timeout must be positive")
  require(burstTimeout < gateTimeout, "gate timeout must be greater than burst timeout")
  require(gateTimeout < listenerTimeout, "listener timeout must be greater than gate timeout")

  override def preStart {
    log.info("{} start", self.path.name)
    super.preStart()
  }

  override def postStop {
    log.info("{} stop", self.path.name)
    super.postStop()
  }

  startWith(Disconnected, Data[Response]())

  def awaitReconnect: StateFunction = {
    case Event(ReconnectTimeout, _) => {
      log.debug("[{}] reconnect timeout, wipe unacked, wipe unsent, goto Disconnected", stateName)
      goto(Disconnected) using Data[Response]()
    }
  }

  when(Disconnected) {
    case Event(CometRequest(_), data) => {
      log.debug("[{}] cometrequest, do listener, goto Ready", stateName)
      doListener(None)
      goto(Ready) using data.copy(listener = Some(sender))
    }
    case _ => stay()
  }

  when(Waiting)(awaitReconnect orElse {
    case Event(CometRequest(_), data@Data(_ :: _, _, listener)) => {
      log.debug("[{}] cometrequest, got unsent, do listener, cancel reconnecttimer, goto Gated", stateName)
      doListener(listener)
      cancelTimer(reconnectTimer)
      gotoGate() using data.copy(listener = Some(sender))
    }
    case Event(CometRequest(_), data@Data(_, _, listener)) => {
      log.debug("[{}] cometrequest, nothing to send, do listener, cancel reconnecttimer, goto Ready", stateName)
      doListener(listener)
      cancelTimer(reconnectTimer)
      goto(Ready) using data.copy(listener = Some(sender))
    }
    case Event(response: CometSend[Response], data@Data(unsent, _, _)) => {
      log.debug("[{}] response, sendLater, stay", stateName)
      stay() using data.copy(unsent = response :: unsent)
    }
  })

  when(Ready) {
    case Event(ListenerTimeout, Data(_, _, listener)) => {
      log.debug("[{}] listener timeout, empty response, set reconnecttimer, goto Waiting", stateName)
      listener.foreach(_ ! CometEmptyResponse)
      setTimer(reconnectTimer, ReconnectTimeout, reconnectTimeout millis)
      goto(Waiting)
    }
    case Event(CometRequest(_), data@Data(_, _, listener)) => {
      log.debug("[{}] cometrequest, do listener, stay", stateName)
      doListener(listener)
      stay() using data.copy(listener = Some(sender))
    }
    case Event(response: CometSend[Response], data@Data(unsent, _, _)) => {
      log.debug("[{}] response, send later, set bursttimer, set gatetimer, goto Gated", stateName)
      gotoGate() using data.copy(unsent = response :: unsent)
    }
  }

  when(Gated) {
    case Event(CometRequest(_), data@Data(_, _, listener)) => {
      log.debug("[{}] cometrequest, do listener, stay", stateName)
      doListener(listener)
      stay() using data.copy(listener = Some(sender))
    }
    case Event(BurstTimeout, data) => {
      log.debug("[{}] burst timeout, stop gatetimer, send, goto sending", stateName)
      cancelTimer(gateTimer)
      send(data)
    }
    case Event(GateTimeout, data) => {
      log.debug("[{}] gate timeout, stop bursttimer, send, goto sending", stateName)
      cancelTimer(burstTimer)
      send(data)
    }
    case Event(ListenerTimeout, data) => {
      log.debug("[{}] listener timeout, stop bursttimer and gatetimer, send, goto sending", stateName)
      cancelTimer(burstTimer)
      cancelTimer(gateTimer)
      send(data)
    }
    case Event(response: CometSend[Response], data@Data(unsent, _, _)) => {
      log.debug("[{}] response, renew bursttimer, stay", stateName)
      setTimer(burstTimer, BurstTimeout, burstTimeout millis)
      stay() using data.copy(response :: unsent)
    }
  }

  when(Sending)(awaitReconnect orElse {
    case Event(CometRequest(Some(ack)), Data(_, Some(resendable@CometResponse(_, unack)), _))
      if useAck && ack != unack => {
      log.debug("[{}] cometrequest, ack mismatch, resending, reset reconnecttimer, stay", stateName)
      sender ! resendable
      setTimer(reconnectTimer, ReconnectTimeout, reconnectTimeout millis)
      stay()
    }
    case Event(CometRequest(None), Data(_, Some(resendable), _))
      if useAck => {
      log.debug("[{}] cometrequest, no ack, resending, reset reconnecttimer, stay", stateName)
      sender ! resendable
      setTimer(reconnectTimer, ReconnectTimeout, reconnectTimeout millis)
      stay()
    }
    case Event(CometRequest(None), data@Data(_ :: _, Some(CometResponse(_, _)), _))
      if !useAck => {
      log.debug("[{}] cometrequest, got unsent, cancel reconnecttimer, goto Gated", stateName)
      setTimer(listenerTimer, ListenerTimeout, listenerTimeout millis)
      cancelTimer(reconnectTimer)
      gotoGate() using data.copy(listener = Some(sender))
    }
    case Event(CometRequest(Some(ack)), data@Data(_ :: _, Some(CometResponse(_, unack)), _))
      if !useAck || ack == unack => {
      log.debug("[{}] cometrequest, ack match, got unsent, cancel reconnecttimer, goto Gated", stateName)
      setTimer(listenerTimer, ListenerTimeout, listenerTimeout millis)
      cancelTimer(reconnectTimer)
      gotoGate() using data.copy(listener = Some(sender))
    }
    case Event(CometRequest(None), data@Data(_, Some(CometResponse(_, _)), _))
      if !useAck => {
      log.debug("[{}] cometrequest, nothing to send, cancel reconnecttimer, goto Ready", stateName)
      setTimer(listenerTimer, ListenerTimeout, listenerTimeout millis)
      cancelTimer(reconnectTimer)
      goto(Ready) using data.copy(listener = Some(sender))
    }
    case Event(CometRequest(Some(ack)), data@Data(_, Some(CometResponse(_, unack)), _))
      if !useAck || ack == unack => {
      log.debug("[{}] cometrequest, ack match, nothing to send, cancel reconnecttimer, goto Ready", stateName)
      setTimer(listenerTimer, ListenerTimeout, listenerTimeout millis)
      cancelTimer(reconnectTimer)
      goto(Ready) using data.copy(listener = Some(sender))
    }
    case Event(response: CometSend[Response], data@Data(unsent, _, _)) => {
      log.debug("[{}] response, sending later, stay", stateName)
      stay() using data.copy(unsent = response :: unsent)
    }
  })

  onTransition {
    case from -> to => log.debug("[{}] -> [{}]", from, to)
  }

  private def doListener(listener: Option[ActorRef]) {
    //listener.foreach(_ ! CometEmptyResponse)
    setTimer(listenerTimer, ListenerTimeout, listenerTimeout millis)
  }

  private def gotoGate() = {
    setTimer(burstTimer, BurstTimeout, burstTimeout millis)
    setTimer(gateTimer, GateTimeout, gateTimeout millis)
    goto(Gated)
  }

  private def send(data: Data[Response]) = {
    val sendable = CometResponse(data.unsent.reverse)
    data.listener foreach (_ ! sendable)
    cancelTimer(listenerTimer)
    setTimer(reconnectTimer, ReconnectTimeout, reconnectTimeout millis)
    goto(Sending) using Data[Response](unacked = Some(sendable))
  }

  initialize()
}

object CometActor {
  val ack = "ack"
  val burstTimer = "burstTimer"
  val gateTimer = "gateTimer"
  val listenerTimer = "listenerTimer"
  val reconnectTimer = "reconnectTimer"

  private val nameFormat = "comet_%s"

  def name(id: String = UUID.randomUUID.toString) = String.format(nameFormat, id)

  object BurstTimeout

  object GateTimeout

  object ListenerTimeout

  object ReconnectTimeout


  sealed trait State

  case object Disconnected extends State

  case object Waiting extends State

  case object Gated extends State

  case object Ready extends State

  case object Sending extends State

  case class Data[Response](
                             unsent: List[CometSend[Response]] = Nil,
                             unacked: Option[CometResponse[Response]] = None,
                             listener: Option[ActorRef] = None)

  case class CometSend[Response](
                                  typeName: String,
                                  response: Patch[Response],
                                  page:Option[Page]
                                )

  case class CometRequest(ack: Option[String] = None)

  case class CometResponse[Response](
                                      events: List[CometSend[Response]],
                                      ack: String = UUID.randomUUID().toString)

  val CometEmptyResponse = CometResponse[Nothing](Nil, "")

  object codec {
    def cometResponseEncoder(
                              encoders: Map[String, Encoder[Patch[_ <: Any]]]
                            ): Encoder[CometResponse[Any]] =
      (cr: CometResponse[Any]) =>
        JsonObject(
          "events" -> cr.events.flatMap {
            case CometSend(typeName, response, _) =>
              encoders.get(typeName).map {
                patchEncoder =>
                  Json.arr(
                    typeName.asJson,
                    Json.obj(typeName -> patchEncoder(response))
                  )
              }
          }.asJson,
          "ack" -> cr.ack.asJson
        ).asJson
  }

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy