├── .gitignore ├── LICENSE ├── README.markdown ├── examples └── resque.conf ├── lib ├── configgy-1.4.jar ├── json-1.1.jar └── redisclient-1.0.1.jar ├── project ├── build.properties └── build │ └── ScalaResqueWorker.scala └── src ├── main └── scala │ ├── FancySeq.scala │ ├── Job.scala │ ├── Machine.scala │ ├── Performable.scala │ ├── Resque.scala │ └── Worker.scala └── test └── scala ├── FancySeqSpec.scala ├── JobSpec.scala ├── MachineSpec.scala ├── PerformableSpec.scala ├── ResqueSpec.scala └── WorkerSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | lib_managed/ 3 | src_managed/ 4 | project/boot/ 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 James Golick 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Resque Workers in Scala 2 | ======================= 3 | 4 | Resque is the awesome queue system written by the github guys. It is a very polished piece of software that includes tremendous visibility in to the status of your workers and your queues. They include a full set of tools written in ruby, which is awesome. But, we're doing a lot of scala these days, and some of our background jobs *need* to be run by scala code. So, I hacked up some code to integrate in to the resque system from scala. 5 | 6 | How it Works 7 | ------------ 8 | 9 | To write the code that runs a job, you need to create an object that inherits from com.protose.resque.Performable. Note that the performable must have a 0-args constructor: 10 | 11 | import com.protose.resque.Performable 12 | 13 | class MyAwesomeJob extends Performable { 14 | override def perform(args: List[String]) = println("I did something awesome with " + args) 15 | } 16 | 17 | The name of your class (minus package) is how your performable will be identified. When you queue it up (presumably from ruby code), that name is what you need to use to make sure your Performable is found. In this example, that would like something like this: 18 | 19 | ## this is ruby code 20 | 21 | class MyAwesomeJob 22 | @queue = :my_queue 23 | end 24 | 25 | Resque.enqueue(MyAwesomeJob, "some arg") 26 | 27 | Note that the MyAwesomeJob ruby class doesn't have a self.perform method. It doesn't need one, because it's really just a placeholder for your scala job which does the actual work. 28 | 29 | Gotchas 30 | ------- 31 | 32 | 1. Scala is statically typed, so, you can only pass arguments that can get parsed in to List[String]. Probably not a big deal in practice, but worth knowing about. 33 | 2. There's currently no way to listen on multiple queues. This could be easily fixed. 34 | 35 | Running It 36 | ---------- 37 | 38 | Grab the latest jar from the Downloads section, and all the dependencies from lib and create some kind of script to setup all the classpath nonsense and stuff. Once you have that, you can either put a config file in /etc/resque.conf or use the CONFIG environment variable to point to a custom location. If you don't provide a config file, it'll assume that redis is running locally. 39 | 40 | In the config file, there are currently four parameters: 41 | 42 | redis.host = "some.host" 43 | redis.port = 12345 44 | queue = "the queue to listen on" 45 | performables = ["com.myco.MyJob"] 46 | 47 | You can also set the queue with the QUEUE environment variable, and the performables with the PERFORMABLES env var. Performables must be a comma separated list of full paths to your performable classes. 48 | 49 | There's an example config file in the examples directory. 50 | 51 | The config is setup using Configgy (http://www.lag.net/configgy/). So, you can use Configgy.config from your Performables to get additional config parameters. 52 | 53 | TODO 54 | ---- 55 | 56 | It should probably use actors for some concurrency. Currently, it processes one job at a time. 57 | 58 | Patches 59 | ------- 60 | 61 | This is my first open source scala project. I'm pretty noobish still, so I'd love some feedback, patches, etc. Please include specs with your patch where appropriate. 62 | 63 | License 64 | ------ 65 | 66 | scala-resque-worker is copyright (c) 2009 James Golick, written for use at FetLife.com (NSFW), released under the terms of the MIT License. See the LICENSE file for details. 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/resque.conf: -------------------------------------------------------------------------------- 1 | redis.host = "localhost" 2 | redis.port = 12345 3 | 4 | -------------------------------------------------------------------------------- /lib/configgy-1.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgolick/scala-resque-worker/9c748e1d0593f01550a7821fff4d8ffe1e75d6b8/lib/configgy-1.4.jar -------------------------------------------------------------------------------- /lib/json-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgolick/scala-resque-worker/9c748e1d0593f01550a7821fff4d8ffe1e75d6b8/lib/json-1.1.jar -------------------------------------------------------------------------------- /lib/redisclient-1.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgolick/scala-resque-worker/9c748e1d0593f01550a7821fff4d8ffe1e75d6b8/lib/redisclient-1.0.1.jar -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Project properties 2 | #Wed Nov 18 09:34:17 PST 2009 3 | project.organization=protose 4 | project.name=scala-resque-worker 5 | sbt.version=0.5.5 6 | project.version=0.2.1 7 | scala.version=2.7.5 8 | project.initialize=false 9 | -------------------------------------------------------------------------------- /project/build/ScalaResqueWorker.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | class ScalaResqueWorkerProject(info: ProjectInfo) extends DefaultProject(info) { 4 | val specs = "org.specs" % "specs" % "1.6.0" from "http://specs.googlecode.com/files/specs-1.6.0.jar" 5 | val mongodb = "com.mongodb" % "mongodb" % "1.0" from "http://cloud.github.com/downloads/mongodb/mongo-java-driver/mongo-1.0.jar" 6 | val mockito = "org.mockito" % "mockito" % "1.8.0" from "http://mockito.googlecode.com/files/mockito-all-1.8.0.jar" 7 | } 8 | 9 | // vim: set ts=4 sw=4 et: 10 | -------------------------------------------------------------------------------- /src/main/scala/FancySeq.scala: -------------------------------------------------------------------------------- 1 | package com.protose.resque 2 | 3 | object FancySeq { 4 | implicit def set2FancySeq[A](set: Seq[A]) = new FancySeq(set) 5 | } 6 | 7 | class FancySeq[A](set: Seq[A]) { 8 | def join: String = { 9 | set.foldLeft("") { (joined, str) => joined + str } 10 | } 11 | 12 | def join(delimiter: String): String = { 13 | set.slice(1, set.length).foldLeft(set.first.toString) { (joined, str) => 14 | joined + delimiter + str 15 | } 16 | } 17 | } 18 | 19 | 20 | // vim: set ts=4 sw=4 et: 21 | -------------------------------------------------------------------------------- /src/main/scala/Job.scala: -------------------------------------------------------------------------------- 1 | package com.protose.resque 2 | import com.twitter.json.Json 3 | 4 | class JobFactory(performableMap: Map[String, Performable]) { 5 | def apply(worker: Worker, queue: String, payload: String): Job = { 6 | Job(worker, queue, payload, performableMap) 7 | } 8 | } 9 | 10 | case class Job(worker: Worker, queue: String, 11 | payload: String, performableMap: Map[String, Performable]) { 12 | def perform = { 13 | performer.perform(parsedPayload("args").asInstanceOf[List[String]]) 14 | } 15 | 16 | def performer: Performable = { 17 | performableMap(parsedPayload("class")) 18 | } 19 | 20 | def parsedPayload: Map[String, String] = 21 | Json.parse(payload).asInstanceOf[Map[String, String]] 22 | } 23 | 24 | // vim: set ts=4 sw=4 et: 25 | -------------------------------------------------------------------------------- /src/main/scala/Machine.scala: -------------------------------------------------------------------------------- 1 | package com.protose.resque 2 | import java.net.InetAddress 3 | import java.lang.management.ManagementFactory 4 | 5 | object Machine { 6 | def hostname = InetAddress.getLocalHost.getHostName 7 | def pid = ManagementFactory.getRuntimeMXBean.getName.split("@").first 8 | } 9 | 10 | // vim: set ts=4 sw=4 et: 11 | -------------------------------------------------------------------------------- /src/main/scala/Performable.scala: -------------------------------------------------------------------------------- 1 | package com.protose.resque 2 | 3 | abstract class Performable { 4 | def perform(args: List[String]): Unit 5 | } 6 | 7 | // vim: set ts=4 sw=4 et: 8 | -------------------------------------------------------------------------------- /src/main/scala/Resque.scala: -------------------------------------------------------------------------------- 1 | package com.protose.resque 2 | import com.redis.Redis 3 | import FancySeq._ 4 | import java.util.Date 5 | import com.twitter.json.Json 6 | 7 | class Resque(val redis: Redis, val jobFactory: JobFactory) { 8 | def reserve(worker: Worker, name: String): Option[Job] = { 9 | try { 10 | val job = jobFactory(worker, name, pop(name)) 11 | setWorkingOn(worker, job) 12 | Some(job) 13 | } catch { 14 | case e: NullPointerException => return None 15 | } 16 | } 17 | 18 | def failure(job: Job, exception: Throwable) = { 19 | val failure = Map("failed_at" -> new Date().toString, 20 | "payload" -> job.parsedPayload, 21 | "error" -> exception.getMessage, 22 | "backtrace" -> exception.getStackTrace.map { s => s.toString }, 23 | "worker" -> job.worker.id, 24 | "queue" -> job.queue) 25 | redis.pushTail("resque:failed", Json.build(failure).toString) 26 | redis.incr(statKey("failed", job.worker)) 27 | redis.incr(statKey("failed")) 28 | deleteWorkingOn(job.worker) 29 | } 30 | 31 | def success(job: Job) = { 32 | redis.incr(statKey("processed", job.worker)) 33 | redis.incr(statKey("processed")) 34 | deleteWorkingOn(job.worker) 35 | } 36 | 37 | def register(worker: Worker): Unit = { 38 | addToWorkersSet(worker) 39 | setStartedTime(worker) 40 | } 41 | 42 | def unregister(worker: Worker): Unit = { 43 | removeFromWorkersSet(worker) 44 | deleteStartTime(worker) 45 | } 46 | 47 | protected def pop(name: String): String = { 48 | redis.popHead(queueName(name)) 49 | } 50 | 51 | protected def queueName(name: String) = List("resque", "queue", name).join(":") 52 | 53 | protected def setStartedTime(worker: Worker) = { 54 | redis.set(startedKey(worker), new Date().toString) 55 | } 56 | 57 | protected def startedKey(worker: Worker) = { 58 | List("resque:worker", worker.id, "started").join(":") 59 | } 60 | 61 | protected def addToWorkersSet(worker: Worker) = { 62 | redis.setAdd(workerSet, worker.id) 63 | } 64 | 65 | protected def removeFromWorkersSet(worker: Worker) = { 66 | redis.setDelete(workerSet, worker.id) 67 | } 68 | 69 | protected def deleteStartTime(worker: Worker) = { 70 | redis.delete(startedKey(worker)) 71 | } 72 | 73 | protected def workerSet = "resque:workers" 74 | protected def workerKey(worker: Worker) = { 75 | List("resque", "worker", worker.id).join(":") 76 | } 77 | 78 | protected def statKey(status: String, worker: Worker) = { 79 | List("resque", "stat", status, worker.id).join(":") 80 | } 81 | 82 | protected def statKey(status: String) = List("resque", "stat", status).join(":") 83 | 84 | protected def setWorkingOn(worker: Worker, job: Job) = { 85 | val data = Map("queue" -> job.queue, 86 | "run_at" -> new Date().toString, 87 | "payload" -> job.parsedPayload) 88 | redis.set(workerKey(worker), Json.build(data).toString) 89 | } 90 | 91 | protected def deleteWorkingOn(worker: Worker) = { 92 | redis.delete(workerKey(worker)) 93 | } 94 | } 95 | 96 | // vim: set ts=4 sw=4 et: 97 | -------------------------------------------------------------------------------- /src/main/scala/Worker.scala: -------------------------------------------------------------------------------- 1 | package com.protose.resque 2 | import Machine._ 3 | import com.protose.resque.FancySeq._ 4 | import java.util.Date 5 | import com.redis.Redis 6 | import net.lag.configgy.Configgy 7 | import net.lag.logging.Logger 8 | import java.io.File 9 | 10 | object Runner { 11 | var redisHost = "localhost" 12 | var redisPort = 6379 13 | var queue = "" 14 | val logger = Logger.get 15 | var performables: Seq[String] = Array[String]() 16 | var performableMap = Map[String, Performable]() 17 | 18 | def main(args: Array[String]) = { 19 | configure 20 | performableMap = performables.foldLeft(performableMap) { (map, path) => 21 | val className = path.split('.').last 22 | val instance = Class.forName(path).newInstance.asInstanceOf[Performable] 23 | map + Tuple(className, instance) 24 | } 25 | 26 | val redis = new Redis(redisHost, redisPort) 27 | 28 | if (!redis.connected) { 29 | logger.critical("Couldn't connect to redis server at: " + redisHost + ":" + redisPort) 30 | System.exit(1) 31 | } 32 | 33 | val resque = new Resque(redis, new JobFactory(performableMap)) 34 | val worker = new Worker(resque, List(queue)) 35 | 36 | worker.workOff 37 | } 38 | 39 | protected def configure = { 40 | val envVar = System.getenv.get("CONFIG") 41 | val filename = if (envVar == null) "/etc/resque.conf" 42 | else envVar 43 | var file = new File(filename) 44 | 45 | if (file.exists) { 46 | logger.info("Found configuration file at " + filename) 47 | configureFromFile(filename) 48 | } else { logger.info("No configuration file found. Using defaults.") } 49 | if (queue == "") attemptToGetQueueFromEnv 50 | if (performables.isEmpty) attemptToGetPerformablesFromEnv 51 | 52 | logger.info("Listening on " + queue) 53 | } 54 | 55 | protected def configureFromFile(filename: String) = { 56 | Configgy.configure(filename) 57 | var config = Configgy.config 58 | redisHost = config.getString("redis.host", "localhost") 59 | redisPort = config.getInt("redis.port", 6379) 60 | queue = config.getString("queue", "") 61 | performables = config.getList("performables") 62 | } 63 | 64 | protected def attemptToGetQueueFromEnv = { 65 | queue = System.getenv.get("QUEUE") 66 | if (queue == null) { 67 | logger.critical("Couldn't find any queues to listen on. Exiting...") 68 | System.exit(1) 69 | } 70 | } 71 | 72 | protected def attemptToGetPerformablesFromEnv = { 73 | val possiblePerformables = System.getenv.get("PERFORMABLES") 74 | if (possiblePerformables == null) { 75 | logger.critical("Couldn't find any performables with which to perform jobs. Exiting...") 76 | System.exit(1) 77 | } 78 | performables = possiblePerformables.split(',') 79 | } 80 | } 81 | 82 | class Worker(resque: Resque, queues: List[String], sleepTime: Int) { 83 | var exit = false 84 | val logger = Logger.get 85 | 86 | def this(resque: Resque, queues: List[String]) = this(resque, queues, 5000) 87 | 88 | def id = List(hostname, pid, queues.join(",")).join(":") 89 | 90 | def start = { 91 | resque.register(this) 92 | logger.info("Started worker " + id) 93 | } 94 | 95 | def stop = { 96 | resque.unregister(this) 97 | } 98 | 99 | def workOff = { 100 | catchShutdown 101 | start 102 | runInfiniteJobLoop 103 | stop 104 | } 105 | 106 | def work(job: Job) = { 107 | try { 108 | job.perform 109 | resque.success(job) 110 | } catch { 111 | case exception: Throwable => failure(job, exception) 112 | } 113 | } 114 | 115 | protected def runInfiniteJobLoop: Unit = { 116 | while(true) { 117 | if (exit) { return } 118 | val job = nextJob 119 | if (job.isEmpty) { 120 | logger.debug("No jobs found. Sleeping for " + sleepTime + "ms.") 121 | Thread.sleep(sleepTime) 122 | } else { 123 | work(job.get) 124 | } 125 | } 126 | } 127 | 128 | protected def nextJob = { 129 | resque.reserve(this, queues.first) 130 | } 131 | 132 | protected def catchShutdown = { 133 | val thread = Thread.currentThread 134 | Runtime.getRuntime.addShutdownHook(new Thread() { 135 | override def run = { 136 | exit = true 137 | thread.join 138 | } 139 | }) 140 | } 141 | 142 | protected def failure(job: Job, exception: Throwable) = { 143 | logger.debug(exception, "Failed to process job with exception.") 144 | resque.failure(job, exception) 145 | } 146 | } 147 | 148 | // vim: set ts=4 sw=4 et: 149 | -------------------------------------------------------------------------------- /src/test/scala/FancySeqSpec.scala: -------------------------------------------------------------------------------- 1 | package test.scala 2 | import org.specs.Specification 3 | import com.protose.resque._ 4 | import com.protose.resque.FancySeq._ 5 | 6 | object FancySeqSpec extends Specification { 7 | "it joins seqs of strings together" in { 8 | List("1", "2", "3").join must_== "123" 9 | } 10 | "it joins with a delimiter" in { 11 | List("1", "2", "3").join(",") must_== "1,2,3" 12 | } 13 | } 14 | 15 | // vim: set ts=4 sw=4 et: 16 | -------------------------------------------------------------------------------- /src/test/scala/JobSpec.scala: -------------------------------------------------------------------------------- 1 | package test.scala 2 | import org.specs.Specification 3 | import org.specs.mock.Mockito 4 | import com.twitter.json.Json 5 | import com.protose.resque._ 6 | 7 | object JobSpec extends Specification with Mockito { 8 | val worker = mock[Worker] 9 | val queue = "some_awesome_queue" 10 | val payload = Map("args" -> List("arg1"), "class" -> "SomeAwesomeJob") 11 | val performable = new Performable { 12 | def perform(args: List[String]) = {} 13 | } 14 | val job = Job(worker, queue, Json.build(payload).toString, Map("SomeAwesomeJob" -> performable)) 15 | 16 | "performing a job" in { 17 | job.perform 18 | 19 | "calls perform on the performable with the args" in { 20 | performable.perform(List("arg1")) was called 21 | } 22 | } 23 | } 24 | 25 | // vim: set ts=4 sw=4 et: 26 | -------------------------------------------------------------------------------- /src/test/scala/MachineSpec.scala: -------------------------------------------------------------------------------- 1 | package test.scala 2 | import org.specs.Specification 3 | import org.specs.mock.Mockito 4 | import com.protose.resque._ 5 | import com.protose.resque.Machine._ 6 | import java.net.InetAddress 7 | import java.lang.management.ManagementFactory 8 | 9 | object MachineSpec extends Specification with Mockito { 10 | "returns the hostname" in { 11 | hostname must_== InetAddress.getLocalHost.getHostName 12 | } 13 | "returns the pid" in { 14 | val expectedPid = ManagementFactory.getRuntimeMXBean.getName.split("@").first 15 | pid must_== expectedPid 16 | } 17 | } 18 | 19 | 20 | // vim: set ts=4 sw=4 et: 21 | -------------------------------------------------------------------------------- /src/test/scala/PerformableSpec.scala: -------------------------------------------------------------------------------- 1 | package test.scala 2 | import org.specs.Specification 3 | import com.protose.resque._ 4 | 5 | object PerformableSpec extends Specification { 6 | } 7 | 8 | 9 | // vim: set ts=4 sw=4 et: 10 | -------------------------------------------------------------------------------- /src/test/scala/ResqueSpec.scala: -------------------------------------------------------------------------------- 1 | package test.scala 2 | import org.specs.Specification 3 | import org.specs.mock.Mockito 4 | import com.protose.resque._ 5 | import com.redis.Redis 6 | import FancySeq._ 7 | import java.util.Date 8 | import java.lang.NullPointerException 9 | import com.twitter.json.Json 10 | 11 | object ResqueSpec extends Specification with Mockito { 12 | val redis = mock[Redis] 13 | val jobFactory = mock[JobFactory] 14 | val resque = new Resque(redis, jobFactory) 15 | val worker = new Worker(resque, List("some queue")) 16 | val job = Job(worker, "some_queue", "{}", Map[String, Performable]()) 17 | val startKey = List("resque", "worker", worker.id, "started").join(":") 18 | 19 | "reserving a job" in { 20 | "when there is a job" in { 21 | redis.popHead("resque:queue:some_queue") returns "{}" 22 | jobFactory.apply(worker, "some_queue", "{}") returns job 23 | val returnVal = resque.reserve(worker, "some_queue").get 24 | 25 | "fetches the next payload from the queue" in { 26 | redis.popHead("resque:queue:some_queue") was called 27 | } 28 | 29 | "returns the job created with {}" in { 30 | jobFactory.apply(worker, "some_queue", "{}") was called 31 | returnVal must_== job 32 | } 33 | 34 | "tells redis about the job we're processing" in { 35 | val json = Map("queue" -> "some_queue", 36 | "run_at" -> new Date().toString, 37 | "payload" -> Map[String, String]()) 38 | redis.set("resque:worker:" + worker.id, Json.build(json).toString) was called 39 | } 40 | } 41 | 42 | "when there is no job" in { 43 | redis.popHead("resque:queue:some_queue") throws new NullPointerException 44 | val returnVal = resque.reserve(worker, "some_queue") 45 | 46 | "returns None" in { 47 | returnVal must_== None 48 | } 49 | } 50 | } 51 | 52 | "registering a worker" in { 53 | val date = new Date().toString 54 | redis.setAdd("resque:workers", worker.id) returns true 55 | resque.register(worker) 56 | 57 | "adds the worker to the workers set" in { 58 | redis.setAdd("resque:workers", worker.id) was called 59 | } 60 | 61 | "informs redis that work has started" in { 62 | redis.set(startKey, date) was called 63 | } 64 | } 65 | 66 | "stopping a worker" in { 67 | redis.delete(startKey) returns true 68 | redis.setDelete("resque:workers", worker.id) returns true 69 | resque.unregister(worker) 70 | 71 | "removes the worker from the workers set" in { 72 | redis.setDelete("resque:workers", worker.id) was called 73 | } 74 | 75 | "deletes the started time" in { 76 | redis.delete(startKey) was called 77 | } 78 | } 79 | 80 | "registering a failure" in { 81 | val exception = new NullPointerException("AHHHH!!!!") 82 | val trace = exception.getStackTrace.map { s => s.toString} 83 | val payload = job.parsedPayload 84 | val failure = Map("failed_at" -> new Date().toString, 85 | "payload" -> payload, 86 | "error" -> exception.getMessage, 87 | "backtrace" -> trace, 88 | "worker" -> job.worker.id, 89 | "queue" -> job.queue) 90 | val json = Json.build(failure).toString 91 | redis.pushTail("resque:failed", json) returns true 92 | resque.failure(job, exception) 93 | 94 | "jsonifies the data and pushes it on to the failures queue" in { 95 | redis.pushTail("resque:failed", json) was called 96 | } 97 | 98 | "increments the failures for this worker" in { 99 | redis.incr("resque:stat:failed:" + worker.id) was called 100 | } 101 | 102 | "increments the overall failure stats" in { 103 | redis.incr("resque:stat:failed") was called 104 | } 105 | 106 | "tells redis we're not workign on the job anymore" in { 107 | redis.delete("resque:worker:" + worker.id) was called 108 | } 109 | } 110 | 111 | "registering success" in { 112 | resque.success(job) 113 | 114 | "increments the processed for this worker" in { 115 | redis.incr("resque:stat:processed:" + worker.id) was called 116 | } 117 | 118 | "increments the overall processed stats" in { 119 | redis.incr("resque:stat:processed") was called 120 | } 121 | 122 | "tells redis we're not workign on the job anymore" in { 123 | redis.delete("resque:worker:" + worker.id) was called 124 | } 125 | } 126 | } 127 | 128 | // vim: set ts=4 sw=4 et: 129 | -------------------------------------------------------------------------------- /src/test/scala/WorkerSpec.scala: -------------------------------------------------------------------------------- 1 | package test.scala 2 | import org.specs.Specification 3 | import org.specs.mock.Mockito 4 | import org.mockito.Matchers._ 5 | import com.protose.resque._ 6 | import com.protose.resque.Machine._ 7 | import com.protose.resque.FancySeq._ 8 | import com.redis.Redis 9 | import java.util.Date 10 | 11 | object WorkerSpec extends Specification with Mockito { 12 | val resque = mock[Resque] 13 | val worker = new Worker(resque, List("someAwesomeQueue", "someOtherAwesomeQueue")) 14 | val startKey = List("resque", "worker", worker.id, "started").join(":") 15 | val job = mock[Job] 16 | 17 | "it has a string representation" in { 18 | val expectedId = hostname + ":" + pid + ":" + "someAwesomeQueue,someOtherAwesomeQueue" 19 | worker.id must_== expectedId 20 | } 21 | 22 | "starting a worker" in { 23 | worker.start 24 | 25 | "registers the worker with resque" in { 26 | resque.register(worker) was called 27 | } 28 | } 29 | 30 | "stopping a worker" in { 31 | worker.stop 32 | 33 | "unregisters the worker" in { 34 | resque.unregister(worker) was called 35 | } 36 | } 37 | 38 | "working off the next job" in { 39 | "when the job succeeds" in { 40 | worker.work(job) 41 | 42 | "performs the job" in { 43 | job.perform was called 44 | } 45 | 46 | "notifies the job of success" in { 47 | resque.success(job) was called 48 | } 49 | } 50 | 51 | "when the job fails" in { 52 | val exception = new NullPointerException("asdf") 53 | job.perform throws exception 54 | worker.work(job) 55 | 56 | "it registers a failure" in { 57 | resque.failure(job, exception) was called 58 | } 59 | } 60 | } 61 | } 62 | 63 | // vim: set ts=4 sw=4 et: 64 | --------------------------------------------------------------------------------