Search code examples
scalawebsocketplayframework

Play Framework Persistent WebSocket Connection


In the current version of the Play Framework, there is no way to have the WebSocket connection to be persistent.

https://www.playframework.com/documentation/2.8.x/ScalaWebSockets#Keeping-a-WebSocket-Alive

I have the following piece of code and the need for this WebSocket connection to be persistent.

class ProvisioningActor(sink: ActorRef) extends Actor {

    private[this] val source = Observable.interval(appConfig.pingInterval).map(elem => elem.toInt)
    private[this] val ping = Consumer.foreach[Int](x => self ! x)
    private[this] val task = source.consumeWith(ping).runToFuture

    override def receive: Receive = {
      case jsValue: JsValue =>
        logger.debug(s"Received OCPPCallRequest: \n ${Json.prettyPrint(jsValue)}")
        jsValue.validate[OCPPCallRequest].asEither match {
          case Right(ocppCall) => handleOCPPCallRequest(ocppCall).materialize.map {
            case Failure(fail) => sink ! JsError(s"${fail.getMessage}")
            case Success(succ) => sink ! Json.toJson(succ)
          }
          case Left(errors) =>
            logger.error(s"Errors occurred when validating OCPPCallRequest: \n $errors")
            sink ! Json.toJson(s"error -> ${errors.head._2}") // TODO: Work on this issue here on how we want to propagate errors
        }
      case x: Int =>
        logger.debug(s"Elem: $x")
        handleHeartBeatRequest(2, "HeartbeatRequest").materialize.map {
          case Failure(fail) => sink ! JsError(s"${fail.getMessage}")
          case Success(succ) => sink ! Json.toJson(succ)
        }
      case msg: Any =>
        logger.warn(s"Received unknown message ${msg.getClass.getTypeName} that cannot be handled, " +
          s"eagerly closing websocket connection")
        task.cancel()
        self ! PoisonPill
    }
  }

It kind of works be sending a heartbeat message back to the client. My question is:

  1. Is this good enough for an implementation?
  2. By default all WebSocket connections will be persistent and this may not be desired. So this has to be on a per connection basis. Correct?

Is there any other way that is advisable?


Solution

  • We use PlayFramework websockets for long running sessions, a busy server supports more than 1000 concurrent Websocket connections and lack of ping-pong packets causes idle websocket connections to be terminated by intermediate firewalls, proxies etc and also the play framework idleTimeout itself - play.server.https.idleTimeout.

    Form PlayFramework v2.1(now with v2.8) we have been using Sockjs protocol, Play Sockjs - https://github.com/fdimuccio/play2-sockjs which uses a application layer heartbeat https://github.com/fdimuccio/play2-sockjs/wiki/API-reference-for-0.5.x#configuring-sockjs-handler

    package controllers
    
    import scala.concurrent.duration._
    
    import play.api.mvc._
    import play.sockjs.api._
    
    // mixin SockJSRouter trait with your controller
    class SockJSController extends Controller with SockJSRouter {
    
      // override this method to specify custom SockJSSettings
      override protected val settings = SockJSSettings(websocket = false, heartbeat = 55 seconds)
    
      // here goes the request handler
      def sockjs = SockJS.accept[String, String] { request =>
        ...
      }
    
    }
    

    We use 20s heartbeat in production which has proven very safe, each connection has the same heartbeat setting, which works well for our usecase.

    This topic may be helpful: Play2.5 Java WebSockets