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

xitrum.sockjs.NonWebSocketSession.scala Maven / Gradle / Ivy

There is a newer version: 3.28.18
Show newest version
package xitrum.sockjs

import scala.collection.mutable.ArrayBuffer

import akka.actor.{Actor, ActorRef, ReceiveTimeout, Terminated}
import scala.concurrent.duration._

import xitrum.{Action, Config, SockJsText}

// There are 2 kinds of non-WebSocket client: receiver and sender
// receiver/sender client <-> NonWebSocketSessionActor <-> SockJsAction
// (See SockJsActions.scala)
//
// For WebSocket:
// receiver/sender client <-> SockJsAction

case object SubscribeFromReceiverClient
case object AbortFromReceiverClient

case class  MessagesFromSenderClient(messages: Seq[String])

case class  MessageFromHandler(index: Int, message: String)
case class  CloseFromHandler(index: Int)

case object SubscribeResultToReceiverClientAnotherConnectionStillOpen
case object SubscribeResultToReceiverClientClosed
case class  SubscribeResultToReceiverClientMessages(messages: Seq[String])
case object SubscribeResultToReceiverClientWaitForMessage

case class  NotificationToReceiverClientMessage(index: Int, message: String, handler: ActorRef)
case class  NotificationToReceiverClientClosed(index: Int, handler: ActorRef)
case object NotificationToReceiverClientHeartbeat

case class  NotificationToHandlerChannelCloseSuccess(index: Int)
case class  NotificationToHandlerChannelCloseFailure(index: Int)
case class  NotificationToHandlerChannelWriteSuccess(index: Int)
case class  NotificationToHandlerChannelWriteFailure(index: Int)

object NonWebSocketSession {
  // The session must time out after 5 seconds of not having a receiving connection
  // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-46
  private val TIMEOUT_CONNECTION = 5.seconds

  private val TIMEOUT_CONNECTION_MILLIS = TIMEOUT_CONNECTION.toMillis
}

/**
 * There should be at most one subscriber:
 * http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html
 *
 * To avoid out of memory, the actor is stopped when there's no subscriber
 * for a long time. Timeout is also used to check if there's no message
 * for subscriber for a long time.
 * See TIMEOUT_CONNECTION and TIMEOUT_HEARTBEAT in NonWebSocketSessions.
 */
class NonWebSocketSession(var receiverCliento: Option[ActorRef], pathPrefix: String, action: Action) extends Actor {
  import NonWebSocketSession._

  private[this] var sockJsActorRef: ActorRef = _

  // Messages from handler to client are buffered here
  private[this] val bufferForClientSubscriber = ArrayBuffer.empty[String]

  // ReceiveTimeout may not occurred if there's frequent Publish, thus we
  // need to manually check if there's no subscriber for a long time.
  // lastSubscribedAt must be Long to avoid Integer overflow, beacuse
  // System.currentTimeMillis() is used.
  private[this] var lastSubscribedAt = 0L

  // Until the timeout occurs, the server must constantly serve the close message
  private[this] var closed = false

  override def preStart() {
    // Attach sockJsActorRef to the current actor, so that sockJsActorRef is
    // automatically stopped when the current actor stops
    sockJsActorRef = Config.routes.sockJsRouteMap.createSockJsAction(context, pathPrefix)
    context.watch(sockJsActorRef)
    sockJsActorRef ! (self, action)

    lastSubscribedAt = System.currentTimeMillis()

    // At start (see constructor), there must be a receiver client
    val receiverClient = receiverCliento.get

    // Unsubscribed when stopped
    context.watch(receiverClient)

    // Will be set to TIMEOUT_CONNECTION when the receiver client stops
    context.setReceiveTimeout(SockJsAction.TIMEOUT_HEARTBEAT)
  }

  private def unwatchAndStop() {
    receiverCliento.foreach(context.unwatch)
    context.unwatch(sockJsActorRef)
    context.stop(sockJsActorRef)
    context.stop(self)
  }

  def receive = {
    // When non-WebSocket receiverClient stops normally after sending data to
    // browser, we need to wait for TIMEOUT_CONNECTION amount of time for the
    // to reconnect. Non-streaming client disconnects everytime. Note that for
    // browser to do garbage collection, streaming client also disconnects after
    // sending a large amount of data (4KB in test mode).
    //
    // See also AbortFromReceiverClient below.
    case Terminated(monitored) =>
      if (monitored == sockJsActorRef && !closed) {
        // See CloseFromHandler
        unwatchAndStop()
      } else if (receiverCliento == Some(monitored)) {  // Scala 2.10 doesn't have Option#contains
        context.unwatch(monitored)
        receiverCliento = None
        context.setReceiveTimeout(TIMEOUT_CONNECTION)
      }

    // Similar to Terminated but no TIMEOUT_CONNECTION is needed
    case AbortFromReceiverClient =>
      unwatchAndStop()

    case SubscribeFromReceiverClient =>
      val s = sender()
      if (closed) {
        s ! SubscribeResultToReceiverClientClosed
      } else {
        lastSubscribedAt = System.currentTimeMillis()
        if (receiverCliento.isEmpty) {
          receiverCliento = Some(s)
          context.watch(s)
          context.setReceiveTimeout(SockJsAction.TIMEOUT_HEARTBEAT)

          if (bufferForClientSubscriber.isEmpty) {
            s ! SubscribeResultToReceiverClientWaitForMessage
          } else {
            s ! SubscribeResultToReceiverClientMessages(bufferForClientSubscriber.toList)
            bufferForClientSubscriber.clear()
          }
        } else {
          s ! SubscribeResultToReceiverClientAnotherConnectionStillOpen
        }
      }

    case CloseFromHandler(index) =>
      // Until the timeout occurs, the server must serve the close message
      closed = true
      receiverCliento.foreach { receiverClient =>
        receiverClient ! NotificationToReceiverClientClosed(index, sockJsActorRef)
        context.unwatch(receiverClient)
        receiverCliento = None
        context.setReceiveTimeout(TIMEOUT_CONNECTION)
      }

    case MessagesFromSenderClient(messages) =>
      if (!closed) messages.foreach { msg => sockJsActorRef ! SockJsText(msg) }

    case MessageFromHandler(index, message) =>
      if (!closed) {
        receiverCliento match {
          case None =>
            // Stop if there's no subscriber for a long time
            val now = System.currentTimeMillis()
            if (now - lastSubscribedAt > TIMEOUT_CONNECTION_MILLIS)
              unwatchAndStop()
            else
              bufferForClientSubscriber.append(message)

          case Some(receiverClient) =>
            // buffer is empty at this moment, because receiverCliento is not empty
            receiverClient ! NotificationToReceiverClientMessage(index, message, sockJsActorRef)
        }
      }

    case ReceiveTimeout =>
      if (closed || receiverCliento.isEmpty) {
        // Closed or no subscriber for a long time
        unwatchAndStop()
      } else {
        // No message for subscriber for a long time
        receiverCliento.get ! NotificationToReceiverClientHeartbeat
      }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy