Search code examples
scalaplayframeworkdependency-injectionakkaguice-3

Using a WebService with Akka Actors and the play framework


I have built a web service using Play & Akka and now need to integrate another Webservice, where my web service is a client.

My default controller (with an associated routes file) looks like

class myController @Inject() (implicit val messagesApi: MessagesApi, 
    config: play.api.Configuration) extends Controller with I18nSupport  {
// Actions
}

This spins up a large actor system and everything is good.

One of the actors is defined as below -

class ActorMgr  ( jobId: Long, 
    config: Config) extends Actor  with ActorLogging {
// Actor specific stuff
}

My problem is that I now need to call a new web service from this actor. This web service is a database that will log the results from this actor.

I have seen and followed instructions from (among others)

  1. https://playframework.com/documentation/2.5.x/ScalaWS
  2. Dependency injection with abstract class and object in Play Framework 2.5

As per the instructions above, i am supposed to inject WSClient into a class where i need to access it.

I am able to solve the dependency injection into a second controller as below

class DbController @Inject() (ws: WSClient) extends Controller {  
  def post = Action { 
         // access webservice
  }
}

This works, and i can execute the "post" action, by accessing the URL that it is mapped to in the routes file, and therefore access the web service. I also now have two controllers.

My problem is to access the web service controller "post" method from ActorMgr (an Akka Actor). How do i enable that ?


Solution

  • After a lot of research, i wanted to update my findings here. Though i was able to solve my specific problem as below, there is a lot more to be said here.

    My specific solution first -

    Instead of DbController, I wraped my service as below and injected it where required -

    trait Db {
      def post
    }
    
    class InfluxDb @Inject() (ws: WSClient) extends Db  {
      val logger = LoggerFactory.getLogger(classOf[InfluxDb])
      logger.info("InfluxDb: Initiatlized")     
    
      def post = { 
    
    
        val req = ws.url("http://localhost:9086/write")
                    .withQueryString("db" -> "db1")
                    .withHeaders("Content-Type" -> "application/json")
                    .post("job_id,command=PUT value=99")
    
        logger.debug("InfluxDb: Post")     
        }
    }
    

    Having said that, injecting stuff gave me a ton of problems. I finally realized that there are a few distinct use cases here -

    1. Using Akka & Guice and not using the Playframework
    2. Using the Playframework + Akka + Guice and injecting a top level actor
    3. Using the Playframework + Akka + Guice and injecting child actors
    4. Using the playframework + Akka + Guice BUT creating not "injecting" your top level actor & Actor System.

    Here is how you would solve each of the above.

    1. For (1) - Refer to guice akka tutorial
    2. For (2) & (3) - Refer to Playframework Documentation
    3. For (4) This is a little more tricky

    You will need to extend "IndirectActorProducer" and then use that for creating your ActorRef. The problem is "Props" does not know how to interface with Guice. This is also part of the solution in (1)

    The below sample code shows all 4 use cases, and compiles. In the code below

    ParentActor - Refers to use case (2) above, ChildActor to use case (3), and ParentActor_2 & ChildActor_2 to use case (4).

     // play imports
    import play.api.mvc._
    import play.api.Logger
    import play.api.mvc.Results
    
    // actor imports
    import akka.actor.{Actor, ActorSystem, ActorRef,   Props, IndirectActorProducer}
    
    // DI imports
    import com.google.inject.{Injector, AbstractModule, Key, Provides}
    import javax.inject._
    import com.google.inject.assistedinject.Assisted
    import play.libs.akka.AkkaGuiceSupport
    import play.api.libs.concurrent.InjectedActorSupport
    
    
    class MainCntrlr @Inject() (injector : Injector, 
                                @Named("PActor") pa: ActorRef,
                                cfP: ParentActor_2.Factory) 
                                extends Controller {
      Logger.debug("MainCntrlr: created")    
    
      val pa_2 = ActorSystem("test")
                   .actorOf(Props(classOf[GuiceActorProducer], injector, "PActor_2"), "PA_2")  
    
      pa   ! 12               
      pa_2 ! 100
    
      def index          = Action {  Ok (views.html.index.render()) }
    }
    
    
    class ParentActor @Inject() (cf: ChildActor.Factory) extends Actor with InjectedActorSupport {
      Logger.debug("ParentActor: created")
      val cactor = injectedChild(cf(2),"childActor")
      cactor ! 10
    
      def receive = { case _ => Logger.debug("ParentActor received msg") } 
    }
    
    
    object ChildActor { trait Factory { def apply(i: Int) : Actor } }
    class  ChildActor @Inject()( i: Injector, @Assisted v: Int) extends Actor {
      Logger.debug("ChildActor: created with value " + v.toString)
    
      def receive = { case _ => Logger.debug("ChildActor received msg") } 
    }
    
    class ParentModule extends AbstractModule with AkkaGuiceSupport {
      def configure () =  {
        bindActor(classOf[ParentActor],"PActor")
        bindActorFactory(classOf[ChildActor], classOf[ChildActor.Factory])
        bindActorFactory(classOf[ParentActor_2], classOf[ParentActor_2.Factory])
        bindActorFactory(classOf[ChildActor_2], classOf[ChildActor_2.Factory])
      }
    }
    
    
    object ParentActor_2 {  trait Factory { def apply() : Actor } }
    class  ParentActor_2 @Inject() (cf: ChildActor_2.Factory) extends Actor with InjectedActorSupport {
      Logger.debug("ParentActor_2: created")
      val cactor = injectedChild(cf(4),"childActor_2")
      cactor ! 10
    
      def receive = { case _ => Logger.debug("ParentActor_2 received msg") } 
    }
    
    
    object ChildActor_2 { trait Factory { def apply(i: Int) : Actor } }
    class  ChildActor_2 @Inject() ( i: Injector, @Assisted v: Int)  extends Actor {
      Logger.debug("ChildActor_2: created with value " + v.toString)
    
      def receive = { case _ => Logger.debug("ChildActor_2 received msg") } 
    }
    
    
    class GuiceActorProducer(val injector: Injector, val actorName: String) 
          extends IndirectActorProducer {
    
      override def actorClass = classOf[ParentActor_2]
      override def produce() =
        injector.getBinding(Key.get(classOf[ParentActor_2])).getProvider.get()
    }
    

    And then in my application.conf

    play.modules.enabled += "package.ParentModule"