Cameron Hotchkies

Categories

  • Coding

Tags

  • akka
  • play
  • scala
  • testing

If you’re writing a play application and using an actor, chances are you’re going to want to test that they work. This is pretty straight forward, but what happens when the actor itself needs to frobitz a whatsits every bleventy seconds?

To do this you can easily have the scheduler schedule a repeated task.

 
package com.semisafe.frobitzer 

import akka.actor.Actor 

import scala.concurrent.duration._ 

// So we can get the cleaner " seconds" usage for durations 
import scala.language.postfixOps 

class Frobitzer(val interval: Long = bleventy) extends Actor { 
  import context.dispatcher 
  /** 
  * Initialize the actor to control it's own destiny for scheduling 
  * heartbeats & frobitzing as it sees fit 
  */ 
  override def preStart() { 
    context.system.scheduler.scheduleOnce(interval seconds, self, 'FrobitzNow) 
  } 
  
  def performTheFrobitz(whatsits: List[Whatsits]) = { 
    // Commence the frobitzing! 
  } 
  
  def receive = { 
    case 'FrobitzNow => { 
        // We'll send another periodic tick after the specified delay 
        // We do this here so they do not generate a backlog 
        context.system.scheduler.scheduleOnce(interval seconds, 
          self, 
          'FrobitzNow) 
        performTheFrobitz(Whatsits.getEmAll) 
    } 
  } 
} 

This is all well and good, but how do you start it? There’s probably a better way to do this, but I stick it in the Play GlobalSettings object.

object SsfwGlobal extends GlobalSettings { 
  override def onStart(app: Application) { 
    Logger.info("[+] Initializing Actors") 
    
    // Read the interval from a possibly available 
    // config setting (or default to bleventy) 
    val interval = app.configuration .getInt("frobitzer.interval") 
      .getOrElse(bleventy) 
    
    // Start the frobitzer! 
    val frobitzer = Akka.system(app) .actorOf(Props(new Frobitzer(interval))) 
  }
} 

At this point, it’s obvious that the default parameter for the actor isn’t really needed. You should also note that I stuck in a configurable parameter for the interval. This can be specified in you application.conf, but more likely it will be handy during unit testing:

 
"this spec should do something useful" in { 
  running(FakeApplication(additionalConfiguration = Map(
    ("frobitzer.interval" -> "2") 
  ))) { 
    testSomethingUseful 
  } 
} 

So instead of running tests forever, you can accelerate the intervals.

But wait! Next week when you have another 400 unit tests on the blahbedy module, your frobitzer is going to be slowing down all those tests (regardless of how elegant your frobitzing algorithms are). So let’s take another swing at the GlobalSettings to see if we can’t smooth this out.

object SsfwGlobalTestable extends GlobalSettings { 
  override def onStart(app: Application) { 
    Logger.info("[+] Initializing normal stuff...") 
    // put stuff here 
    Logger.info("[+] Initializing Actors") 
    // Initialize scheduled Actors 
    play.api.Play.mode(app) match { 
      case play.api.Mode.Test => { 
        // Only run actors if explicitly enabled 
        val enableScheduledActors = app.configuration
          .getBoolean("enable.scheduledActors") 
          .getOrElse(false) 
        if (enableScheduledActors) { 
          initializeScheduledTasks(app) 
        } 
      } 
      case _ => initializeScheduledTasks(app) 
    } 
  } 
  
  def initializeScheduledTasks(app: Application) = { 
    // Read the interval from a possibly available 
    // config setting (or default to bleventy) 
    val interval = app.configuration 
      .getInt("frobitzer.interval") 
      .getOrElse(bleventy) 
      
    // Start the frobitzer! 
    val frobitzer = Akka.system(app).actorOf(Props(new Frobitzer(interval))) 
  } 

and the Frobitz tests look more like:

"this spec should do something useful" in { 
  running(FakeApplication(additionalConfiguration = Map( 
    ("enable.scheduledActors" -> "true"), 
    ("frobitzer.interval" -> "2") ))) { 
      testSomethingUseful 
  } 
} 

Now we have a scheduled actor that only runs when either not in test mode, or the test explicitly requests it to be present. In the event the actor is both scheduled and expecting externally sourced messages, the actor can be initialized in the main section of preStart, and the schedule can be triggered by passing a message. If this is the case, you may find it’s better to pass the interval in the message as opposed to just using a symbol to trigger the heartbeat.

 
case class FrobitzTask(interval: Long) {} 

class OnDemandFrobitzer() extends Actor { 
  import context.dispatcher 
  
  def performTheFrobitz(whatsits: List[Whatsits]) = { 
    // Commence the frobitzing! 
  } 
  
  def receive = { 
    case FrobitzTask(interval) => { 
      // We'll send another periodic tick after the specified delay 
      // We do this here so they do not generate a backlog 
      context.system.scheduler.scheduleOnce(interval seconds, self, FrobitzTask(interval)) 
      performTheFrobitz(Whatsits.getEmAll) 
    } 
    // Leave this here for On-Demand frobitzing 
    case 'FrobitzNow => { 
      performTheFrobitz(Whatsits.getEmAll) 
    } 
  } 
} 

object SsfwGlobalSuperTestable extends GlobalSettings { 
  override def onStart(app: Application) { 
    Logger.info("[+] Initializing normal stuff...") 
    // put stuff here 
    Logger.info("[+] Initializing Actors") 
    val frobitzer = Akka.system(app) 
      .actorOf(Props(new OnDemandFrobitzer())) 
    // Initialize scheduled Actors 
    play.api.Play.mode(app) match { 
      case play.api.Mode.Test => { 
        // Only run actors if explicitly enabled 
        val enableScheduledActors = app.configuration 
          .getBoolean("enable.scheduledActors") 
          .getOrElse(false) 
        
        if (enableScheduledActors) { 
          initializeScheduledTasks(app, frobitzer) 
        } 
      } 
      case _ => initializeScheduledTasks(app, frobitzer) 
    } 
  } 
  
  def initializeScheduledTasks(app: Application, 
      frobitzer: OnDemandFrobitzer) = { 
    // Read the interval from a possibly available 
    // config setting (or default to bleventy) 
    val interval = app.configuration 
      .getInt("frobitzer.interval") 
      .getOrElse(bleventy) 
    
    val frobitzTask = FrobitzTask(interval) 
    // Start the frobitzer! 
    frobitzer ! frobitzTask 
  } 
}