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.

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