Contents

Use Akka-Quartz-Scheduler for cron schedule in Play Framework 2.7

The Akka-Quartz-Scheduler is a commended project by play official for implementing cron task scheduling , refer: https://www.playframework.com/documentation/2.7.x/ModuleDirectory#Akka-Quartz-Scheduler

But Akka-Quartz-Scheduler documentation does not cover the play framework, so there is a article to show how to use Akka-Quartz-Scheduler to achieve Cron-like scheduling when play app startup.

You can find this demo project in github.

Simple Steps

Step 1. Add Dependencies in build.sbt

// For Akka 2.5.x and Scala 2.11.x, 2.12.x
libraryDependencies += "com.enragedginger" %% "akka-quartz-scheduler" % "1.8.0-akka-2.5.x"

Step 2. Add cron exprssion configuration in application.conf

akka {
  quartz {
    defaultTimezone = "UTC"
    schedules {
      every15seconds {
        description = "job that fires off 9 clock every day"
        expression = "0/15 * * * * ?"
      }
      everyday9clock {
        description = "job that fires off 9 clock every day"
        expression = "0 0 9 * * ?"
      }
    }
  }
}

Step 3. Implement task by extends Actor

import akka.actor.{Actor, Props}

object HelloActor {
  def props = Props[HelloActor]

  case class SayHello(name: String)
}

class HelloActor extends Actor {
  import HelloActor._

  override def receive: Receive = {
    case SayHello(name: String) => {
      println("hello, " + name)
    }
  }
}

Step 4. Start scheduling when play app run

there is a very simple way, modify configure method in app/Module.scala as follow:

override def configure() = {
    // Use the system clock as the default implementation of Clock
    bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
    // Ask Guice to create an instance of ApplicationTimer when the
    // application starts.
    bind(classOf[ApplicationTimer]).asEagerSingleton()
    // Set AtomicCounter as the implementation for Counter.
    bind(classOf[Counter]).to(classOf[AtomicCounter])

    // Start scheduling
    val system = ActorSystem("SchedulerSystem")
    val scheduler = QuartzSchedulerExtension(system)
    val receiver = system.actorOf(Props(new HelloActor))
    scheduler.schedule("every15seconds", receiver, HelloActor.SayHello("Peter"), None)

  }

ok, a simple cron schedule is finished,  startup this play framework app, you will see a message “Hello, Peter” printed every 15s in the console.

/IMG_1501.png
hello, Peter

Handle Inject

But in practice, the actor task cannot be so simple, sometimes we need to inject other services by using annotation @Inject, for example: we need send mail in task, so I edit HelloActor.scala:

import akka.actor.{Actor, Props}
import com.google.inject.{Inject, Singleton}
import com.typesafe.config.ConfigFactory
import play.api.libs.mailer.{Email, MailerClient}

object HelloActor {
  def props = Props[HelloActor]

  case class SayHello(name: String)
}

@Singleton
class HelloActor @Inject()(mailerClient: MailerClient) extends Actor {
  import HelloActor._

  private val mailConfig = ConfigFactory.load

  override def receive: Receive = {
    case SayHello(name: String) => {
      val email = Email(
        "subject",
        mailConfig.getString("mailSender"),
        Seq("[email protected]").filterNot(_.equals("")),
        bodyText = Some("mail body")
      )
      println("hello, " + name)
    }
  }
}

Injected mailerClient in above code,  therefore we cannot new HelloActor instance in Module.scala,  what should we do?very simple, we need to use IndirectActorProducer,add GuiceActorProducer.scala extends IndirectActorProducer:

import akka.actor.{Actor, IndirectActorProducer}

class GuiceActorProducer(val injector: play.inject.Injector, val cls: Class[_ <: Actor]) extends IndirectActorProducer {

  override def actorClass = classOf[Actor]

  override def produce() = {
    injector.instanceOf(cls)
  }
}

then, we can create HelloActor instance like:

Props.create(classOf[GuiceActorProducer], injector, classOf[HelloActor])

Here is still another question, how to get play.inject.Injector instance in Module.scala, cannot use @Inject to inject injector。

So, I have to change the way to start scheduling, first add new ApplicationStart.scala, use @inject to inject injector:

import akka.actor.{ActorSystem, Props}
import com.google.inject.Inject
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import play.api.inject.ApplicationLifecycle
import play.inject.Injector

import scala.concurrent.Future

class ApplicationStart @Inject()(
                                   lifecycle: ApplicationLifecycle,
                                   system: ActorSystem,
                                   injector: Injector
                                 ) {

  // Shut-down hook
  lifecycle.addStopHook { () =>
    Future.successful()
  }

  // Start scheduling
  val scheduler = QuartzSchedulerExtension(system)
  val receiver = system.actorOf(Props.create(classOf[GuiceActorProducer], injector, classOf[HelloActor]))
  scheduler.schedule("every15seconds", receiver, HelloActor.SayHello("Peter"), None)

}

And then , bind this class as eager singleton in configure method of Module.scala , this will instantiate ApplicationStart as soon as the play app startup :

class Module extends AbstractModule {

  override def configure() = {
    // Use the system clock as the default implementation of Clock
    bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
    // Ask Guice to create an instance of ApplicationTimer when the
    // application starts.
    bind(classOf[ApplicationTimer]).asEagerSingleton()
    // Set AtomicCounter as the implementation for Counter.
    bind(classOf[Counter]).to(classOf[AtomicCounter])

    // Start scheduling
//    val system = ActorSystem("SchedulerSystem")
//    val scheduler = QuartzSchedulerExtension(system)
//    val receiver = system.actorOf(Props(new HelloActor))
//    scheduler.schedule("every15seconds", receiver, HelloActor.SayHello("Peter"), None)

    bind(classOf[ApplicationStart]).asEagerSingleton()

  }

}

Startup the app, it will work fine!

Reference

https://stackoverflow.com/questions/33889224/play-2-4-how-to-inject-akka-actors-using-guice