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.
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.
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("receiver@icharm.me").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
Comments | NOTHING