Play Scala Akka WebSockets change actor path

2019-05-15 03:57发布

问题:

I've followed the example for creating Web Sockets with Scala Play and Akka actors:

https://www.playframework.com/documentation/2.5.x/ScalaWebSockets#Handling-WebSockets-with-Akka-Streams-and-actors

On resume, the controller:

import play.api.mvc._
import play.api.libs.streams._

class Controller1 @Inject() (implicit system: ActorSystem, materializer: Materializer) {

  def socket = WebSocket.accept[String, String] { request =>
    ActorFlow.actorRef(out => MyWebSocketActor.props(out))
  }

And the Actor:

import akka.actor._

object MyWebSocketActor {
  def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}

class MyWebSocketActor(out: ActorRef) extends Actor {
  def receive = {
    case msg: String =>
      out ! ("I received your message: " + msg)
  }
}

The actors created (one per websocket connection) are child of /user actor. I've created 3 connections and the actor created were:

  1. /user/$b
  2. /user/$c
  3. /user/$d

I want to change the actors' name based in a field of the web socket message. How could i do this?.

回答1:

You can set the name of the actor as follows:

  1. Create a file BetterActorFlow.scala

    package your.package
    
    import akka.actor._
    import akka.stream.scaladsl.{Keep, Sink, Source, Flow}
    import akka.stream.{Materializer, OverflowStrategy}
    
    object BetterActorFlow {
    
      def actorRef[In, Out](props: ActorRef => Props, bufferSize: Int = 16, overflowStrategy: OverflowStrategy = OverflowStrategy.dropNew, maybeName: Option[String] = None)(implicit factory: ActorRefFactory, mat: Materializer): Flow[In, Out, _] = {
    
        val (outActor, publisher) = Source.actorRef[Out](bufferSize, overflowStrategy)
                            .toMat(Sink.asPublisher(false))(Keep.both).run()
    
        def flowActorProps: Props = {
          Props(new Actor {
            val flowActor = context.watch(context.actorOf(props(outActor), "flowActor"))
    
            def receive = {
              case Status.Success(_) | Status.Failure(_) => flowActor ! PoisonPill
              case Terminated(_) => context.stop(self)
              case other => flowActor ! other
            }
    
            override def supervisorStrategy = OneForOneStrategy() { case _ => SupervisorStrategy.Stop }
          })
        }
    
        def actorRefForSink =
          maybeName.fold(factory.actorOf(flowActorProps)) { name => factory.actorOf(flowActorProps, name) }
    
        Flow.fromSinkAndSource(Sink.actorRef(actorRefForSink, Status.Success(())), Source.fromPublisher(publisher))
    
      }
    }
    
  2. Use BetterActorFlow instead of ActorFlow:

    BetterActorFlow.actorRef(out =>
      ChatActor.props(out), 16, OverflowStrategy.dropNew, Some("alicebob"))
    

This worked for me. The created actor is at user/alicebob (use this with context.system.actorSelection("user/alicebob"))



回答2:

According to the source code of ActorFlow, it is currently not possible to thange the name of the actual actor spawned for a connection (line 38):

Sink.actorRef(factory.actorOf(Props(new Actor { ... }) /*, name parameter is omitted */)

However, ActorFlow.actorRef accepts an implicit ActorRefFactory, which is implicit system: ActorSystem in all cases in your code. As we know, there are 2 most common ActorRefFactories: ActorSystem and ActorContext. You can modify your code in such a way that each time a connection is accepted another dummy actor would spawn with your preferred name (e.g. myActor1), and pass this new actor's context to ActorFlow.actorRef instead. In return, actors for connections would be named as follows:

  • /user/myActor1/$a
  • /user/myActor2/$a
  • etc