├── .gitignore ├── init.rb ├── lib ├── resque │ ├── version.rb │ ├── server │ │ ├── views │ │ │ ├── error.erb │ │ │ ├── overview.erb │ │ │ ├── key_string.erb │ │ │ ├── next_more.erb │ │ │ ├── key_sets.erb │ │ │ ├── layout.erb │ │ │ ├── stats.erb │ │ │ ├── queues.erb │ │ │ ├── failed.erb │ │ │ ├── working.erb │ │ │ └── workers.erb │ │ ├── public │ │ │ ├── idle.png │ │ │ ├── poll.png │ │ │ ├── favicon.ico │ │ │ ├── working.png │ │ │ ├── reset.css │ │ │ ├── ranger.js │ │ │ ├── jquery.relatize_date.js │ │ │ └── style.css │ │ └── test_helper.rb │ ├── errors.rb │ ├── tasks.rb │ ├── failure │ │ ├── multiple.rb │ │ ├── hoptoad.rb │ │ ├── redis.rb │ │ └── base.rb │ ├── stat.rb │ ├── plugin.rb │ ├── helpers.rb │ ├── failure.rb │ ├── server.rb │ ├── job.rb │ └── worker.rb ├── tasks │ ├── resque.rake │ └── redis.rake └── resque.rb ├── Gemfile ├── examples ├── demo │ ├── Rakefile │ ├── job.rb │ ├── config.ru │ ├── app.rb │ └── README.markdown ├── instance.rb ├── monit │ └── resque.monit ├── god │ ├── stale.god │ └── resque.god ├── simple.rb └── async_helper.rb ├── config.ru ├── .kick ├── bin ├── resque-web └── resque ├── test ├── hoptoad_test.rb ├── resque-web_test.rb ├── plugin_test.rb ├── test_helper.rb ├── redis-test.conf ├── job_plugins_test.rb ├── resque_test.rb ├── job_hooks_test.rb └── worker_test.rb ├── LICENSE ├── Rakefile ├── resque.gemspec ├── docs ├── PLUGINS.md └── HOOKS.md ├── HISTORY.md └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'resque' 2 | -------------------------------------------------------------------------------- /lib/resque/version.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | Version = VERSION = '1.17.1' 3 | end 4 | -------------------------------------------------------------------------------- /lib/resque/server/views/error.erb: -------------------------------------------------------------------------------- 1 |

<%= error %>

-------------------------------------------------------------------------------- /lib/tasks/resque.rake: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' 2 | require 'resque/tasks' 3 | -------------------------------------------------------------------------------- /lib/resque/server/public/idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/resque/master/lib/resque/server/public/idle.png -------------------------------------------------------------------------------- /lib/resque/server/public/poll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/resque/master/lib/resque/server/public/poll.png -------------------------------------------------------------------------------- /lib/resque/server/views/overview.erb: -------------------------------------------------------------------------------- 1 | <%= partial :queues %> 2 |
3 | <%= partial :working %> 4 | <%= poll %> 5 | -------------------------------------------------------------------------------- /lib/resque/server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/resque/master/lib/resque/server/public/favicon.ico -------------------------------------------------------------------------------- /lib/resque/server/public/working.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/resque/master/lib/resque/server/public/working.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem "rake" 7 | gem "rack-test", "~> 0.5" 8 | gem "mocha", "~> 0.9.7" 9 | gem "leftright", :platforms => :mri_18 10 | end 11 | -------------------------------------------------------------------------------- /examples/demo/Rakefile: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib' 2 | require 'resque/tasks' 3 | require 'job' 4 | 5 | desc "Start the demo using `rackup`" 6 | task :start do 7 | exec "rackup config.ru" 8 | end 9 | -------------------------------------------------------------------------------- /examples/instance.rb: -------------------------------------------------------------------------------- 1 | # DelayedJob wants you to create instances. No problem. 2 | 3 | class Archive < Struct.new(:repo_id, :branch) 4 | def self.perform(*args) 5 | new(*args).perform 6 | end 7 | 8 | def perform 9 | # do work! 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/resque/server/views/key_string.erb: -------------------------------------------------------------------------------- 1 | <% if key = params[:key] %> 2 |

Key "<%= key %>" is a <%= resque.redis.type key %>

3 |

size: <%= redis_get_size(key) %>

4 | 5 | 6 | 9 | 10 |
7 | <%= redis_get_value_as_array(key) %> 8 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/resque/errors.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | # Raised whenever we need a queue but none is provided. 3 | class NoQueueError < RuntimeError; end 4 | 5 | # Raised when trying to create a job without a class 6 | class NoClassError < RuntimeError; end 7 | 8 | # Raised when a worker was killed while processing a job. 9 | class DirtyExit < RuntimeError; end 10 | end 11 | -------------------------------------------------------------------------------- /lib/resque/server/views/next_more.erb: -------------------------------------------------------------------------------- 1 | <%if start - 20 >= 0 || start + 20 <= size%> 2 |

3 | <% if start - 20 >= 0 %> 4 | « less 5 | <% end %> 6 | <% if start + 20 <= size %> 7 | more » 8 | <% end %> 9 |

10 | <%end%> -------------------------------------------------------------------------------- /examples/demo/job.rb: -------------------------------------------------------------------------------- 1 | require 'resque' 2 | 3 | module Demo 4 | module Job 5 | @queue = :default 6 | 7 | def self.perform(params) 8 | sleep 1 9 | puts "Processed a job!" 10 | end 11 | end 12 | 13 | module FailingJob 14 | @queue = :failing 15 | 16 | def self.perform(params) 17 | sleep 1 18 | raise 'not processable!' 19 | puts "Processed a job!" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'logger' 3 | 4 | $LOAD_PATH.unshift ::File.expand_path(::File.dirname(__FILE__) + '/lib') 5 | require 'resque/server' 6 | 7 | # Set the RESQUECONFIG env variable if you've a `resque.rb` or similar 8 | # config file you want loaded on boot. 9 | if ENV['RESQUECONFIG'] && ::File.exists?(::File.expand_path(ENV['RESQUECONFIG'])) 10 | load ::File.expand_path(ENV['RESQUECONFIG']) 11 | end 12 | 13 | use Rack::ShowExceptions 14 | run Resque::Server.new 15 | -------------------------------------------------------------------------------- /lib/resque/server/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rack/test' 2 | require 'resque/server' 3 | 4 | module Resque 5 | module TestHelper 6 | class Test::Unit::TestCase 7 | include Rack::Test::Methods 8 | def app 9 | Resque::Server.new 10 | end 11 | 12 | def self.should_respond_with_success 13 | test "should respond with success" do 14 | assert last_response.ok?, last_response.errors 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/resque/server/views/key_sets.erb: -------------------------------------------------------------------------------- 1 | <% if key = params[:key] %> 2 | 3 |

4 | Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <%=size = redis_get_size(key) %> 5 |

6 | 7 |

Key "<%= key %>" is a <%= resque.redis.type key %>

8 | 9 | <% for row in redis_get_value_as_array(key, start) %> 10 | 11 | 14 | 15 | <% end %> 16 |
12 | <%= row %> 13 |
17 | 18 | <%= partial :next_more, :start => start, :size => size %> 19 | <% end %> 20 | -------------------------------------------------------------------------------- /examples/demo/config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'logger' 3 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib' 4 | require 'app' 5 | require 'resque/server' 6 | 7 | use Rack::ShowExceptions 8 | 9 | # Set the AUTH env variable to your basic auth password to protect Resque. 10 | AUTH_PASSWORD = ENV['AUTH'] 11 | if AUTH_PASSWORD 12 | Resque::Server.use Rack::Auth::Basic do |username, password| 13 | password == AUTH_PASSWORD 14 | end 15 | end 16 | 17 | run Rack::URLMap.new \ 18 | "/" => Demo::App.new, 19 | "/resque" => Resque::Server.new 20 | -------------------------------------------------------------------------------- /.kick: -------------------------------------------------------------------------------- 1 | # take control of the growl notifications 2 | module GrowlHacks 3 | def growl(type, subject, body, *args, &block) 4 | case type 5 | when Kicker::GROWL_NOTIFICATIONS[:succeeded] 6 | puts subject = "Success" 7 | body = body.split("\n").last 8 | when Kicker::GROWL_NOTIFICATIONS[:failed] 9 | subject = "Failure" 10 | puts body 11 | body = body.split("\n").last 12 | else 13 | return nil 14 | end 15 | super(type, subject, body, *args, &block) 16 | end 17 | end 18 | 19 | Kicker.send :extend, GrowlHacks 20 | 21 | # no logging 22 | Kicker::Utils.module_eval do 23 | def log(message) 24 | nil 25 | end 26 | end -------------------------------------------------------------------------------- /examples/monit/resque.monit: -------------------------------------------------------------------------------- 1 | check process resque_worker_QUEUE 2 | with pidfile /data/APP_NAME/current/tmp/pids/resque_worker_QUEUE.pid 3 | start program = "/bin/sh -l -c 'cd /data/APP_NAME/current; nohup bundle exec rake environment resque:work RAILS_ENV=production QUEUE=queue_name VERBOSE=1 PIDFILE=tmp/pids/resque_worker_QUEUE.pid & >> log/resque_worker_QUEUE.log 2>&1'" as uid deploy and gid deploy 4 | stop program = "/bin/sh -c 'cd /data/APP_NAME/current && kill -s QUIT `cat tmp/pids/resque_worker_QUEUE.pid` && rm -f tmp/pids/resque_worker_QUEUE.pid; exit 0;'" 5 | if totalmem is greater than 300 MB for 10 cycles then restart # eating up memory? 6 | group resque_workers 7 | -------------------------------------------------------------------------------- /bin/resque-web: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') 4 | begin 5 | require 'vegas' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'vegas' 9 | end 10 | require 'resque/server' 11 | 12 | 13 | Vegas::Runner.new(Resque::Server, 'resque-web', { 14 | :before_run => lambda {|v| 15 | path = (ENV['RESQUECONFIG'] || v.args.first) 16 | load path.to_s.strip if path 17 | } 18 | }) do |runner, opts, app| 19 | opts.on('-N NAMESPACE', "--namespace NAMESPACE", "set the Redis namespace") {|namespace| 20 | runner.logger.info "Using Redis namespace '#{namespace}'" 21 | Resque.redis.namespace = namespace 22 | } 23 | end 24 | -------------------------------------------------------------------------------- /examples/god/stale.god: -------------------------------------------------------------------------------- 1 | # This will ride alongside god and kill any rogue stale worker 2 | # processes. Their sacrifice is for the greater good. 3 | 4 | WORKER_TIMEOUT = 60 * 10 # 10 minutes 5 | 6 | Thread.new do 7 | loop do 8 | begin 9 | `ps -e -o pid,command | grep [r]esque`.split("\n").each do |line| 10 | parts = line.split(' ') 11 | next if parts[-2] != "at" 12 | started = parts[-1].to_i 13 | elapsed = Time.now - Time.at(started) 14 | 15 | if elapsed >= WORKER_TIMEOUT 16 | ::Process.kill('USR1', parts[0].to_i) 17 | end 18 | end 19 | rescue 20 | # don't die because of stupid exceptions 21 | nil 22 | end 23 | 24 | sleep 30 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/hoptoad_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | begin 4 | require 'hoptoad_notifier' 5 | rescue LoadError 6 | warn "Install hoptoad_notifier gem to run Hoptoad tests." 7 | end 8 | 9 | if defined? HoptoadNotifier 10 | context "Hoptoad" do 11 | test "should be notified of an error" do 12 | exception = StandardError.new("BOOM") 13 | worker = Resque::Worker.new(:test) 14 | queue = "test" 15 | payload = {'class' => Object, 'args' => 66} 16 | 17 | HoptoadNotifier.expects(:notify_or_ignore).with( 18 | exception, 19 | :parameters => {:payload_class => 'Object', :payload_args => '66'}) 20 | 21 | backend = Resque::Failure::Hoptoad.new(exception, worker, queue, payload) 22 | backend.save 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/simple.rb: -------------------------------------------------------------------------------- 1 | # This is a simple Resque job. 2 | class Archive 3 | @queue = :file_serve 4 | 5 | def self.perform(repo_id, branch = 'master') 6 | repo = Repository.find(repo_id) 7 | repo.create_archive(branch) 8 | end 9 | end 10 | 11 | # This is in our app code 12 | class Repository < Model 13 | # ... stuff ... 14 | 15 | def async_create_archive(branch) 16 | Resque.enqueue(Archive, self.id, branch) 17 | end 18 | 19 | # ... more stuff ... 20 | end 21 | 22 | # Calling this code: 23 | repo = Repository.find(22) 24 | repo.async_create_archive('homebrew') 25 | 26 | # Will return immediately and create a Resque job which is later 27 | # processed. 28 | 29 | # Essentially, this code is run by the worker when processing: 30 | Archive.perform(22, 'homebrew') 31 | -------------------------------------------------------------------------------- /examples/async_helper.rb: -------------------------------------------------------------------------------- 1 | # If you want to just call a method on an object in the background, 2 | # we can easily add that functionality to Resque. 3 | # 4 | # This is similar to DelayedJob's `send_later`. 5 | # 6 | # Keep in mind that, unlike DelayedJob, only simple Ruby objects 7 | # can be persisted. 8 | # 9 | # If it can be represented in JSON, it can be stored in a job. 10 | 11 | # Here's our ActiveRecord class 12 | class Repository < ActiveRecord::Base 13 | # This will be called by a worker when a job needs to be processed 14 | def self.perform(id, method, *args) 15 | find(id).send(method, *args) 16 | end 17 | 18 | # We can pass this any Repository instance method that we want to 19 | # run later. 20 | def async(method, *args) 21 | Resque.enqueue(Repository, id, method, *args) 22 | end 23 | end 24 | 25 | # Now we can call any method and have it execute later: 26 | 27 | @repo.async(:update_disk_usage) 28 | 29 | # or 30 | 31 | @repo.async(:update_network_source_id, 34) 32 | -------------------------------------------------------------------------------- /lib/resque/server/public/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, font, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | dl, dt, dd, ul, li, 7 | form, label, legend, 8 | table, caption, tbody, tfoot, thead, tr, th, td { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | outline: 0; 13 | font-weight: inherit; 14 | font-style: normal; 15 | font-size: 100%; 16 | font-family: inherit; 17 | } 18 | 19 | :focus { 20 | outline: 0; 21 | } 22 | 23 | body { 24 | line-height: 1; 25 | } 26 | 27 | ul { 28 | list-style: none; 29 | } 30 | 31 | table { 32 | border-collapse: collapse; 33 | border-spacing: 0; 34 | } 35 | 36 | caption, th, td { 37 | text-align: left; 38 | font-weight: normal; 39 | } 40 | 41 | blockquote:before, blockquote:after, 42 | q:before, q:after { 43 | content: ""; 44 | } 45 | 46 | blockquote, q { 47 | quotes: "" ""; 48 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/resque-web_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'resque/server/test_helper' 3 | 4 | # Root path test 5 | context "on GET to /" do 6 | setup { get "/" } 7 | 8 | test "redirect to overview" do 9 | follow_redirect! 10 | end 11 | end 12 | 13 | # Global overview 14 | context "on GET to /overview" do 15 | setup { get "/overview" } 16 | 17 | test "should at least display 'queues'" do 18 | assert last_response.body.include?('Queues') 19 | end 20 | end 21 | 22 | # Working jobs 23 | context "on GET to /working" do 24 | setup { get "/working" } 25 | 26 | should_respond_with_success 27 | end 28 | 29 | # Failed 30 | context "on GET to /failed" do 31 | setup { get "/failed" } 32 | 33 | should_respond_with_success 34 | end 35 | 36 | # Stats 37 | context "on GET to /stats/resque" do 38 | setup { get "/stats/resque" } 39 | 40 | should_respond_with_success 41 | end 42 | 43 | context "on GET to /stats/redis" do 44 | setup { get "/stats/redis" } 45 | 46 | should_respond_with_success 47 | end 48 | 49 | context "on GET to /stats/resque" do 50 | setup { get "/stats/keys" } 51 | 52 | should_respond_with_success 53 | end 54 | -------------------------------------------------------------------------------- /lib/resque/tasks.rb: -------------------------------------------------------------------------------- 1 | # require 'resque/tasks' 2 | # will give you the resque tasks 3 | 4 | namespace :resque do 5 | task :setup 6 | 7 | desc "Start a Resque worker" 8 | task :work => :setup do 9 | require 'resque' 10 | 11 | queues = (ENV['QUEUES'] || ENV['QUEUE']).to_s.split(',') 12 | 13 | begin 14 | worker = Resque::Worker.new(*queues) 15 | worker.verbose = ENV['LOGGING'] || ENV['VERBOSE'] 16 | worker.very_verbose = ENV['VVERBOSE'] 17 | rescue Resque::NoQueueError 18 | abort "set QUEUE env var, e.g. $ QUEUE=critical,high rake resque:work" 19 | end 20 | 21 | if ENV['PIDFILE'] 22 | File.open(ENV['PIDFILE'], 'w') { |f| f << worker.pid } 23 | end 24 | 25 | worker.log "Starting worker #{worker}" 26 | 27 | worker.work(ENV['INTERVAL'] || 5) # interval, will block 28 | end 29 | 30 | desc "Start multiple Resque workers. Should only be used in dev mode." 31 | task :workers do 32 | threads = [] 33 | 34 | ENV['COUNT'].to_i.times do 35 | threads << Thread.new do 36 | system "rake resque:work" 37 | end 38 | end 39 | 40 | threads.each { |thread| thread.join } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /examples/demo/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'resque' 3 | require 'job' 4 | 5 | module Demo 6 | class App < Sinatra::Base 7 | get '/' do 8 | info = Resque.info 9 | out = "Resque Demo" 10 | out << "

" 11 | out << "There are #{info[:pending]} pending and " 12 | out << "#{info[:processed]} processed jobs across #{info[:queues]} queues." 13 | out << "

" 14 | out << '
' 15 | out << '' 16 | out << '  View Resque' 17 | out << '
' 18 | 19 | out << "
" 20 | out << '' 21 | out << '  View Resque' 22 | out << '
' 23 | 24 | out << "" 25 | out 26 | end 27 | 28 | post '/' do 29 | Resque.enqueue(Job, params) 30 | redirect "/" 31 | end 32 | 33 | post '/failing' do 34 | Resque.enqueue(FailingJob, params) 35 | redirect "/" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/resque/failure/multiple.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | module Failure 3 | # A Failure backend that uses multiple backends 4 | # delegates all queries to the first backend 5 | class Multiple < Base 6 | 7 | class << self 8 | attr_accessor :classes 9 | end 10 | 11 | def self.configure 12 | yield self 13 | Resque::Failure.backend = self 14 | end 15 | 16 | def initialize(*args) 17 | super 18 | @backends = self.class.classes.map {|klass| klass.new(*args)} 19 | end 20 | 21 | def save 22 | @backends.each(&:save) 23 | end 24 | 25 | # The number of failures. 26 | def self.count 27 | classes.first.count 28 | end 29 | 30 | # Returns a paginated array of failure objects. 31 | def self.all(start = 0, count = 1) 32 | classes.first.all(start,count) 33 | end 34 | 35 | # A URL where someone can go to view failures. 36 | def self.url 37 | classes.first.url 38 | end 39 | 40 | # Clear all failure objects 41 | def self.clear 42 | classes.first.clear 43 | end 44 | 45 | def self.requeue(*args) 46 | classes.first.requeue(*args) 47 | end 48 | 49 | def self.remove(index) 50 | classes.each { |klass| klass.remove(index) } 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/resque/stat.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | # The stat subsystem. Used to keep track of integer counts. 3 | # 4 | # Get a stat: Stat[name] 5 | # Incr a stat: Stat.incr(name) 6 | # Decr a stat: Stat.decr(name) 7 | # Kill a stat: Stat.clear(name) 8 | module Stat 9 | extend self 10 | extend Helpers 11 | 12 | # Returns the int value of a stat, given a string stat name. 13 | def get(stat) 14 | redis.get("stat:#{stat}").to_i 15 | end 16 | 17 | # Alias of `get` 18 | def [](stat) 19 | get(stat) 20 | end 21 | 22 | # For a string stat name, increments the stat by one. 23 | # 24 | # Can optionally accept a second int parameter. The stat is then 25 | # incremented by that amount. 26 | def incr(stat, by = 1) 27 | redis.incrby("stat:#{stat}", by) 28 | end 29 | 30 | # Increments a stat by one. 31 | def <<(stat) 32 | incr stat 33 | end 34 | 35 | # For a string stat name, decrements the stat by one. 36 | # 37 | # Can optionally accept a second int parameter. The stat is then 38 | # decremented by that amount. 39 | def decr(stat, by = 1) 40 | redis.decrby("stat:#{stat}", by) 41 | end 42 | 43 | # Decrements a stat by one. 44 | def >>(stat) 45 | decr stat 46 | end 47 | 48 | # Removes a stat from Redis, effectively setting it to 0. 49 | def clear(stat) 50 | redis.del("stat:#{stat}") 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # 2 | # Setup 3 | # 4 | 5 | load 'lib/tasks/redis.rake' 6 | 7 | $LOAD_PATH.unshift 'lib' 8 | require 'resque/tasks' 9 | 10 | def command?(command) 11 | system("type #{command} > /dev/null 2>&1") 12 | end 13 | 14 | 15 | # 16 | # Tests 17 | # 18 | 19 | require 'rake/testtask' 20 | 21 | task :default => :test 22 | 23 | if command?(:rg) 24 | desc "Run the test suite with rg" 25 | task :test do 26 | Dir['test/**/*_test.rb'].each do |f| 27 | sh("rg #{f}") 28 | end 29 | end 30 | else 31 | Rake::TestTask.new do |test| 32 | test.libs << "test" 33 | test.test_files = FileList['test/**/*_test.rb'] 34 | end 35 | end 36 | 37 | if command? :kicker 38 | desc "Launch Kicker (like autotest)" 39 | task :kicker do 40 | puts "Kicking... (ctrl+c to cancel)" 41 | exec "kicker -e rake test lib examples" 42 | end 43 | end 44 | 45 | 46 | # 47 | # Install 48 | # 49 | 50 | task :install => [ 'redis:install', 'dtach:install' ] 51 | 52 | 53 | # 54 | # Documentation 55 | # 56 | 57 | begin 58 | require 'sdoc_helpers' 59 | rescue LoadError 60 | end 61 | 62 | 63 | # 64 | # Publishing 65 | # 66 | 67 | desc "Push a new version to Gemcutter" 68 | task :publish do 69 | require 'resque/version' 70 | 71 | sh "gem build resque.gemspec" 72 | sh "gem push resque-#{Resque::Version}.gem" 73 | sh "git tag v#{Resque::Version}" 74 | sh "git push origin v#{Resque::Version}" 75 | sh "git push origin master" 76 | sh "git clean -fd" 77 | exec "rake pages" 78 | end 79 | -------------------------------------------------------------------------------- /lib/resque/server/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Resque. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 19 | <% if Resque.redis.namespace != :resque %> 20 | 21 | <%= Resque.redis.namespace %> 22 | 23 | <% end %> 24 |
25 | 26 | <% if @subtabs %> 27 | 32 | <% end %> 33 | 34 |
35 | <%= yield %> 36 |
37 | 38 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/resque/server/views/stats.erb: -------------------------------------------------------------------------------- 1 | <% @subtabs = %w( resque redis keys ) %> 2 | 3 | <% if params[:key] %> 4 | 5 | <%= partial resque.redis.type(params[:key]).eql?("string") ? :key_string : :key_sets %> 6 | 7 | <% elsif params[:id] == "resque" %> 8 | 9 |

<%= resque %>

10 | 11 | <% for key, value in resque.info.to_a.sort_by { |i| i[0].to_s } %> 12 | 13 | 16 | 19 | 20 | <% end %> 21 |
14 | <%= key %> 15 | 17 | <%= value %> 18 |
22 | 23 | <% elsif params[:id] == 'redis' %> 24 | 25 |

<%= resque.redis_id %>

26 | 27 | <% for key, value in resque.redis.info.to_a.sort_by { |i| i[0].to_s } %> 28 | 29 | 32 | 35 | 36 | <% end %> 37 |
30 | <%= key %> 31 | 33 | <%= value %> 34 |
38 | 39 | <% elsif params[:id] == 'keys' %> 40 | 41 |

Keys owned by <%= resque %>

42 |

(All keys are actually prefixed with "<%= Resque.redis.namespace %>:")

43 | 44 | 45 | 46 | 47 | 48 | 49 | <% for key in resque.keys.sort %> 50 | 51 | 54 | 55 | 56 | 57 | <% end %> 58 |
keytypesize
52 | "><%= key %> 53 | <%= resque.redis.type key %><%= redis_get_size key %>
59 | 60 | <% else %> 61 | 62 | <% end %> 63 | -------------------------------------------------------------------------------- /lib/resque/plugin.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | module Plugin 3 | extend self 4 | 5 | LintError = Class.new(RuntimeError) 6 | 7 | # Ensure that your plugin conforms to good hook naming conventions. 8 | # 9 | # Resque::Plugin.lint(MyResquePlugin) 10 | def lint(plugin) 11 | hooks = before_hooks(plugin) + around_hooks(plugin) + after_hooks(plugin) 12 | 13 | hooks.each do |hook| 14 | if hook =~ /perform$/ 15 | raise LintError, "#{plugin}.#{hook} is not namespaced" 16 | end 17 | end 18 | 19 | failure_hooks(plugin).each do |hook| 20 | if hook =~ /failure$/ 21 | raise LintError, "#{plugin}.#{hook} is not namespaced" 22 | end 23 | end 24 | end 25 | 26 | # Given an object, returns a list `before_perform` hook names. 27 | def before_hooks(job) 28 | job.methods.grep(/^before_perform/).sort 29 | end 30 | 31 | # Given an object, returns a list `around_perform` hook names. 32 | def around_hooks(job) 33 | job.methods.grep(/^around_perform/).sort 34 | end 35 | 36 | # Given an object, returns a list `after_perform` hook names. 37 | def after_hooks(job) 38 | job.methods.grep(/^after_perform/).sort 39 | end 40 | 41 | # Given an object, returns a list `on_failure` hook names. 42 | def failure_hooks(job) 43 | job.methods.grep(/^on_failure/).sort 44 | end 45 | 46 | # Given an object, returns a list `after_enqueue` hook names. 47 | def after_enqueue_hooks(job) 48 | job.methods.grep(/^after_enqueue/).sort 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/resque/failure/hoptoad.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'hoptoad_notifier' 3 | rescue LoadError 4 | raise "Can't find 'hoptoad_notifier' gem. Please add it to your Gemfile or install it." 5 | end 6 | 7 | module Resque 8 | module Failure 9 | # A Failure backend that sends exceptions raised by jobs to Hoptoad. 10 | # 11 | # To use it, put this code in an initializer, Rake task, or wherever: 12 | # 13 | # require 'resque/failure/hoptoad' 14 | # 15 | # Resque::Failure::Multiple.classes = [Resque::Failure::Redis, Resque::Failure::Hoptoad] 16 | # Resque::Failure.backend = Resque::Failure::Multiple 17 | # 18 | # Once you've configured resque to use the Hoptoad failure backend, 19 | # you'll want to setup an initializer to configure the Hoptoad. 20 | # 21 | # HoptoadNotifier.configure do |config| 22 | # config.api_key = 'your_key_here' 23 | # end 24 | # For more information see https://github.com/thoughtbot/hoptoad_notifier 25 | class Hoptoad < Base 26 | def self.configure(&block) 27 | Resque::Failure.backend = self 28 | HoptoadNotifier.configure(&block) 29 | end 30 | 31 | def self.count 32 | # We can't get the total # of errors from Hoptoad so we fake it 33 | # by asking Resque how many errors it has seen. 34 | Stat[:failed] 35 | end 36 | 37 | def save 38 | HoptoadNotifier.notify_or_ignore(exception, 39 | :parameters => { 40 | :payload_class => payload['class'].to_s, 41 | :payload_args => payload['args'].inspect 42 | } 43 | ) 44 | end 45 | 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /examples/god/resque.god: -------------------------------------------------------------------------------- 1 | rails_env = ENV['RAILS_ENV'] || "production" 2 | rails_root = ENV['RAILS_ROOT'] || "/data/github/current" 3 | num_workers = rails_env == 'production' ? 5 : 2 4 | 5 | num_workers.times do |num| 6 | God.watch do |w| 7 | w.dir = "#{rails_root}" 8 | w.name = "resque-#{num}" 9 | w.group = 'resque' 10 | w.interval = 30.seconds 11 | w.env = {"QUEUE"=>"critical,high,low", "RAILS_ENV"=>rails_env} 12 | w.start = "/usr/bin/rake -f #{rails_root}/Rakefile environment resque:work" 13 | 14 | w.uid = 'git' 15 | w.gid = 'git' 16 | 17 | # retart if memory gets too high 18 | w.transition(:up, :restart) do |on| 19 | on.condition(:memory_usage) do |c| 20 | c.above = 350.megabytes 21 | c.times = 2 22 | end 23 | end 24 | 25 | # determine the state on startup 26 | w.transition(:init, { true => :up, false => :start }) do |on| 27 | on.condition(:process_running) do |c| 28 | c.running = true 29 | end 30 | end 31 | 32 | # determine when process has finished starting 33 | w.transition([:start, :restart], :up) do |on| 34 | on.condition(:process_running) do |c| 35 | c.running = true 36 | c.interval = 5.seconds 37 | end 38 | 39 | # failsafe 40 | on.condition(:tries) do |c| 41 | c.times = 5 42 | c.transition = :start 43 | c.interval = 5.seconds 44 | end 45 | end 46 | 47 | # start if process is not running 48 | w.transition(:up, :start) do |on| 49 | on.condition(:process_running) do |c| 50 | c.running = false 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/resque/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'multi_json' 2 | 3 | module Resque 4 | # Methods used by various classes in Resque. 5 | module Helpers 6 | class DecodeException < StandardError; end 7 | 8 | # Direct access to the Redis instance. 9 | def redis 10 | Resque.redis 11 | end 12 | 13 | # Given a Ruby object, returns a string suitable for storage in a 14 | # queue. 15 | def encode(object) 16 | ::MultiJson.encode(object) 17 | end 18 | 19 | # Given a string, returns a Ruby object. 20 | def decode(object) 21 | return unless object 22 | 23 | begin 24 | ::MultiJson.decode(object) 25 | rescue ::MultiJson::DecodeError => e 26 | raise DecodeException, e 27 | end 28 | end 29 | 30 | # Given a word with dashes, returns a camel cased version of it. 31 | # 32 | # classify('job-name') # => 'JobName' 33 | def classify(dashed_word) 34 | dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join 35 | end 36 | 37 | # Given a camel cased word, returns the constant it represents 38 | # 39 | # constantize('JobName') # => JobName 40 | def constantize(camel_cased_word) 41 | camel_cased_word = camel_cased_word.to_s 42 | 43 | if camel_cased_word.include?('-') 44 | camel_cased_word = classify(camel_cased_word) 45 | end 46 | 47 | names = camel_cased_word.split('::') 48 | names.shift if names.empty? || names.first.empty? 49 | 50 | constant = Object 51 | names.each do |name| 52 | constant = constant.const_get(name) || constant.const_missing(name) 53 | end 54 | constant 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/resque/failure/redis.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | module Failure 3 | # A Failure backend that stores exceptions in Redis. Very simple but 4 | # works out of the box, along with support in the Resque web app. 5 | class Redis < Base 6 | def save 7 | data = { 8 | :failed_at => Time.now.strftime("%Y/%m/%d %H:%M:%S"), 9 | :payload => payload, 10 | :exception => exception.class.to_s, 11 | :error => exception.to_s, 12 | :backtrace => filter_backtrace(Array(exception.backtrace)), 13 | :worker => worker.to_s, 14 | :queue => queue 15 | } 16 | data = Resque.encode(data) 17 | Resque.redis.rpush(:failed, data) 18 | end 19 | 20 | def self.count 21 | Resque.redis.llen(:failed).to_i 22 | end 23 | 24 | def self.all(start = 0, count = 1) 25 | Resque.list_range(:failed, start, count) 26 | end 27 | 28 | def self.clear 29 | Resque.redis.del(:failed) 30 | end 31 | 32 | def self.requeue(index) 33 | item = all(index) 34 | item['retried_at'] = Time.now.strftime("%Y/%m/%d %H:%M:%S") 35 | Resque.redis.lset(:failed, index, Resque.encode(item)) 36 | Job.create(item['queue'], item['payload']['class'], *item['payload']['args']) 37 | end 38 | 39 | def self.remove(index) 40 | id = rand(0xffffff) 41 | Resque.redis.lset(:failed, index, id) 42 | Resque.redis.lrem(:failed, 1, id) 43 | end 44 | 45 | def filter_backtrace(backtrace) 46 | backtrace.first(backtrace.index {|item| item.include?('/lib/resque/job.rb')}) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/resque/failure/base.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | module Failure 3 | # All Failure classes are expected to subclass Base. 4 | # 5 | # When a job fails, a new instance of your Failure backend is created 6 | # and #save is called. 7 | class Base 8 | # The exception object raised by the failed job 9 | attr_accessor :exception 10 | 11 | # The worker object who detected the failure 12 | attr_accessor :worker 13 | 14 | # The string name of the queue from which the failed job was pulled 15 | attr_accessor :queue 16 | 17 | # The payload object associated with the failed job 18 | attr_accessor :payload 19 | 20 | def initialize(exception, worker, queue, payload) 21 | @exception = exception 22 | @worker = worker 23 | @queue = queue 24 | @payload = payload 25 | end 26 | 27 | # When a job fails, a new instance of your Failure backend is created 28 | # and #save is called. 29 | # 30 | # This is where you POST or PUT or whatever to your Failure service. 31 | def save 32 | end 33 | 34 | # The number of failures. 35 | def self.count 36 | 0 37 | end 38 | 39 | # Returns a paginated array of failure objects. 40 | def self.all(start = 0, count = 1) 41 | [] 42 | end 43 | 44 | # A URL where someone can go to view failures. 45 | def self.url 46 | end 47 | 48 | # Clear all failure objects 49 | def self.clear 50 | end 51 | 52 | def self.requeue(index) 53 | end 54 | 55 | def self.remove(index) 56 | end 57 | 58 | # Logging! 59 | def log(message) 60 | @worker.log(message) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /bin/resque: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') 4 | begin 5 | require 'redis-namespace' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'redis-namespace' 9 | end 10 | require 'resque' 11 | require 'optparse' 12 | 13 | parser = OptionParser.new do |opts| 14 | opts.banner = "Usage: resque [options] COMMAND" 15 | 16 | opts.separator "" 17 | opts.separator "Options:" 18 | 19 | opts.on("-r", "--redis [HOST:PORT]", "Redis connection string") do |host| 20 | Resque.redis = host 21 | end 22 | 23 | opts.on("-N", "--namespace [NAMESPACE]", "Redis namespace") do |namespace| 24 | Resque.redis.namespace = namespace 25 | end 26 | 27 | opts.on("-h", "--help", "Show this message") do 28 | puts opts 29 | exit 30 | end 31 | 32 | opts.separator "" 33 | opts.separator "Commands:" 34 | opts.separator " remove WORKER Removes a worker" 35 | opts.separator " kill WORKER Kills a worker" 36 | opts.separator " list Lists known workers" 37 | end 38 | 39 | def kill(worker) 40 | abort "** resque kill WORKER_ID" if worker.nil? 41 | pid = worker.split(':')[1].to_i 42 | 43 | begin 44 | Process.kill("KILL", pid) 45 | puts "** killed #{worker}" 46 | rescue Errno::ESRCH 47 | puts "** worker #{worker} not running" 48 | end 49 | 50 | remove worker 51 | end 52 | 53 | def remove(worker) 54 | abort "** resque remove WORKER_ID" if worker.nil? 55 | 56 | Resque.remove_worker(worker) 57 | puts "** removed #{worker}" 58 | end 59 | 60 | def list 61 | if Resque.workers.any? 62 | Resque.workers.each do |worker| 63 | puts "#{worker} (#{worker.state})" 64 | end 65 | else 66 | puts "None" 67 | end 68 | end 69 | 70 | parser.parse! 71 | 72 | case ARGV[0] 73 | when 'kill' 74 | kill ARGV[1] 75 | when 'remove' 76 | remove ARGV[1] 77 | when 'list' 78 | list 79 | else 80 | puts parser.help 81 | end 82 | -------------------------------------------------------------------------------- /resque.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift 'lib' 2 | require 'resque/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "resque" 6 | s.version = Resque::Version 7 | s.date = Time.now.strftime('%Y-%m-%d') 8 | s.summary = "Resque is a Redis-backed queueing system." 9 | s.homepage = "http://github.com/defunkt/resque" 10 | s.email = "chris@ozmm.org" 11 | s.authors = [ "Chris Wanstrath" ] 12 | 13 | s.files = %w( README.markdown Rakefile LICENSE HISTORY.md ) 14 | s.files += Dir.glob("lib/**/*") 15 | s.files += Dir.glob("bin/**/*") 16 | s.files += Dir.glob("man/**/*") 17 | s.files += Dir.glob("test/**/*") 18 | s.files += Dir.glob("tasks/**/*") 19 | s.executables = [ "resque", "resque-web" ] 20 | 21 | s.extra_rdoc_files = [ "LICENSE", "README.markdown" ] 22 | s.rdoc_options = ["--charset=UTF-8"] 23 | 24 | s.add_dependency "redis-namespace", "~> 1.0.2" 25 | s.add_dependency "vegas", "~> 0.1.2" 26 | s.add_dependency "sinatra", ">= 0.9.2" 27 | s.add_dependency "multi_json", "~> 1.0" 28 | 29 | s.description = < 2 | 3 | <% if queue = params[:id] %> 4 | 5 |

Pending jobs on <%= queue %>

6 |
" class='remove-queue'> 7 | 8 |
9 |

Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <%=size = resque.size(queue)%> jobs

10 | 11 | 12 | 13 | 14 | 15 | <% for job in (jobs = resque.peek(queue, start, 20)) %> 16 | 17 | 18 | 19 | 20 | <% end %> 21 | <% if jobs.empty? %> 22 | 23 | 24 | 25 | <% end %> 26 |
ClassArgs
<%= job['class'] %><%=h job['args'].inspect %>
There are no pending jobs in this queue
27 | <%= partial :next_more, :start => start, :size => size %> 28 | <% else %> 29 | 30 |

Queues

31 |

The list below contains all the registered queues with the number of jobs currently in the queue. Select a queue from above to view all jobs currently pending on the queue.

32 | 33 | 34 | 35 | 36 | 37 | <% for queue in resque.queues.sort_by { |q| q.to_s } %> 38 | 39 | 40 | 41 | 42 | <% end %> 43 | "> 44 | 45 | 46 | 47 |
NameJobs
"><%= queue %><%= resque.size queue %>
failed<%= Resque::Failure.count %>
48 | 49 | <% end %> 50 | -------------------------------------------------------------------------------- /docs/PLUGINS.md: -------------------------------------------------------------------------------- 1 | Resque Plugins 2 | ============== 3 | 4 | Resque encourages plugin development. For a list of available plugins, 5 | please see . 6 | 7 | The `docs/HOOKS.md` file included with Resque documents the available 8 | hooks you can use to add or change Resque functionality. This document 9 | describes best practice for plugins themselves. 10 | 11 | 12 | Version 13 | ------- 14 | 15 | Plugins should declare the major.minor version of Resque they are 16 | known to work with explicitly in their README. 17 | 18 | For example, if your plugin depends on features in Resque 2.1, please 19 | list "Depends on Resque 2.1" very prominently near the beginning of 20 | your README. 21 | 22 | Because Resque uses [Semantic Versioning][sv], you can safely make the 23 | following assumptions: 24 | 25 | * Your plugin will work with 2.2, 2.3, etc - no methods will be 26 | removed or changed, only added. 27 | * Your plugin might not work with 3.0+, as APIs may change or be 28 | removed. 29 | 30 | 31 | Namespace 32 | --------- 33 | 34 | All plugins should live under the `Resque::Plugins` module to avoid 35 | clashing with first class Resque constants or other Ruby libraries. 36 | 37 | Good: 38 | 39 | * Resque::Plugins::Lock 40 | * Resque::Plugins::FastRetry 41 | 42 | Bad: 43 | 44 | * Resque::Lock 45 | * ResqueQueue 46 | 47 | 48 | Gem Name 49 | -------- 50 | 51 | Gem names should be in the format of `resque-FEATURE`, where `FEATURE` 52 | succinctly describes the feature your plugin adds to Resque. 53 | 54 | Good: 55 | 56 | * resque-status 57 | * resque-scheduler 58 | 59 | Bad: 60 | 61 | * multi-queue 62 | * defunkt-resque-lock 63 | 64 | 65 | Hooks 66 | ----- 67 | 68 | Job hook names should be namespaced to work properly. 69 | 70 | Good: 71 | 72 | * before_perform_lock 73 | * around_perform_check_status 74 | 75 | Bad: 76 | 77 | * before_perform 78 | * on_failure 79 | 80 | 81 | Lint 82 | ---- 83 | 84 | Plugins should test compliance to this document using the 85 | `Resque::Plugin.lint` method. 86 | 87 | For example: 88 | 89 | assert_nothing_raised do 90 | Resque::Plugin.lint(Resque::Plugins::Lock) 91 | end 92 | 93 | [sv]: http://semver.org/ 94 | -------------------------------------------------------------------------------- /lib/resque/server/public/ranger.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var poll_interval = 2 3 | 4 | var relatizer = function(){ 5 | var dt = $(this).text(), relatized = $.relatizeDate(this) 6 | if ($(this).parents("a").length > 0 || $(this).is("a")) { 7 | $(this).relatizeDate() 8 | if (!$(this).attr('title')) { 9 | $(this).attr('title', dt) 10 | } 11 | } else { 12 | $(this) 13 | .text('') 14 | .append( $('') 15 | .append('' + dt + 16 | '' + 17 | relatized + '') ) 18 | } 19 | }; 20 | 21 | $('.time').each(relatizer); 22 | 23 | $('.time a.toggle_format .date_time').hide() 24 | 25 | var format_toggler = function(){ 26 | $('.time a.toggle_format span').toggle() 27 | $(this).attr('title', $('span:hidden',this).text()) 28 | return false 29 | }; 30 | 31 | $('.time a.toggle_format').click(format_toggler); 32 | 33 | $('.backtrace').click(function() { 34 | $(this).next().toggle() 35 | return false 36 | }) 37 | 38 | $('a[rel=poll]').click(function() { 39 | var href = $(this).attr('href') 40 | $(this).parent().text('Starting...') 41 | $("#main").addClass('polling') 42 | 43 | setInterval(function() { 44 | $.ajax({dataType: 'text', type: 'get', url: href, success: function(data) { 45 | $('#main').html(data) 46 | $('#main .time').relatizeDate() 47 | }}) 48 | }, poll_interval * 1000) 49 | 50 | return false 51 | }) 52 | 53 | $('ul.failed li').hover(function() { 54 | $(this).addClass('hover'); 55 | }, function() { 56 | $(this).removeClass('hover'); 57 | }) 58 | 59 | $('ul.failed a[rel=retry]').click(function() { 60 | var href = $(this).attr('href'); 61 | $(this).text('Retrying...'); 62 | var parent = $(this).parent(); 63 | $.ajax({dataType: 'text', type: 'get', url: href, success: function(data) { 64 | parent.html('Retried ' + data + ''); 65 | relatizer.apply($('.time', parent)); 66 | $('.date_time', parent).hide(); 67 | $('a.toggle_format span', parent).click(format_toggler); 68 | }}); 69 | return false; 70 | }) 71 | 72 | 73 | }) -------------------------------------------------------------------------------- /lib/resque/failure.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | # The Failure module provides an interface for working with different 3 | # failure backends. 4 | # 5 | # You can use it to query the failure backend without knowing which specific 6 | # backend is being used. For instance, the Resque web app uses it to display 7 | # stats and other information. 8 | module Failure 9 | # Creates a new failure, which is delegated to the appropriate backend. 10 | # 11 | # Expects a hash with the following keys: 12 | # :exception - The Exception object 13 | # :worker - The Worker object who is reporting the failure 14 | # :queue - The string name of the queue from which the job was pulled 15 | # :payload - The job's payload 16 | def self.create(options = {}) 17 | backend.new(*options.values_at(:exception, :worker, :queue, :payload)).save 18 | end 19 | 20 | # 21 | # Sets the current backend. Expects a class descendent of 22 | # `Resque::Failure::Base`. 23 | # 24 | # Example use: 25 | # require 'resque/failure/hoptoad' 26 | # Resque::Failure.backend = Resque::Failure::Hoptoad 27 | def self.backend=(backend) 28 | @backend = backend 29 | end 30 | 31 | # Returns the current backend class. If none has been set, falls 32 | # back to `Resque::Failure::Redis` 33 | def self.backend 34 | return @backend if @backend 35 | require 'resque/failure/redis' 36 | @backend = Failure::Redis 37 | end 38 | 39 | # Returns the int count of how many failures we have seen. 40 | def self.count 41 | backend.count 42 | end 43 | 44 | # Returns an array of all the failures, paginated. 45 | # 46 | # `start` is the int of the first item in the page, `count` is the 47 | # number of items to return. 48 | def self.all(start = 0, count = 1) 49 | backend.all(start, count) 50 | end 51 | 52 | # The string url of the backend's web interface, if any. 53 | def self.url 54 | backend.url 55 | end 56 | 57 | # Clear all failure jobs 58 | def self.clear 59 | backend.clear 60 | end 61 | 62 | def self.requeue(index) 63 | backend.requeue(index) 64 | end 65 | 66 | def self.remove(index) 67 | backend.remove(index) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /examples/demo/README.markdown: -------------------------------------------------------------------------------- 1 | Resque Demo 2 | ----------- 3 | 4 | This is a dirt simple Resque setup for you to play with. 5 | 6 | 7 | ### Starting the Demo App 8 | 9 | Here's how to run the Sinatra app: 10 | 11 | $ git clone git://github.com/defunkt/resque.git 12 | $ cd resque/examples/demo 13 | $ rackup config.ru 14 | $ open http://localhost:9292/ 15 | 16 | Click 'Create New Job' a few times. You should see the number of 17 | pending jobs rising. 18 | 19 | 20 | ### Starting the Demo Worker 21 | 22 | Now in another shell terminal start the worker: 23 | 24 | $ cd resque/examples/demo 25 | $ VERBOSE=true QUEUE=default rake resque:work 26 | 27 | You should see the following output: 28 | 29 | *** Starting worker hostname:90185:default 30 | *** got: (Job{default} | Demo::Job | [{}]) 31 | Processed a job! 32 | *** done: (Job{default} | Demo::Job | [{}]) 33 | 34 | You can also use `VVERBOSE` (very verbose) if you want to see more: 35 | 36 | $ VERBOSE=true QUEUE=default rake resque:work 37 | *** Starting worker hostname:90399:default 38 | ** [05:55:09 2009-09-16] 90399: Registered signals 39 | ** [05:55:09 2009-09-16] 90399: Checking default 40 | ** [05:55:09 2009-09-16] 90399: Found job on default 41 | ** [05:55:09 2009-09-16] 90399: got: (Job{default} | Demo::Job | [{}]) 42 | ** [05:55:09 2009-09-16] 90399: resque: Forked 90401 at 1253141709 43 | ** [05:55:09 2009-09-16] 90401: resque: Processing default since 1253141709 44 | Processed a job! 45 | ** [05:55:10 2009-09-16] 90401: done: (Job{default} | Demo::Job | [{}]) 46 | 47 | Notice that our workers `require 'job'` in our `Rakefile`. This 48 | ensures they have our app loaded and can access the job classes. 49 | 50 | 51 | ### Starting the Resque frontend 52 | 53 | Great, now let's check out the Resque frontend. Either click on 'View 54 | Resque' in your web browser or run: 55 | 56 | $ open http://localhost:9292/resque/ 57 | 58 | You should see the Resque web frontend. 404 page? Don't forget the 59 | trailing slash! 60 | 61 | 62 | ### config.ru 63 | 64 | The `config.ru` shows you how to mount multiple Rack apps. Resque 65 | should work fine on a subpath - feel free to load it up in your 66 | Passenger app and protect it with some basic auth. 67 | 68 | 69 | ### That's it! 70 | 71 | Click around, add some more queues, add more jobs, do whatever, have fun. 72 | -------------------------------------------------------------------------------- /lib/resque/server/views/failed.erb: -------------------------------------------------------------------------------- 1 | <%start = params[:start].to_i %> 2 | <%failed = Resque::Failure.all(start, 20)%> 3 | <% index = 0 %> 4 | <% date_format = "%Y/%m/%d %T %z" %> 5 | 6 |

Failed Jobs

7 | <%unless failed.empty?%> 8 |
9 | 10 |
11 | <%end%> 12 | 13 |

Showing <%=start%> to <%= start + 20 %> of <%= size = Resque::Failure.count %> jobs

14 | 15 |
62 | 63 | <%= partial :next_more, :start => start, :size => size %> 64 | 65 | -------------------------------------------------------------------------------- /lib/resque/server/views/working.erb: -------------------------------------------------------------------------------- 1 | <% if params[:id] && (worker = Resque::Worker.find(params[:id])) && worker.job %> 2 |

<%= worker %>'s job

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% host, pid, _ = worker.to_s.split(':') %> 16 | 17 | <% data = worker.job %> 18 | <% queue = data['queue'] %> 19 | 20 | 21 | 24 | 25 | 26 |
 WhereQueueStartedClassArgs
working"><%= host %>:<%= pid %>"><%= queue %><%= data['run_at'] %> 22 | <%= data['payload']['class'] %> 23 | <%=h data['payload']['args'].inspect %>
27 | 28 | <% else %> 29 | 30 | <% 31 | workers = resque.working 32 | jobs = workers.collect {|w| w.job } 33 | worker_jobs = workers.zip(jobs) 34 | worker_jobs = worker_jobs.reject { |w, j| w.idle? } 35 | %> 36 | 37 |

<%= worker_jobs.size %> of <%= resque.workers.size %> Workers Working

38 |

The list below contains all workers which are currently running a job.

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | <% if worker_jobs.empty? %> 47 | 48 | 49 | 50 | <% end %> 51 | 52 | <% worker_jobs.sort_by {|w, j| j['run_at'] ? j['run_at'] : '' }.each do |worker, job| %> 53 | 54 | 55 | <% host, pid, queues = worker.to_s.split(':') %> 56 | 57 | 60 | 68 | 69 | <% end %> 70 |
 WhereQueueProcessing
Nothing is happening right now...
<%= state %>"><%= host %>:<%= pid %> 58 | "><%= job['queue'] %> 59 | 61 | <% if job['queue'] %> 62 | <%= job['payload']['class'] %> 63 | "><%= job['run_at'] %> 64 | <% else %> 65 | Waiting for a job... 66 | <% end %> 67 |
71 | 72 | <% end %> 73 | -------------------------------------------------------------------------------- /test/plugin_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | context "Resque::Plugin finding hooks" do 4 | module SimplePlugin 5 | extend self 6 | def before_perform1; end 7 | def before_perform; end 8 | def before_perform2; end 9 | def after_perform1; end 10 | def after_perform; end 11 | def after_perform2; end 12 | def perform; end 13 | def around_perform1; end 14 | def around_perform; end 15 | def around_perform2; end 16 | def on_failure1; end 17 | def on_failure; end 18 | def on_failure2; end 19 | end 20 | 21 | test "before_perform hooks are found and sorted" do 22 | assert_equal ["before_perform", "before_perform1", "before_perform2"], Resque::Plugin.before_hooks(SimplePlugin).map {|m| m.to_s} 23 | end 24 | 25 | test "after_perform hooks are found and sorted" do 26 | assert_equal ["after_perform", "after_perform1", "after_perform2"], Resque::Plugin.after_hooks(SimplePlugin).map {|m| m.to_s} 27 | end 28 | 29 | test "around_perform hooks are found and sorted" do 30 | assert_equal ["around_perform", "around_perform1", "around_perform2"], Resque::Plugin.around_hooks(SimplePlugin).map {|m| m.to_s} 31 | end 32 | 33 | test "on_failure hooks are found and sorted" do 34 | assert_equal ["on_failure", "on_failure1", "on_failure2"], Resque::Plugin.failure_hooks(SimplePlugin).map {|m| m.to_s} 35 | end 36 | end 37 | 38 | context "Resque::Plugin linting" do 39 | module ::BadBefore 40 | def self.before_perform; end 41 | end 42 | module ::BadAfter 43 | def self.after_perform; end 44 | end 45 | module ::BadAround 46 | def self.around_perform; end 47 | end 48 | module ::BadFailure 49 | def self.on_failure; end 50 | end 51 | 52 | test "before_perform must be namespaced" do 53 | begin 54 | Resque::Plugin.lint(BadBefore) 55 | assert false, "should have failed" 56 | rescue Resque::Plugin::LintError => e 57 | assert_equal "BadBefore.before_perform is not namespaced", e.message 58 | end 59 | end 60 | 61 | test "after_perform must be namespaced" do 62 | begin 63 | Resque::Plugin.lint(BadAfter) 64 | assert false, "should have failed" 65 | rescue Resque::Plugin::LintError => e 66 | assert_equal "BadAfter.after_perform is not namespaced", e.message 67 | end 68 | end 69 | 70 | test "around_perform must be namespaced" do 71 | begin 72 | Resque::Plugin.lint(BadAround) 73 | assert false, "should have failed" 74 | rescue Resque::Plugin::LintError => e 75 | assert_equal "BadAround.around_perform is not namespaced", e.message 76 | end 77 | end 78 | 79 | test "on_failure must be namespaced" do 80 | begin 81 | Resque::Plugin.lint(BadFailure) 82 | assert false, "should have failed" 83 | rescue Resque::Plugin::LintError => e 84 | assert_equal "BadFailure.on_failure is not namespaced", e.message 85 | end 86 | end 87 | 88 | module GoodBefore 89 | def self.before_perform1; end 90 | end 91 | module GoodAfter 92 | def self.after_perform1; end 93 | end 94 | module GoodAround 95 | def self.around_perform1; end 96 | end 97 | module GoodFailure 98 | def self.on_failure1; end 99 | end 100 | 101 | test "before_perform1 is an ok name" do 102 | Resque::Plugin.lint(GoodBefore) 103 | end 104 | 105 | test "after_perform1 is an ok name" do 106 | Resque::Plugin.lint(GoodAfter) 107 | end 108 | 109 | test "around_perform1 is an ok name" do 110 | Resque::Plugin.lint(GoodAround) 111 | end 112 | 113 | test "on_failure1 is an ok name" do 114 | Resque::Plugin.lint(GoodFailure) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | Bundler.setup(:default, :test) 4 | Bundler.require(:default, :test) 5 | 6 | dir = File.dirname(File.expand_path(__FILE__)) 7 | $LOAD_PATH.unshift dir + '/../lib' 8 | $TESTING = true 9 | require 'test/unit' 10 | 11 | begin 12 | require 'leftright' 13 | rescue LoadError 14 | end 15 | 16 | 17 | # 18 | # make sure we can run redis 19 | # 20 | 21 | if !system("which redis-server") 22 | puts '', "** can't find `redis-server` in your path" 23 | puts "** try running `sudo rake install`" 24 | abort '' 25 | end 26 | 27 | 28 | # 29 | # start our own redis when the tests start, 30 | # kill it when they end 31 | # 32 | 33 | at_exit do 34 | next if $! 35 | 36 | if defined?(MiniTest) 37 | exit_code = MiniTest::Unit.new.run(ARGV) 38 | else 39 | exit_code = Test::Unit::AutoRunner.run 40 | end 41 | 42 | pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0] 43 | puts "Killing test redis server..." 44 | `rm -f #{dir}/dump.rdb` 45 | Process.kill("KILL", pid.to_i) 46 | exit exit_code 47 | end 48 | 49 | puts "Starting redis for testing at localhost:9736..." 50 | `redis-server #{dir}/redis-test.conf` 51 | Resque.redis = 'localhost:9736' 52 | 53 | 54 | ## 55 | # test/spec/mini 3 56 | # http://gist.github.com/25455 57 | # chris@ozmm.org 58 | # 59 | def context(*args, &block) 60 | return super unless (name = args.first) && block 61 | require 'test/unit' 62 | klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do 63 | def self.test(name, &block) 64 | define_method("test_#{name.gsub(/\W/,'_')}", &block) if block 65 | end 66 | def self.xtest(*args) end 67 | def self.setup(&block) define_method(:setup, &block) end 68 | def self.teardown(&block) define_method(:teardown, &block) end 69 | end 70 | (class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') } 71 | klass.class_eval &block 72 | # XXX: In 1.8.x, not all tests will run unless anonymous classes are kept in scope. 73 | ($test_classes ||= []) << klass 74 | end 75 | 76 | ## 77 | # Helper to perform job classes 78 | # 79 | module PerformJob 80 | def perform_job(klass, *args) 81 | resque_job = Resque::Job.new(:testqueue, 'class' => klass, 'args' => args) 82 | resque_job.perform 83 | end 84 | end 85 | 86 | # 87 | # fixture classes 88 | # 89 | 90 | class SomeJob 91 | def self.perform(repo_id, path) 92 | end 93 | end 94 | 95 | class SomeIvarJob < SomeJob 96 | @queue = :ivar 97 | end 98 | 99 | class SomeMethodJob < SomeJob 100 | def self.queue 101 | :method 102 | end 103 | end 104 | 105 | class BadJob 106 | def self.perform 107 | raise "Bad job!" 108 | end 109 | end 110 | 111 | class GoodJob 112 | def self.perform(name) 113 | "Good job, #{name}" 114 | end 115 | end 116 | 117 | class BadJobWithSyntaxError 118 | def self.perform 119 | raise SyntaxError, "Extra Bad job!" 120 | end 121 | end 122 | 123 | class BadFailureBackend < Resque::Failure::Base 124 | def save 125 | raise Exception.new("Failure backend error") 126 | end 127 | end 128 | 129 | def with_failure_backend(failure_backend, &block) 130 | previous_backend = Resque::Failure.backend 131 | Resque::Failure.backend = failure_backend 132 | yield block 133 | ensure 134 | Resque::Failure.backend = previous_backend 135 | end 136 | 137 | class Time 138 | # Thanks, Timecop 139 | class << self 140 | alias_method :now_without_mock_time, :now 141 | 142 | def now_with_mock_time 143 | $fake_time || now_without_mock_time 144 | end 145 | 146 | alias_method :now, :now_with_mock_time 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/resque/server/public/jquery.relatize_date.js: -------------------------------------------------------------------------------- 1 | // All credit goes to Rick Olson. 2 | (function($) { 3 | $.fn.relatizeDate = function() { 4 | return $(this).each(function() { 5 | if ($(this).hasClass( 'relatized' )) return 6 | $(this).text( $.relatizeDate(this) ).addClass( 'relatized' ) 7 | }) 8 | } 9 | 10 | $.relatizeDate = function(element) { 11 | return $.relatizeDate.timeAgoInWords( new Date($(element).text()) ) 12 | } 13 | 14 | // shortcut 15 | $r = $.relatizeDate 16 | 17 | $.extend($.relatizeDate, { 18 | shortDays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], 19 | days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 20 | shortMonths: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ], 21 | months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ], 22 | 23 | /** 24 | * Given a formatted string, replace the necessary items and return. 25 | * Example: Time.now().strftime("%B %d, %Y") => February 11, 2008 26 | * @param {String} format The formatted string used to format the results 27 | */ 28 | strftime: function(date, format) { 29 | var day = date.getDay(), month = date.getMonth(); 30 | var hours = date.getHours(), minutes = date.getMinutes(); 31 | 32 | var pad = function(num) { 33 | var string = num.toString(10); 34 | return new Array((2 - string.length) + 1).join('0') + string 35 | }; 36 | 37 | return format.replace(/\%([aAbBcdHImMpSwyY])/g, function(part) { 38 | switch(part[1]) { 39 | case 'a': return $r.shortDays[day]; break; 40 | case 'A': return $r.days[day]; break; 41 | case 'b': return $r.shortMonths[month]; break; 42 | case 'B': return $r.months[month]; break; 43 | case 'c': return date.toString(); break; 44 | case 'd': return pad(date.getDate()); break; 45 | case 'H': return pad(hours); break; 46 | case 'I': return pad((hours + 12) % 12); break; 47 | case 'm': return pad(month + 1); break; 48 | case 'M': return pad(minutes); break; 49 | case 'p': return hours > 12 ? 'PM' : 'AM'; break; 50 | case 'S': return pad(date.getSeconds()); break; 51 | case 'w': return day; break; 52 | case 'y': return pad(date.getFullYear() % 100); break; 53 | case 'Y': return date.getFullYear().toString(); break; 54 | } 55 | }) 56 | }, 57 | 58 | timeAgoInWords: function(targetDate, includeTime) { 59 | return $r.distanceOfTimeInWords(targetDate, new Date(), includeTime); 60 | }, 61 | 62 | /** 63 | * Return the distance of time in words between two Date's 64 | * Example: '5 days ago', 'about an hour ago' 65 | * @param {Date} fromTime The start date to use in the calculation 66 | * @param {Date} toTime The end date to use in the calculation 67 | * @param {Boolean} Include the time in the output 68 | */ 69 | distanceOfTimeInWords: function(fromTime, toTime, includeTime) { 70 | var delta = parseInt((toTime.getTime() - fromTime.getTime()) / 1000); 71 | if (delta < 60) { 72 | return 'just now'; 73 | } else if (delta < 120) { 74 | return 'about a minute ago'; 75 | } else if (delta < (45*60)) { 76 | return (parseInt(delta / 60)).toString() + ' minutes ago'; 77 | } else if (delta < (120*60)) { 78 | return 'about an hour ago'; 79 | } else if (delta < (24*60*60)) { 80 | return 'about ' + (parseInt(delta / 3600)).toString() + ' hours ago'; 81 | } else if (delta < (48*60*60)) { 82 | return '1 day ago'; 83 | } else { 84 | var days = (parseInt(delta / 86400)).toString(); 85 | if (days > 5) { 86 | var fmt = '%B %d, %Y' 87 | if (includeTime) fmt += ' %I:%M %p' 88 | return $r.strftime(fromTime, fmt); 89 | } else { 90 | return days + " days ago" 91 | } 92 | } 93 | } 94 | }) 95 | })(jQuery); 96 | -------------------------------------------------------------------------------- /lib/resque/server/views/workers.erb: -------------------------------------------------------------------------------- 1 | <% @subtabs = worker_hosts.keys.sort unless worker_hosts.size == 1 %> 2 | 3 | <% if params[:id] && worker = Resque::Worker.find(params[:id]) %> 4 | 5 |

Worker <%= worker %>

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <% host, pid, queues = worker.to_s.split(':') %> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 36 | 37 |
 HostPidStartedQueuesProcessedFailedProcessing
<%= state %><%= host %><%= pid %><%= worker.started %><%= queues.split(',').map { |q| '' + q + ''}.join('') %><%= worker.processed %><%= worker.failed %> 28 | <% data = worker.processing || {} %> 29 | <% if data['queue'] %> 30 | <%= data['payload']['class'] %> 31 | "><%= data['run_at'] %> 32 | <% else %> 33 | Waiting for a job... 34 | <% end %> 35 |
38 | 39 | <% elsif params[:id] && !worker_hosts.keys.include?(params[:id]) && params[:id] != 'all' %> 40 | 41 |

Worker doesn't exist

42 | 43 | <% elsif worker_hosts.size == 1 || params[:id] %> 44 | 45 | <% if worker_hosts.size == 1 || params[:id] == 'all' %> 46 | <% workers = Resque.workers %> 47 | <% else %> 48 | <% workers = worker_hosts[params[:id]].map { |id| Resque::Worker.find(id) } %> 49 | <% end %> 50 | 51 |

<%= workers.size %> Workers

52 |

The workers listed below are all registered as active on your system.

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | <% for worker in (workers = workers.sort_by { |w| w.to_s }) %> 61 | 62 | 63 | 64 | <% host, pid, queues = worker.to_s.split(':') %> 65 | 66 | 67 | 68 | 77 | 78 | <% end %> 79 | <% if workers.empty? %> 80 | 81 | 82 | 83 | <% end %> 84 |
 WhereQueuesProcessing
<%= state %>"><%= host %>:<%= pid %><%= queues.split(',').map { |q| '' + q + ''}.join('') %> 69 | <% data = worker.processing || {} %> 70 | <% if data['queue'] %> 71 | <%= data['payload']['class'] %> 72 | "><%= data['run_at'] %> 73 | <% else %> 74 | Waiting for a job... 75 | <% end %> 76 |
There are no registered workers
85 | <%=poll%> 86 | 87 | <% else %> 88 | <% @subtabs = [] %> 89 |

Workers

90 |

The hostnames below all have registered workers. Select a hostname to view its workers, or "all" to see all workers.

91 | 92 | 93 | 94 | 95 | 96 | <% for hostname, workers in worker_hosts.sort_by { |h,w| h } %> 97 | 98 | 99 | 100 | 101 | <% end %> 102 | 103 | 104 | 105 | 106 |
HostnameWorkers
"><%= hostname %><%= workers.size %>
">all workers<%= Resque.workers.size %>
107 | 108 | 109 | <% end %> 110 | -------------------------------------------------------------------------------- /docs/HOOKS.md: -------------------------------------------------------------------------------- 1 | Resque Hooks 2 | ============ 3 | 4 | You can customize Resque or write plugins using its hook API. In many 5 | cases you can use a hook rather than mess with Resque's internals. 6 | 7 | For a list of available plugins see 8 | . 9 | 10 | 11 | Worker Hooks 12 | ------------ 13 | 14 | If you wish to have a Proc called before the worker forks for the 15 | first time, you can add it in the initializer like so: 16 | 17 | Resque.before_first_fork do 18 | puts "Call me once before the worker forks the first time" 19 | end 20 | 21 | You can also run a hook before _every_ fork: 22 | 23 | Resque.before_fork do |job| 24 | puts "Call me before the worker forks" 25 | end 26 | 27 | The `before_fork` hook will be run in the **parent** process. So, be 28 | careful - any changes you make will be permanent for the lifespan of 29 | the worker. 30 | 31 | And after forking: 32 | 33 | Resque.after_fork do |job| 34 | puts "Call me after the worker forks" 35 | end 36 | 37 | The `after_fork` hook will be run in the child process and is passed 38 | the current job. Any changes you make, therefor, will only live as 39 | long as the job currently being processes. 40 | 41 | All worker hooks can also be set using a setter, e.g. 42 | 43 | Resque.after_fork = proc { puts "called" } 44 | 45 | 46 | Job Hooks 47 | --------- 48 | 49 | Plugins can utilize job hooks to provide additional behavior. A job 50 | hook is a method name in the following format: 51 | 52 | HOOKNAME_IDENTIFIER 53 | 54 | For example, a `before_perform` hook which adds locking may be defined 55 | like this: 56 | 57 | def before_perform_with_lock(*args) 58 | set_lock! 59 | end 60 | 61 | Once this hook is made available to your job (either by way of 62 | inheritence or `extend`), it will be run before the job's `perform` 63 | method is called. Hooks of each type are executed in alphabetical order, 64 | so `before_perform_a` will always be executed before `before_perform_b`. 65 | An unnamed hook (`before_perform`) will be executed first. 66 | 67 | The available hooks are: 68 | 69 | * `after_enqueue`: Called with the job args after a job is placed on the queue. 70 | Any exception raised propagates up to the code which queued the job. 71 | 72 | * `before_perform`: Called with the job args before perform. If it raises 73 | `Resque::Job::DontPerform`, the job is aborted. If other exceptions 74 | are raised, they will be propagated up the the `Resque::Failure` 75 | backend. 76 | 77 | * `after_perform`: Called with the job args after it performs. Uncaught 78 | exceptions will propagate up to the `Resque::Failure` backend. 79 | 80 | * `around_perform`: Called with the job args. It is expected to yield in order 81 | to perform the job (but is not required to do so). It may handle exceptions 82 | thrown by `perform`, but any that are not caught will propagate up to the 83 | `Resque::Failure` backend. 84 | 85 | * `on_failure`: Called with the exception and job args if any exception occurs 86 | while performing the job (or hooks). 87 | 88 | Hooks are easily implemented with superclasses or modules. A superclass could 89 | look something like this. 90 | 91 | class LoggedJob 92 | def self.before_perform_log_job(*args) 93 | Logger.info "About to perform #{self} with #{args.inspect}" 94 | end 95 | end 96 | 97 | class MyJob < LoggedJob 98 | def self.perform(*args) 99 | ... 100 | end 101 | end 102 | 103 | Modules are even better because jobs can use many of them. 104 | 105 | module ScaledJob 106 | def after_enqueue_scale_workers(*args) 107 | Logger.info "Scaling worker count up" 108 | Scaler.up! if Redis.info[:pending].to_i > 25 109 | end 110 | end 111 | 112 | module LoggedJob 113 | def before_perform_log_job(*args) 114 | Logger.info "About to perform #{self} with #{args.inspect}" 115 | end 116 | end 117 | 118 | module RetriedJob 119 | def on_failure_retry(e, *args) 120 | Logger.info "Performing #{self} caused an exception (#{e}). Retrying..." 121 | Resque.enqueue self, *args 122 | end 123 | end 124 | 125 | class MyJob 126 | extend LoggedJob 127 | extend RetriedJob 128 | extend ScaledJob 129 | def self.perform(*args) 130 | ... 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/tasks/redis.rake: -------------------------------------------------------------------------------- 1 | # Inspired by rabbitmq.rake the Redbox project at http://github.com/rick/redbox/tree/master 2 | require 'fileutils' 3 | require 'open-uri' 4 | require 'pathname' 5 | 6 | class RedisRunner 7 | def self.redis_dir 8 | @redis_dir ||= if ENV['PREFIX'] 9 | Pathname.new(ENV['PREFIX']) 10 | else 11 | Pathname.new(`which redis-server`) + '..' + '..' 12 | end 13 | end 14 | 15 | def self.bin_dir 16 | redis_dir + 'bin' 17 | end 18 | 19 | def self.config 20 | @config ||= if File.exists?(redis_dir + 'etc/redis.conf') 21 | redis_dir + 'etc/redis.conf' 22 | else 23 | redis_dir + '../etc/redis.conf' 24 | end 25 | end 26 | 27 | def self.dtach_socket 28 | '/tmp/redis.dtach' 29 | end 30 | 31 | # Just check for existance of dtach socket 32 | def self.running? 33 | File.exists? dtach_socket 34 | end 35 | 36 | def self.start 37 | puts 'Detach with Ctrl+\ Re-attach with rake redis:attach' 38 | sleep 1 39 | command = "#{bin_dir}/dtach -A #{dtach_socket} #{bin_dir}/redis-server #{config}" 40 | sh command 41 | end 42 | 43 | def self.attach 44 | exec "#{bin_dir}/dtach -a #{dtach_socket}" 45 | end 46 | 47 | def self.stop 48 | sh 'echo "SHUTDOWN" | nc localhost 6379' 49 | end 50 | end 51 | 52 | INSTALL_DIR = ENV['INSTALL_DIR'] || '/tmp/redis' 53 | 54 | namespace :redis do 55 | desc 'About redis' 56 | task :about do 57 | puts "\nSee http://code.google.com/p/redis/ for information about redis.\n\n" 58 | end 59 | 60 | desc 'Start redis' 61 | task :start do 62 | RedisRunner.start 63 | end 64 | 65 | desc 'Stop redis' 66 | task :stop do 67 | RedisRunner.stop 68 | end 69 | 70 | desc 'Restart redis' 71 | task :restart do 72 | RedisRunner.stop 73 | RedisRunner.start 74 | end 75 | 76 | desc 'Attach to redis dtach socket' 77 | task :attach do 78 | RedisRunner.attach 79 | end 80 | 81 | desc <<-DOC 82 | Install the latest verison of Redis from Github (requires git, duh). 83 | Use INSTALL_DIR env var like "rake redis:install INSTALL_DIR=~/tmp" 84 | in order to get an alternate location for your install files. 85 | DOC 86 | 87 | task :install => [:about, :download, :make] do 88 | bin_dir = '/usr/bin' 89 | conf_dir = '/etc' 90 | 91 | if ENV['PREFIX'] 92 | bin_dir = "#{ENV['PREFIX']}/bin" 93 | sh "mkdir -p #{bin_dir}" unless File.exists?("#{bin_dir}") 94 | 95 | conf_dir = "#{ENV['PREFIX']}/etc" 96 | sh "mkdir -p #{conf_dir}" unless File.exists?("#{conf_dir}") 97 | end 98 | 99 | %w(redis-benchmark redis-cli redis-server).each do |bin| 100 | sh "cp #{INSTALL_DIR}/src/#{bin} #{bin_dir}" 101 | end 102 | 103 | puts "Installed redis-benchmark, redis-cli and redis-server to #{bin_dir}" 104 | 105 | unless File.exists?("#{conf_dir}/redis.conf") 106 | sh "cp #{INSTALL_DIR}/redis.conf #{conf_dir}/redis.conf" 107 | puts "Installed redis.conf to #{conf_dir} \n You should look at this file!" 108 | end 109 | end 110 | 111 | task :make do 112 | sh "cd #{INSTALL_DIR}/src && make clean" 113 | sh "cd #{INSTALL_DIR}/src && make" 114 | end 115 | 116 | desc "Download package" 117 | task :download do 118 | sh "rm -rf #{INSTALL_DIR}/" if File.exists?("#{INSTALL_DIR}/.svn") 119 | sh "git clone git://github.com/antirez/redis.git #{INSTALL_DIR}" unless File.exists?(INSTALL_DIR) 120 | sh "cd #{INSTALL_DIR} && git pull" if File.exists?("#{INSTALL_DIR}/.git") 121 | end 122 | end 123 | 124 | namespace :dtach do 125 | desc 'About dtach' 126 | task :about do 127 | puts "\nSee http://dtach.sourceforge.net/ for information about dtach.\n\n" 128 | end 129 | 130 | desc 'Install dtach 0.8 from source' 131 | task :install => [:about, :download, :make] do 132 | 133 | bin_dir = "/usr/bin" 134 | 135 | 136 | if ENV['PREFIX'] 137 | bin_dir = "#{ENV['PREFIX']}/bin" 138 | sh "mkdir -p #{bin_dir}" unless File.exists?("#{bin_dir}") 139 | end 140 | 141 | sh "cp #{INSTALL_DIR}/dtach-0.8/dtach #{bin_dir}" 142 | end 143 | 144 | task :make do 145 | sh "cd #{INSTALL_DIR}/dtach-0.8/ && ./configure && make" 146 | end 147 | 148 | desc "Download package" 149 | task :download do 150 | unless File.exists?("#{INSTALL_DIR}/dtach-0.8.tar.gz") 151 | require 'net/http' 152 | 153 | url = 'http://downloads.sourceforge.net/project/dtach/dtach/0.8/dtach-0.8.tar.gz' 154 | open("#{INSTALL_DIR}/dtach-0.8.tar.gz", 'wb') do |file| file.write(open(url).read) end 155 | end 156 | 157 | unless File.directory?("#{INSTALL_DIR}/dtach-0.8") 158 | sh "cd #{INSTALL_DIR} && tar xzf dtach-0.8.tar.gz" 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /test/redis-test.conf: -------------------------------------------------------------------------------- 1 | # Redis configuration file example 2 | 3 | # By default Redis does not run as a daemon. Use 'yes' if you need it. 4 | # Note that Redis will write a pid file in /var/run/redis.pid when daemonized. 5 | daemonize yes 6 | 7 | # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default. 8 | # You can specify a custom pid file location here. 9 | pidfile ./test/redis-test.pid 10 | 11 | # Accept connections on the specified port, default is 6379 12 | port 9736 13 | 14 | # If you want you can bind a single interface, if the bind option is not 15 | # specified all the interfaces will listen for connections. 16 | # 17 | # bind 127.0.0.1 18 | 19 | # Close the connection after a client is idle for N seconds (0 to disable) 20 | timeout 300 21 | 22 | # Save the DB on disk: 23 | # 24 | # save 25 | # 26 | # Will save the DB if both the given number of seconds and the given 27 | # number of write operations against the DB occurred. 28 | # 29 | # In the example below the behaviour will be to save: 30 | # after 900 sec (15 min) if at least 1 key changed 31 | # after 300 sec (5 min) if at least 10 keys changed 32 | # after 60 sec if at least 10000 keys changed 33 | save 900 1 34 | save 300 10 35 | save 60 10000 36 | 37 | # The filename where to dump the DB 38 | dbfilename dump.rdb 39 | 40 | # For default save/load DB in/from the working directory 41 | # Note that you must specify a directory not a file name. 42 | dir ./test/ 43 | 44 | # Set server verbosity to 'debug' 45 | # it can be one of: 46 | # debug (a lot of information, useful for development/testing) 47 | # notice (moderately verbose, what you want in production probably) 48 | # warning (only very important / critical messages are logged) 49 | loglevel debug 50 | 51 | # Specify the log file name. Also 'stdout' can be used to force 52 | # the demon to log on the standard output. Note that if you use standard 53 | # output for logging but daemonize, logs will be sent to /dev/null 54 | logfile stdout 55 | 56 | # Set the number of databases. The default database is DB 0, you can select 57 | # a different one on a per-connection basis using SELECT where 58 | # dbid is a number between 0 and 'databases'-1 59 | databases 16 60 | 61 | ################################# REPLICATION ################################# 62 | 63 | # Master-Slave replication. Use slaveof to make a Redis instance a copy of 64 | # another Redis server. Note that the configuration is local to the slave 65 | # so for example it is possible to configure the slave to save the DB with a 66 | # different interval, or to listen to another port, and so on. 67 | 68 | # slaveof 69 | 70 | ################################## SECURITY ################################### 71 | 72 | # Require clients to issue AUTH before processing any other 73 | # commands. This might be useful in environments in which you do not trust 74 | # others with access to the host running redis-server. 75 | # 76 | # This should stay commented out for backward compatibility and because most 77 | # people do not need auth (e.g. they run their own servers). 78 | 79 | # requirepass foobared 80 | 81 | ################################### LIMITS #################################### 82 | 83 | # Set the max number of connected clients at the same time. By default there 84 | # is no limit, and it's up to the number of file descriptors the Redis process 85 | # is able to open. The special value '0' means no limts. 86 | # Once the limit is reached Redis will close all the new connections sending 87 | # an error 'max number of clients reached'. 88 | 89 | # maxclients 128 90 | 91 | # Don't use more memory than the specified amount of bytes. 92 | # When the memory limit is reached Redis will try to remove keys with an 93 | # EXPIRE set. It will try to start freeing keys that are going to expire 94 | # in little time and preserve keys with a longer time to live. 95 | # Redis will also try to remove objects from free lists if possible. 96 | # 97 | # If all this fails, Redis will start to reply with errors to commands 98 | # that will use more memory, like SET, LPUSH, and so on, and will continue 99 | # to reply to most read-only commands like GET. 100 | # 101 | # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a 102 | # 'state' server or cache, not as a real DB. When Redis is used as a real 103 | # database the memory usage will grow over the weeks, it will be obvious if 104 | # it is going to use too much memory in the long run, and you'll have the time 105 | # to upgrade. With maxmemory after the limit is reached you'll start to get 106 | # errors for write operations, and this may even lead to DB inconsistency. 107 | 108 | # maxmemory 109 | 110 | ############################### ADVANCED CONFIG ############################### 111 | 112 | # Glue small output buffers together in order to send small replies in a 113 | # single TCP packet. Uses a bit more CPU but most of the times it is a win 114 | # in terms of number of queries per second. Use 'yes' if unsure. 115 | glueoutputbuf yes 116 | -------------------------------------------------------------------------------- /lib/resque/server/public/style.css: -------------------------------------------------------------------------------- 1 | html { background:#efefef; font-family:Arial, Verdana, sans-serif; font-size:13px; } 2 | body { padding:0; margin:0; } 3 | 4 | .header { background:#000; padding:8px 5% 0 5%; border-bottom:1px solid #444;border-bottom:5px solid #ce1212;} 5 | .header h1 { color:#333; font-size:90%; font-weight:bold; margin-bottom:6px;} 6 | .header ul li { display:inline;} 7 | .header ul li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; padding:8px; -webkit-border-top-right-radius:6px; -webkit-border-top-left-radius:6px; -moz-border-radius-topleft:6px; -moz-border-radius-topright:6px; } 8 | .header ul li a:hover { background:#333;} 9 | .header ul li.current a { background:#ce1212; font-weight:bold; color:#fff;} 10 | 11 | .header .namespace { position: absolute; right: 75px; top: 10px; color: #7A7A7A; } 12 | 13 | .subnav { padding:2px 5% 7px 5%; background:#ce1212; font-size:90%;} 14 | .subnav li { display:inline;} 15 | .subnav li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; background:#dd5b5b; padding:5px; -webkit-border-radius:3px; -moz-border-radius:3px;} 16 | .subnav li.current a { background:#fff; font-weight:bold; color:#ce1212;} 17 | .subnav li a:active { background:#b00909;} 18 | 19 | #main { padding:10px 5%; background:#fff; overflow:hidden; } 20 | #main .logo { float:right; margin:10px;} 21 | #main span.hl { background:#efefef; padding:2px;} 22 | #main h1 { margin:10px 0; font-size:190%; font-weight:bold; color:#ce1212;} 23 | #main h2 { margin:10px 0; font-size:130%;} 24 | #main table { width:100%; margin:10px 0;} 25 | #main table tr td, #main table tr th { border:1px solid #ccc; padding:6px;} 26 | #main table tr th { background:#efefef; color:#888; font-size:80%; font-weight:bold;} 27 | #main table tr td.no-data { text-align:center; padding:40px 0; color:#999; font-style:italic; font-size:130%;} 28 | #main a { color:#111;} 29 | #main p { margin:5px 0;} 30 | #main p.intro { margin-bottom:15px; font-size:85%; color:#999; margin-top:0; line-height:1.3;} 31 | #main h1.wi { margin-bottom:5px;} 32 | #main p.sub { font-size:95%; color:#999;} 33 | 34 | #main table.queues { width:40%;} 35 | #main table.queues td.queue { font-weight:bold; width:50%;} 36 | #main table.queues tr.failed td { border-top:2px solid; font-size:90%; } 37 | #main table.queues tr.failure td { background:#ffecec; border-top:2px solid #d37474; font-size:90%; color:#d37474;} 38 | #main table.queues tr.failure td a{ color:#d37474;} 39 | 40 | #main table.jobs td.class { font-family:Monaco, "Courier New", monospace; font-size:90%; width:50%;} 41 | #main table.jobs td.args{ width:50%;} 42 | 43 | #main table.workers td.icon {width:1%; background:#efefef;text-align:center;} 44 | #main table.workers td.where { width:25%;} 45 | #main table.workers td.queues { width:35%;} 46 | #main .queue-tag { background:#b1d2e9; padding:2px; margin:0 3px; font-size:80%; text-decoration:none; text-transform:uppercase; font-weight:bold; color:#3274a2; -webkit-border-radius:4px; -moz-border-radius:4px;} 47 | #main table.workers td.queues.queue { width:10%;} 48 | #main table.workers td.process { width:35%;} 49 | #main table.workers td.process span.waiting { color:#999; font-size:90%;} 50 | #main table.workers td.process small { font-size:80%; margin-left:5px;} 51 | #main table.workers td.process code { font-family:Monaco, "Courier New", monospace; font-size:90%;} 52 | #main table.workers td.process small a { color:#999;} 53 | #main.polling table.workers tr.working td { background:#f4ffe4; color:#7ac312;} 54 | #main.polling table.workers tr.working td.where a { color:#7ac312;} 55 | #main.polling table.workers tr.working td.process code { font-weight:bold;} 56 | 57 | 58 | #main table.stats th { font-size:100%; width:40%; color:#000;} 59 | #main hr { border:0; border-top:5px solid #efefef; margin:15px 0;} 60 | 61 | #footer { padding:10px 5%; background:#efefef; color:#999; font-size:85%; line-height:1.5; border-top:5px solid #ccc; padding-top:10px;} 62 | #footer p a { color:#999;} 63 | 64 | #main p.poll { background:url(poll.png) no-repeat 0 2px; padding:3px 0; padding-left:23px; float:right; font-size:85%; } 65 | 66 | #main ul.failed {} 67 | #main ul.failed li {background:-webkit-gradient(linear, left top, left bottom, from(#efefef), to(#fff)) #efefef; margin-top:10px; padding:10px; overflow:hidden; -webkit-border-radius:5px; border:1px solid #ccc; } 68 | #main ul.failed li dl dt {font-size:80%; color:#999; width:60px; float:left; padding-top:1px; text-align:right;} 69 | #main ul.failed li dl dd {margin-bottom:10px; margin-left:70px;} 70 | #main ul.failed li dl dd .retried { float:right; text-align: right; } 71 | #main ul.failed li dl dd .retried .remove { display:none; margin-top: 8px; } 72 | #main ul.failed li.hover dl dd .retried .remove { display:block; } 73 | #main ul.failed li dl dd .controls { display:none; float:right; } 74 | #main ul.failed li.hover dl dd .controls { display:block; } 75 | #main ul.failed li dl dd code, #main ul.failed li dl dd pre { font-family:Monaco, "Courier New", monospace; font-size:90%; white-space: pre-wrap;} 76 | #main ul.failed li dl dd.error a {font-family:Monaco, "Courier New", monospace; font-size:90%; } 77 | #main ul.failed li dl dd.error pre { margin-top:3px; line-height:1.3;} 78 | 79 | #main p.pagination { background:#efefef; padding:10px; overflow:hidden;} 80 | #main p.pagination a.less { float:left;} 81 | #main p.pagination a.more { float:right;} 82 | 83 | #main form {float:right; margin-top:-10px;} 84 | 85 | #main .time a.toggle_format {text-decoration:none;} -------------------------------------------------------------------------------- /lib/resque/server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'erb' 3 | require 'resque' 4 | require 'resque/version' 5 | require 'time' 6 | 7 | module Resque 8 | class Server < Sinatra::Base 9 | dir = File.dirname(File.expand_path(__FILE__)) 10 | 11 | set :views, "#{dir}/server/views" 12 | set :public, "#{dir}/server/public" 13 | set :static, true 14 | 15 | helpers do 16 | include Rack::Utils 17 | alias_method :h, :escape_html 18 | 19 | def current_section 20 | url_path request.path_info.sub('/','').split('/')[0].downcase 21 | end 22 | 23 | def current_page 24 | url_path request.path_info.sub('/','') 25 | end 26 | 27 | def url_path(*path_parts) 28 | [ path_prefix, path_parts ].join("/").squeeze('/') 29 | end 30 | alias_method :u, :url_path 31 | 32 | def path_prefix 33 | request.env['SCRIPT_NAME'] 34 | end 35 | 36 | def class_if_current(path = '') 37 | 'class="current"' if current_page[0, path.size] == path 38 | end 39 | 40 | def tab(name) 41 | dname = name.to_s.downcase 42 | path = url_path(dname) 43 | "
  • #{name}
  • " 44 | end 45 | 46 | def tabs 47 | Resque::Server.tabs 48 | end 49 | 50 | def redis_get_size(key) 51 | case Resque.redis.type(key) 52 | when 'none' 53 | [] 54 | when 'list' 55 | Resque.redis.llen(key) 56 | when 'set' 57 | Resque.redis.scard(key) 58 | when 'string' 59 | Resque.redis.get(key).length 60 | when 'zset' 61 | Resque.redis.zcard(key) 62 | end 63 | end 64 | 65 | def redis_get_value_as_array(key, start=0) 66 | case Resque.redis.type(key) 67 | when 'none' 68 | [] 69 | when 'list' 70 | Resque.redis.lrange(key, start, start + 20) 71 | when 'set' 72 | Resque.redis.smembers(key)[start..(start + 20)] 73 | when 'string' 74 | [Resque.redis.get(key)] 75 | when 'zset' 76 | Resque.redis.zrange(key, start, start + 20) 77 | end 78 | end 79 | 80 | def show_args(args) 81 | Array(args).map { |a| a.inspect }.join("\n") 82 | end 83 | 84 | def worker_hosts 85 | @worker_hosts ||= worker_hosts! 86 | end 87 | 88 | def worker_hosts! 89 | hosts = Hash.new { [] } 90 | 91 | Resque.workers.each do |worker| 92 | host, _ = worker.to_s.split(':') 93 | hosts[host] += [worker.to_s] 94 | end 95 | 96 | hosts 97 | end 98 | 99 | def partial? 100 | @partial 101 | end 102 | 103 | def partial(template, local_vars = {}) 104 | @partial = true 105 | erb(template.to_sym, {:layout => false}, local_vars) 106 | ensure 107 | @partial = false 108 | end 109 | 110 | def poll 111 | if @polling 112 | text = "Last Updated: #{Time.now.strftime("%H:%M:%S")}" 113 | else 114 | text = "Live Poll" 115 | end 116 | "

    #{text}

    " 117 | end 118 | 119 | end 120 | 121 | def show(page, layout = true) 122 | begin 123 | erb page.to_sym, {:layout => layout}, :resque => Resque 124 | rescue Errno::ECONNREFUSED 125 | erb :error, {:layout => false}, :error => "Can't connect to Redis! (#{Resque.redis_id})" 126 | end 127 | end 128 | 129 | def show_for_polling(page) 130 | content_type "text/html" 131 | @polling = true 132 | show(page.to_sym, false).gsub(/\s{1,}/, ' ') 133 | end 134 | 135 | # to make things easier on ourselves 136 | get "/?" do 137 | redirect url_path(:overview) 138 | end 139 | 140 | %w( overview workers ).each do |page| 141 | get "/#{page}.poll" do 142 | show_for_polling(page) 143 | end 144 | 145 | get "/#{page}/:id.poll" do 146 | show_for_polling(page) 147 | end 148 | end 149 | 150 | %w( overview queues working workers key ).each do |page| 151 | get "/#{page}" do 152 | show page 153 | end 154 | 155 | get "/#{page}/:id" do 156 | show page 157 | end 158 | end 159 | 160 | post "/queues/:id/remove" do 161 | Resque.remove_queue(params[:id]) 162 | redirect u('queues') 163 | end 164 | 165 | get "/failed" do 166 | if Resque::Failure.url 167 | redirect Resque::Failure.url 168 | else 169 | show :failed 170 | end 171 | end 172 | 173 | post "/failed/clear" do 174 | Resque::Failure.clear 175 | redirect u('failed') 176 | end 177 | 178 | get "/failed/requeue/:index" do 179 | Resque::Failure.requeue(params[:index]) 180 | if request.xhr? 181 | return Resque::Failure.all(params[:index])['retried_at'] 182 | else 183 | redirect u('failed') 184 | end 185 | end 186 | 187 | get "/failed/remove/:index" do 188 | Resque::Failure.remove(params[:index]) 189 | redirect u('failed') 190 | end 191 | 192 | get "/stats" do 193 | redirect url_path("/stats/resque") 194 | end 195 | 196 | get "/stats/:id" do 197 | show :stats 198 | end 199 | 200 | get "/stats/keys/:key" do 201 | show :stats 202 | end 203 | 204 | get "/stats.txt" do 205 | info = Resque.info 206 | 207 | stats = [] 208 | stats << "resque.pending=#{info[:pending]}" 209 | stats << "resque.processed+=#{info[:processed]}" 210 | stats << "resque.failed+=#{info[:failed]}" 211 | stats << "resque.workers=#{info[:workers]}" 212 | stats << "resque.working=#{info[:working]}" 213 | 214 | Resque.queues.each do |queue| 215 | stats << "queues.#{queue}=#{Resque.size(queue)}" 216 | end 217 | 218 | content_type 'text/html' 219 | stats.join "\n" 220 | end 221 | 222 | def resque 223 | Resque 224 | end 225 | 226 | def self.tabs 227 | @tabs ||= ["Overview", "Working", "Failed", "Queues", "Workers", "Stats"] 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /test/job_plugins_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | context "Multiple plugins with multiple hooks" do 4 | include PerformJob 5 | 6 | module Plugin1 7 | def before_perform_record_history1(history) 8 | history << :before1 9 | end 10 | def after_perform_record_history1(history) 11 | history << :after1 12 | end 13 | end 14 | 15 | module Plugin2 16 | def before_perform_record_history2(history) 17 | history << :before2 18 | end 19 | def after_perform_record_history2(history) 20 | history << :after2 21 | end 22 | end 23 | 24 | class ::ManyBeforesJob 25 | extend Plugin1 26 | extend Plugin2 27 | def self.perform(history) 28 | history << :perform 29 | end 30 | end 31 | 32 | test "hooks of each type are executed in alphabetical order" do 33 | result = perform_job(ManyBeforesJob, history=[]) 34 | assert_equal true, result, "perform returned true" 35 | assert_equal [:before1, :before2, :perform, :after1, :after2], history 36 | end 37 | end 38 | 39 | context "Resque::Plugin ordering before_perform" do 40 | include PerformJob 41 | 42 | module BeforePerformPlugin 43 | def before_perform1(history) 44 | history << :before_perform1 45 | end 46 | end 47 | 48 | class ::JobPluginsTestBeforePerformJob 49 | extend BeforePerformPlugin 50 | def self.perform(history) 51 | history << :perform 52 | end 53 | def self.before_perform(history) 54 | history << :before_perform 55 | end 56 | end 57 | 58 | test "before_perform hooks are executed in order" do 59 | result = perform_job(JobPluginsTestBeforePerformJob, history=[]) 60 | assert_equal true, result, "perform returned true" 61 | assert_equal [:before_perform, :before_perform1, :perform], history 62 | end 63 | end 64 | 65 | context "Resque::Plugin ordering after_perform" do 66 | include PerformJob 67 | 68 | module AfterPerformPlugin 69 | def after_perform_record_history(history) 70 | history << :after_perform1 71 | end 72 | end 73 | 74 | class ::JobPluginsTestAfterPerformJob 75 | extend AfterPerformPlugin 76 | def self.perform(history) 77 | history << :perform 78 | end 79 | def self.after_perform(history) 80 | history << :after_perform 81 | end 82 | end 83 | 84 | test "after_perform hooks are executed in order" do 85 | result = perform_job(JobPluginsTestAfterPerformJob, history=[]) 86 | assert_equal true, result, "perform returned true" 87 | assert_equal [:perform, :after_perform, :after_perform1], history 88 | end 89 | end 90 | 91 | context "Resque::Plugin ordering around_perform" do 92 | include PerformJob 93 | 94 | module AroundPerformPlugin1 95 | def around_perform1(history) 96 | history << :around_perform_plugin1 97 | yield 98 | end 99 | end 100 | 101 | class ::AroundPerformJustPerformsJob 102 | extend AroundPerformPlugin1 103 | def self.perform(history) 104 | history << :perform 105 | end 106 | end 107 | 108 | test "around_perform hooks are executed before the job" do 109 | result = perform_job(AroundPerformJustPerformsJob, history=[]) 110 | assert_equal true, result, "perform returned true" 111 | assert_equal [:around_perform_plugin1, :perform], history 112 | end 113 | 114 | class ::JobPluginsTestAroundPerformJob 115 | extend AroundPerformPlugin1 116 | def self.perform(history) 117 | history << :perform 118 | end 119 | def self.around_perform(history) 120 | history << :around_perform 121 | yield 122 | end 123 | end 124 | 125 | test "around_perform hooks are executed in order" do 126 | result = perform_job(JobPluginsTestAroundPerformJob, history=[]) 127 | assert_equal true, result, "perform returned true" 128 | assert_equal [:around_perform, :around_perform_plugin1, :perform], history 129 | end 130 | 131 | module AroundPerformPlugin2 132 | def around_perform2(history) 133 | history << :around_perform_plugin2 134 | yield 135 | end 136 | end 137 | 138 | class ::AroundPerformJob2 139 | extend AroundPerformPlugin1 140 | extend AroundPerformPlugin2 141 | def self.perform(history) 142 | history << :perform 143 | end 144 | def self.around_perform(history) 145 | history << :around_perform 146 | yield 147 | end 148 | end 149 | 150 | test "many around_perform are executed in order" do 151 | result = perform_job(AroundPerformJob2, history=[]) 152 | assert_equal true, result, "perform returned true" 153 | assert_equal [:around_perform, :around_perform_plugin1, :around_perform_plugin2, :perform], history 154 | end 155 | 156 | module AroundPerformDoesNotYield 157 | def around_perform0(history) 158 | history << :around_perform0 159 | end 160 | end 161 | 162 | class ::AroundPerformJob3 163 | extend AroundPerformPlugin1 164 | extend AroundPerformPlugin2 165 | extend AroundPerformDoesNotYield 166 | def self.perform(history) 167 | history << :perform 168 | end 169 | def self.around_perform(history) 170 | history << :around_perform 171 | yield 172 | end 173 | end 174 | 175 | test "the job is aborted if an around_perform hook does not yield" do 176 | result = perform_job(AroundPerformJob3, history=[]) 177 | assert_equal false, result, "perform returned false" 178 | assert_equal [:around_perform, :around_perform0], history 179 | end 180 | 181 | module AroundPerformGetsJobResult 182 | @@result = nil 183 | def last_job_result 184 | @@result 185 | end 186 | 187 | def around_perform_gets_job_result(*args) 188 | @@result = yield 189 | end 190 | end 191 | 192 | class ::AroundPerformJobWithReturnValue < GoodJob 193 | extend AroundPerformGetsJobResult 194 | end 195 | 196 | test "the job is aborted if an around_perform hook does not yield" do 197 | result = perform_job(AroundPerformJobWithReturnValue, 'Bob') 198 | assert_equal true, result, "perform returned true" 199 | assert_equal 'Good job, Bob', AroundPerformJobWithReturnValue.last_job_result 200 | end 201 | end 202 | 203 | context "Resque::Plugin ordering on_failure" do 204 | include PerformJob 205 | 206 | module OnFailurePlugin 207 | def on_failure1(exception, history) 208 | history << "#{exception.message} plugin" 209 | end 210 | end 211 | 212 | class ::FailureJob 213 | extend OnFailurePlugin 214 | def self.perform(history) 215 | history << :perform 216 | raise StandardError, "oh no" 217 | end 218 | def self.on_failure(exception, history) 219 | history << exception.message 220 | end 221 | end 222 | 223 | test "on_failure hooks are executed in order" do 224 | history = [] 225 | assert_raises StandardError do 226 | perform_job(FailureJob, history) 227 | end 228 | assert_equal [:perform, "oh no", "oh no plugin"], history 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /lib/resque/job.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | # A Resque::Job represents a unit of work. Each job lives on a 3 | # single queue and has an associated payload object. The payload 4 | # is a hash with two attributes: `class` and `args`. The `class` is 5 | # the name of the Ruby class which should be used to run the 6 | # job. The `args` are an array of arguments which should be passed 7 | # to the Ruby class's `perform` class-level method. 8 | # 9 | # You can manually run a job using this code: 10 | # 11 | # job = Resque::Job.reserve(:high) 12 | # klass = Resque::Job.constantize(job.payload['class']) 13 | # klass.perform(*job.payload['args']) 14 | class Job 15 | include Helpers 16 | extend Helpers 17 | 18 | # Raise Resque::Job::DontPerform from a before_perform hook to 19 | # abort the job. 20 | DontPerform = Class.new(StandardError) 21 | 22 | # The worker object which is currently processing this job. 23 | attr_accessor :worker 24 | 25 | # The name of the queue from which this job was pulled (or is to be 26 | # placed) 27 | attr_reader :queue 28 | 29 | # This job's associated payload object. 30 | attr_reader :payload 31 | 32 | def initialize(queue, payload) 33 | @queue = queue 34 | @payload = payload 35 | end 36 | 37 | # Creates a job by placing it on a queue. Expects a string queue 38 | # name, a string class name, and an optional array of arguments to 39 | # pass to the class' `perform` method. 40 | # 41 | # Raises an exception if no queue or class is given. 42 | def self.create(queue, klass, *args) 43 | Resque.validate(klass, queue) 44 | 45 | if Resque.inline? 46 | constantize(klass).perform(*decode(encode(args))) 47 | else 48 | Resque.push(queue, :class => klass.to_s, :args => args) 49 | end 50 | end 51 | 52 | # Removes a job from a queue. Expects a string queue name, a 53 | # string class name, and, optionally, args. 54 | # 55 | # Returns the number of jobs destroyed. 56 | # 57 | # If no args are provided, it will remove all jobs of the class 58 | # provided. 59 | # 60 | # That is, for these two jobs: 61 | # 62 | # { 'class' => 'UpdateGraph', 'args' => ['defunkt'] } 63 | # { 'class' => 'UpdateGraph', 'args' => ['mojombo'] } 64 | # 65 | # The following call will remove both: 66 | # 67 | # Resque::Job.destroy(queue, 'UpdateGraph') 68 | # 69 | # Whereas specifying args will only remove the 2nd job: 70 | # 71 | # Resque::Job.destroy(queue, 'UpdateGraph', 'mojombo') 72 | # 73 | # This method can be potentially very slow and memory intensive, 74 | # depending on the size of your queue, as it loads all jobs into 75 | # a Ruby array before processing. 76 | def self.destroy(queue, klass, *args) 77 | klass = klass.to_s 78 | queue = "queue:#{queue}" 79 | destroyed = 0 80 | 81 | if args.empty? 82 | redis.lrange(queue, 0, -1).each do |string| 83 | if decode(string)['class'] == klass 84 | destroyed += redis.lrem(queue, 0, string).to_i 85 | end 86 | end 87 | else 88 | destroyed += redis.lrem(queue, 0, encode(:class => klass, :args => args)) 89 | end 90 | 91 | destroyed 92 | end 93 | 94 | # Given a string queue name, returns an instance of Resque::Job 95 | # if any jobs are available. If not, returns nil. 96 | def self.reserve(queue) 97 | return unless payload = Resque.pop(queue) 98 | new(queue, payload) 99 | end 100 | 101 | # Attempts to perform the work represented by this job instance. 102 | # Calls #perform on the class given in the payload with the 103 | # arguments given in the payload. 104 | def perform 105 | job = payload_class 106 | job_args = args || [] 107 | job_was_performed = false 108 | 109 | before_hooks = Plugin.before_hooks(job) 110 | around_hooks = Plugin.around_hooks(job) 111 | after_hooks = Plugin.after_hooks(job) 112 | failure_hooks = Plugin.failure_hooks(job) 113 | 114 | begin 115 | # Execute before_perform hook. Abort the job gracefully if 116 | # Resque::DontPerform is raised. 117 | begin 118 | before_hooks.each do |hook| 119 | job.send(hook, *job_args) 120 | end 121 | rescue DontPerform 122 | return false 123 | end 124 | 125 | # Execute the job. Do it in an around_perform hook if available. 126 | if around_hooks.empty? 127 | job.perform(*job_args) 128 | job_was_performed = true 129 | else 130 | # We want to nest all around_perform plugins, with the last one 131 | # finally calling perform 132 | stack = around_hooks.reverse.inject(nil) do |last_hook, hook| 133 | if last_hook 134 | lambda do 135 | job.send(hook, *job_args) { last_hook.call } 136 | end 137 | else 138 | lambda do 139 | job.send(hook, *job_args) do 140 | result = job.perform(*job_args) 141 | job_was_performed = true 142 | result 143 | end 144 | end 145 | end 146 | end 147 | stack.call 148 | end 149 | 150 | # Execute after_perform hook 151 | after_hooks.each do |hook| 152 | job.send(hook, *job_args) 153 | end 154 | 155 | # Return true if the job was performed 156 | return job_was_performed 157 | 158 | # If an exception occurs during the job execution, look for an 159 | # on_failure hook then re-raise. 160 | rescue Object => e 161 | failure_hooks.each { |hook| job.send(hook, e, *job_args) } 162 | raise e 163 | end 164 | end 165 | 166 | # Returns the actual class constant represented in this job's payload. 167 | def payload_class 168 | @payload_class ||= constantize(@payload['class']) 169 | end 170 | 171 | # Returns an array of args represented in this job's payload. 172 | def args 173 | @payload['args'] 174 | end 175 | 176 | # Given an exception object, hands off the needed parameters to 177 | # the Failure module. 178 | def fail(exception) 179 | Failure.create \ 180 | :payload => payload, 181 | :exception => exception, 182 | :worker => worker, 183 | :queue => queue 184 | end 185 | 186 | # Creates an identical job, essentially placing this job back on 187 | # the queue. 188 | def recreate 189 | self.class.create(queue, payload_class, *args) 190 | end 191 | 192 | # String representation 193 | def inspect 194 | obj = @payload 195 | "(Job{%s} | %s | %s)" % [ @queue, obj['class'], obj['args'].inspect ] 196 | end 197 | 198 | # Equality 199 | def ==(other) 200 | queue == other.queue && 201 | payload_class == other.payload_class && 202 | args == other.args 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /test/resque_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | context "Resque" do 4 | setup do 5 | Resque.redis.flushall 6 | 7 | Resque.push(:people, { 'name' => 'chris' }) 8 | Resque.push(:people, { 'name' => 'bob' }) 9 | Resque.push(:people, { 'name' => 'mark' }) 10 | end 11 | 12 | test "can set a namespace through a url-like string" do 13 | assert Resque.redis 14 | assert_equal :resque, Resque.redis.namespace 15 | Resque.redis = 'localhost:9736/namespace' 16 | assert_equal 'namespace', Resque.redis.namespace 17 | end 18 | 19 | test "redis= works correctly with a Redis::Namespace param" do 20 | new_redis = Redis.new(:host => "localhost", :port => 9736) 21 | new_namespace = Redis::Namespace.new("namespace", :redis => new_redis) 22 | Resque.redis = new_namespace 23 | assert_equal new_namespace, Resque.redis 24 | 25 | Resque.redis = 'localhost:9736/namespace' 26 | end 27 | 28 | test "can put jobs on a queue" do 29 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 30 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 31 | end 32 | 33 | test "can grab jobs off a queue" do 34 | Resque::Job.create(:jobs, 'some-job', 20, '/tmp') 35 | 36 | job = Resque.reserve(:jobs) 37 | 38 | assert_kind_of Resque::Job, job 39 | assert_equal SomeJob, job.payload_class 40 | assert_equal 20, job.args[0] 41 | assert_equal '/tmp', job.args[1] 42 | end 43 | 44 | test "can re-queue jobs" do 45 | Resque::Job.create(:jobs, 'some-job', 20, '/tmp') 46 | 47 | job = Resque.reserve(:jobs) 48 | job.recreate 49 | 50 | assert_equal job, Resque.reserve(:jobs) 51 | end 52 | 53 | test "can put jobs on a queue by way of an ivar" do 54 | assert_equal 0, Resque.size(:ivar) 55 | assert Resque.enqueue(SomeIvarJob, 20, '/tmp') 56 | assert Resque.enqueue(SomeIvarJob, 20, '/tmp') 57 | 58 | job = Resque.reserve(:ivar) 59 | 60 | assert_kind_of Resque::Job, job 61 | assert_equal SomeIvarJob, job.payload_class 62 | assert_equal 20, job.args[0] 63 | assert_equal '/tmp', job.args[1] 64 | 65 | assert Resque.reserve(:ivar) 66 | assert_equal nil, Resque.reserve(:ivar) 67 | end 68 | 69 | test "can remove jobs from a queue by way of an ivar" do 70 | assert_equal 0, Resque.size(:ivar) 71 | assert Resque.enqueue(SomeIvarJob, 20, '/tmp') 72 | assert Resque.enqueue(SomeIvarJob, 30, '/tmp') 73 | assert Resque.enqueue(SomeIvarJob, 20, '/tmp') 74 | assert Resque::Job.create(:ivar, 'blah-job', 20, '/tmp') 75 | assert Resque.enqueue(SomeIvarJob, 20, '/tmp') 76 | assert_equal 5, Resque.size(:ivar) 77 | 78 | assert Resque.dequeue(SomeIvarJob, 30, '/tmp') 79 | assert_equal 4, Resque.size(:ivar) 80 | assert Resque.dequeue(SomeIvarJob) 81 | assert_equal 1, Resque.size(:ivar) 82 | end 83 | 84 | test "jobs have a nice #inspect" do 85 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 86 | job = Resque.reserve(:jobs) 87 | assert_equal '(Job{jobs} | SomeJob | [20, "/tmp"])', job.inspect 88 | end 89 | 90 | test "jobs can be destroyed" do 91 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 92 | assert Resque::Job.create(:jobs, 'BadJob', 20, '/tmp') 93 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 94 | assert Resque::Job.create(:jobs, 'BadJob', 30, '/tmp') 95 | assert Resque::Job.create(:jobs, 'BadJob', 20, '/tmp') 96 | 97 | assert_equal 5, Resque.size(:jobs) 98 | assert_equal 2, Resque::Job.destroy(:jobs, 'SomeJob') 99 | assert_equal 3, Resque.size(:jobs) 100 | assert_equal 1, Resque::Job.destroy(:jobs, 'BadJob', 30, '/tmp') 101 | assert_equal 2, Resque.size(:jobs) 102 | end 103 | 104 | test "jobs can test for equality" do 105 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 106 | assert Resque::Job.create(:jobs, 'some-job', 20, '/tmp') 107 | assert_equal Resque.reserve(:jobs), Resque.reserve(:jobs) 108 | 109 | assert Resque::Job.create(:jobs, 'SomeMethodJob', 20, '/tmp') 110 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 111 | assert_not_equal Resque.reserve(:jobs), Resque.reserve(:jobs) 112 | 113 | assert Resque::Job.create(:jobs, 'SomeJob', 20, '/tmp') 114 | assert Resque::Job.create(:jobs, 'SomeJob', 30, '/tmp') 115 | assert_not_equal Resque.reserve(:jobs), Resque.reserve(:jobs) 116 | end 117 | 118 | test "can put jobs on a queue by way of a method" do 119 | assert_equal 0, Resque.size(:method) 120 | assert Resque.enqueue(SomeMethodJob, 20, '/tmp') 121 | assert Resque.enqueue(SomeMethodJob, 20, '/tmp') 122 | 123 | job = Resque.reserve(:method) 124 | 125 | assert_kind_of Resque::Job, job 126 | assert_equal SomeMethodJob, job.payload_class 127 | assert_equal 20, job.args[0] 128 | assert_equal '/tmp', job.args[1] 129 | 130 | assert Resque.reserve(:method) 131 | assert_equal nil, Resque.reserve(:method) 132 | end 133 | 134 | test "needs to infer a queue with enqueue" do 135 | assert_raises Resque::NoQueueError do 136 | Resque.enqueue(SomeJob, 20, '/tmp') 137 | end 138 | end 139 | 140 | test "validates job for queue presence" do 141 | assert_raises Resque::NoQueueError do 142 | Resque.validate(SomeJob) 143 | end 144 | end 145 | 146 | test "can put items on a queue" do 147 | assert Resque.push(:people, { 'name' => 'jon' }) 148 | end 149 | 150 | test "can pull items off a queue" do 151 | assert_equal({ 'name' => 'chris' }, Resque.pop(:people)) 152 | assert_equal({ 'name' => 'bob' }, Resque.pop(:people)) 153 | assert_equal({ 'name' => 'mark' }, Resque.pop(:people)) 154 | assert_equal nil, Resque.pop(:people) 155 | end 156 | 157 | test "knows how big a queue is" do 158 | assert_equal 3, Resque.size(:people) 159 | 160 | assert_equal({ 'name' => 'chris' }, Resque.pop(:people)) 161 | assert_equal 2, Resque.size(:people) 162 | 163 | assert_equal({ 'name' => 'bob' }, Resque.pop(:people)) 164 | assert_equal({ 'name' => 'mark' }, Resque.pop(:people)) 165 | assert_equal 0, Resque.size(:people) 166 | end 167 | 168 | test "can peek at a queue" do 169 | assert_equal({ 'name' => 'chris' }, Resque.peek(:people)) 170 | assert_equal 3, Resque.size(:people) 171 | end 172 | 173 | test "can peek multiple items on a queue" do 174 | assert_equal({ 'name' => 'bob' }, Resque.peek(:people, 1, 1)) 175 | 176 | assert_equal([{ 'name' => 'bob' }, { 'name' => 'mark' }], Resque.peek(:people, 1, 2)) 177 | assert_equal([{ 'name' => 'chris' }, { 'name' => 'bob' }], Resque.peek(:people, 0, 2)) 178 | assert_equal([{ 'name' => 'chris' }, { 'name' => 'bob' }, { 'name' => 'mark' }], Resque.peek(:people, 0, 3)) 179 | assert_equal({ 'name' => 'mark' }, Resque.peek(:people, 2, 1)) 180 | assert_equal nil, Resque.peek(:people, 3) 181 | assert_equal [], Resque.peek(:people, 3, 2) 182 | end 183 | 184 | test "knows what queues it is managing" do 185 | assert_equal %w( people ), Resque.queues 186 | Resque.push(:cars, { 'make' => 'bmw' }) 187 | assert_equal %w( cars people ), Resque.queues 188 | end 189 | 190 | test "queues are always a list" do 191 | Resque.redis.flushall 192 | assert_equal [], Resque.queues 193 | end 194 | 195 | test "can delete a queue" do 196 | Resque.push(:cars, { 'make' => 'bmw' }) 197 | assert_equal %w( cars people ), Resque.queues 198 | Resque.remove_queue(:people) 199 | assert_equal %w( cars ), Resque.queues 200 | assert_equal nil, Resque.pop(:people) 201 | end 202 | 203 | test "keeps track of resque keys" do 204 | assert_equal ["queue:people", "queues"], Resque.keys 205 | end 206 | 207 | test "badly wants a class name, too" do 208 | assert_raises Resque::NoClassError do 209 | Resque::Job.create(:jobs, nil) 210 | end 211 | end 212 | 213 | test "keeps stats" do 214 | Resque::Job.create(:jobs, SomeJob, 20, '/tmp') 215 | Resque::Job.create(:jobs, BadJob) 216 | Resque::Job.create(:jobs, GoodJob) 217 | 218 | Resque::Job.create(:others, GoodJob) 219 | Resque::Job.create(:others, GoodJob) 220 | 221 | stats = Resque.info 222 | assert_equal 8, stats[:pending] 223 | 224 | @worker = Resque::Worker.new(:jobs) 225 | @worker.register_worker 226 | 2.times { @worker.process } 227 | 228 | job = @worker.reserve 229 | @worker.working_on job 230 | 231 | stats = Resque.info 232 | assert_equal 1, stats[:working] 233 | assert_equal 1, stats[:workers] 234 | 235 | @worker.done_working 236 | 237 | stats = Resque.info 238 | assert_equal 3, stats[:queues] 239 | assert_equal 3, stats[:processed] 240 | assert_equal 1, stats[:failed] 241 | assert_equal [Resque.redis.respond_to?(:server) ? 'localhost:9736' : 'redis://localhost:9736/0'], stats[:servers] 242 | end 243 | 244 | test "decode bad json" do 245 | assert_raises Resque::Helpers::DecodeException do 246 | Resque.decode("{\"error\":\"Module not found \\u002\"}") 247 | end 248 | end 249 | 250 | test "inlining jobs" do 251 | begin 252 | Resque.inline = true 253 | Resque.enqueue(SomeIvarJob, 20, '/tmp') 254 | assert_equal 0, Resque.size(:ivar) 255 | ensure 256 | Resque.inline = false 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 1.17.1 (2011-05-27) 2 | 3 | * Reverted `exit` change. Back to `exit!`. 4 | 5 | ## 1.17.0 (2011-05-26) 6 | 7 | * Workers exit with `exit` instead of `exit!`. This means you 8 | can now use `at_exit` hooks inside workers. 9 | * More monit typo fixes. 10 | * Fixed bug in Hoptoad backend. 11 | * Web UI: Wrap preformatted arguments. 12 | 13 | ## 1.16.1 (2011-05-17) 14 | 15 | * Bugfix: Resque::Failure::Hoptoad.configure works again 16 | * Bugfix: Loading rake tasks 17 | 18 | ## 1.16.0 (2011-05-16) 19 | 20 | * Optional Hoptoad backend extracted into hoptoad_notifier. Install the gem to use it. 21 | * Added `Worker#paused?` method 22 | * Bugfix: Properly reseed random number generator after forking. 23 | * Bugfix: Resque.redis=() 24 | * Bugfix: Monit example stdout/stderr redirection 25 | * Bugfix: Removing single failure now works with multiple failure backends 26 | * Web: 'Remove Queue' now requires confirmation 27 | * Web: Favicon! 28 | * Web Bugfix: Dates display in Safari 29 | * Web Bugfix: Dates display timezone 30 | * Web Bugfix: Race condition querying working workers 31 | * Web Bugfix: Fix polling /workers/all in resque-web 32 | 33 | ## 1.15.0 (2011-03-18) 34 | 35 | * Fallback to Redis.connect. Makes ENV variables and whatnot work. 36 | * Fixed Sinatra 1.2 compatibility 37 | 38 | ## 1.14.0 (2011-03-17) 39 | 40 | * Sleep interval can now be a float 41 | * Added Resque.inline to allow in-process performing of jobs (for testing) 42 | * Fixed tests for Ruby 1.9.2 43 | * Added Resque.validate(klass) to validate a Job 44 | * Decode errors are no longer ignored to help debugging 45 | * Web: Sinatra 1.2 compatibility 46 | * Fixed after_enqueue hook to actually run in `Resque.enqueue` 47 | * Fixed very_verbose timestamps to use 24 hour time (AM/PM wasn't included) 48 | * Fixed monit example 49 | * Fixed Worker#pid 50 | 51 | ## 1.13.0 (2011-02-07) 52 | 53 | * Depend on redis-namespace >= 0.10 54 | * README tweaks 55 | * Use thread_safe option when setting redis url 56 | * Bugfix: worker pruning 57 | 58 | ## 1.12.0 (2011-02-03) 59 | 60 | * Added pidfile writing from `rake resque:work` 61 | * Added Worker#pid method 62 | * Added configurable location for `rake install` 63 | * Bugfix: Errors in failure backend are rescue'd 64 | * Bugfix: Non-working workers no longer counted in "working" count 65 | * Bugfix: Don't think resque-web is a worker 66 | 67 | ## 1.11.0 (2010-08-23) 68 | 69 | * Web UI: Group /workers page by hostnames 70 | 71 | ## 1.10.0 (2010-08-23) 72 | 73 | * Support redis:// string format in `Resque.redis=` 74 | * Using new cross-platform JSON gem. 75 | * Added `after_enqueue` plugin hook. 76 | * Added `shutdown?` method which can be overridden. 77 | * Added support for the "leftright" gem when running tests. 78 | * Grammarfix: In the README 79 | 80 | ## 1.9.10 (2010-08-06) 81 | 82 | * Bugfix: before_fork should get passed the job 83 | 84 | ## 1.9.9 (2010-07-26) 85 | 86 | * Depend on redis-namespace 0.8.0 87 | * Depend on json_pure instead of json (for JRuby compat) 88 | * Bugfix: rails_env display in stats view 89 | 90 | ## 1.9.8 (2010-07-20) 91 | 92 | * Bugfix: Worker.all should never return nil 93 | * monit example: Fixed Syntax Error and adding environment to the rake task 94 | * redis rake task: Fixed typo in copy command 95 | 96 | ## 1.9.7 (2010-07-09) 97 | 98 | * Improved memory usage in Job.destroy 99 | * redis-namespace 0.7.0 now required 100 | * Bugfix: Reverted $0 changes 101 | * Web Bugfix: Payload-less failures in the web ui work 102 | 103 | ## 1.9.6 (2010-06-22) 104 | 105 | * Bugfix: Rakefile logging works the same as all the other logging 106 | 107 | ## 1.9.5 (2010-06-16) 108 | 109 | * Web Bugfix: Display the configured namespace on the stats page 110 | * Revert Bugfix: Make ps -o more cross platform friendly 111 | 112 | ## 1.9.4 (2010-06-14) 113 | 114 | * Bugfix: Multiple failure backend gets exception information when created 115 | 116 | ## 1.9.3 (2010-06-14) 117 | 118 | * Bugfix: Resque#queues always returns an array 119 | 120 | ## 1.9.2 (2010-06-13) 121 | 122 | * Bugfix: Worker.all returning nil fix 123 | * Bugfix: Make ps -o more cross platform friendly 124 | 125 | ## 1.9.1 (2010-06-04) 126 | 127 | * Less strict JSON dependency 128 | * Included HISTORY.md in gem 129 | 130 | ## 1.9.0 (2010-06-04) 131 | 132 | * Redis 2 support 133 | * Depend on redis-namespace 0.5.0 134 | * Added Resque::VERSION constant (alias of Resque::Version) 135 | * Bugfix: Specify JSON dependency 136 | * Bugfix: Hoptoad plugin now works on 1.9 137 | 138 | ## 1.8.5 (2010-05-18) 139 | 140 | * Bugfix: Be more liberal in which Redis clients we accept. 141 | 142 | ## 1.8.4 (2010-05-18) 143 | 144 | * Try to resolve redis-namespace dependency issue 145 | 146 | ## 1.8.3 (2010-05-17) 147 | 148 | * Depend on redis-rb ~> 1.0.7 149 | 150 | ## 1.8.2 (2010-05-03) 151 | 152 | * Bugfix: Include "tasks/" dir in RubyGem 153 | 154 | ## 1.8.1 (2010-04-29) 155 | 156 | * Bugfix: Multiple failure backend did not support requeue-ing failed jobs 157 | * Bugfix: Fix /failed when error has no backtrace 158 | * Bugfix: Add `Redis::DistRedis` as a valid client 159 | 160 | ## 1.8.0 (2010-04-07) 161 | 162 | * Jobs that never complete due to killed worker are now failed. 163 | * Worker "working" state is now maintained by the parent, not the child. 164 | * Stopped using deprecated redis.rb methods 165 | * `Worker.working` race condition fixed 166 | * `Worker#process` has been deprecated. 167 | * Monit example fixed 168 | * Redis::Client and Redis::Namespace can be passed to `Resque.redis=` 169 | 170 | ## 1.7.1 (2010-04-02) 171 | 172 | * Bugfix: Make job hook execution order consistent 173 | * Bugfix: stdout buffering in child process 174 | 175 | ## 1.7.0 (2010-03-31) 176 | 177 | * Job hooks API. See docs/HOOKS.md. 178 | * web: Hovering over dates shows a timestamp 179 | * web: AJAXify retry action for failed jobs 180 | * web bugfix: Fix pagination bug 181 | 182 | ## 1.6.1 (2010-03-25) 183 | 184 | * Bugfix: Workers may not be clearing their state correctly on 185 | shutdown 186 | * Added example monit config. 187 | * Exception class is now recorded when an error is raised in a 188 | worker. 189 | * web: Unit tests 190 | * web: Show namespace in header and footer 191 | * web: Remove a queue 192 | * web: Retry failed jobs 193 | 194 | ## 1.6.0 (2010-03-09) 195 | 196 | * Added `before_first_fork`, `before_fork`, and `after_fork` hooks. 197 | * Hoptoad: Added server_environment config setting 198 | * Hoptoad bugfix: Don't depend on RAILS_ROOT 199 | * 1.8.6 compat fixes 200 | 201 | ## 1.5.2 (2010-03-03) 202 | 203 | * Bugfix: JSON check was crazy. 204 | 205 | ## 1.5.1 (2010-03-03) 206 | 207 | * `Job.destroy` and `Resque.dequeue` return the # of destroyed jobs. 208 | * Hoptoad notifier improvements 209 | * Specify the namespace with `resque-web` by passing `-N namespace` 210 | * Bugfix: Don't crash when trying to parse invalid JSON. 211 | * Bugfix: Non-standard namespace support 212 | * Web: Red backgound for queue "failed" only shown if there are failed jobs. 213 | * Web bugfix: Tabs highlight properly now 214 | * Web bugfix: ZSET partial support in stats 215 | * Web bugfix: Deleting failed jobs works again 216 | * Web bugfix: Sets (or zsets, lists, etc) now paginate. 217 | 218 | ## 1.5.0 (2010-02-17) 219 | 220 | * Version now included in procline, e.g. `resque-1.5.0: Message` 221 | * Web bugfix: Ignore idle works in the "working" page 222 | * Added `Resque::Job.destroy(queue, klass, *args)` 223 | * Added `Resque.dequeue(klass, *args)` 224 | 225 | ## 1.4.0 (2010-02-11) 226 | 227 | * Fallback when unable to bind QUIT and USR1 for Windows and JRuby. 228 | * Fallback when no `Kernel.fork` is provided (for IronRuby). 229 | * Web: Rounded corners in Firefox 230 | * Cut down system calls in `Worker#prune_dead_workers` 231 | * Enable switching DB in a Redis server from config 232 | * Support USR2 and CONT to stop and start job processing. 233 | * Web: Add example failing job 234 | * Bugfix: `Worker#unregister_worker` shouldn't call `done_working` 235 | * Bugfix: Example god config now restarts Resque properly. 236 | * Multiple failure backends now permitted. 237 | * Hoptoad failure backend updated to new API 238 | 239 | ## 1.3.1 (2010-01-11) 240 | 241 | * Vegas bugfix: Don't error without a config 242 | 243 | ## 1.3.0 (2010-01-11) 244 | 245 | * Use Vegas for resque-web 246 | * Web Bugfix: Show proper date/time value for failed_at on Failures 247 | * Web Bugfix: Make the / route more flexible 248 | * Add Resque::Server.tabs array (so plugins can add their own tabs) 249 | * Start using [Semantic Versioning](http://semver.org/) 250 | 251 | ## 1.2.4 (2009-12-15) 252 | 253 | * Web Bugfix: fix key links on stat page 254 | 255 | ## 1.2.3 (2009-12-15) 256 | 257 | * Bugfix: Fixed `rand` seeding in child processes. 258 | * Bugfix: Better JSON encoding/decoding without Yajl. 259 | * Bugfix: Avoid `ps` flag error on Linux 260 | * Add `PREFIX` observance to `rake` install tasks. 261 | 262 | ## 1.2.2 (2009-12-08) 263 | 264 | * Bugfix: Job equality was not properly implemented. 265 | 266 | ## 1.2.1 (2009-12-07) 267 | 268 | * Added `rake resque:workers` task for starting multiple workers. 269 | * 1.9.x compatibility 270 | * Bugfix: Yajl decoder doesn't care about valid UTF-8 271 | * config.ru loads RESQUECONFIG if the ENV variable is set. 272 | * `resque-web` now sets RESQUECONFIG 273 | * Job objects know if they are equal. 274 | * Jobs can be re-queued using `Job#recreate` 275 | 276 | ## 1.2.0 (2009-11-25) 277 | 278 | * If USR1 is sent and no child is found, shutdown. 279 | * Raise when a job class does not respond to `perform`. 280 | * Added `Resque.remove_queue` for deleting a queue 281 | 282 | ## 1.1.0 (2009-11-04) 283 | 284 | * Bugfix: Broken ERB tag in failure UI 285 | * Bugfix: Save the worker's ID, not the worker itself, in the failure module 286 | * Redesigned the sinatra web interface 287 | * Added option to clear failed jobs 288 | 289 | ## 1.0.0 (2009-11-03) 290 | 291 | * First release. 292 | -------------------------------------------------------------------------------- /test/job_hooks_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | context "Resque::Job before_perform" do 4 | include PerformJob 5 | 6 | class ::BeforePerformJob 7 | def self.before_perform_record_history(history) 8 | history << :before_perform 9 | end 10 | 11 | def self.perform(history) 12 | history << :perform 13 | end 14 | end 15 | 16 | test "it runs before_perform before perform" do 17 | result = perform_job(BeforePerformJob, history=[]) 18 | assert_equal true, result, "perform returned true" 19 | assert_equal history, [:before_perform, :perform] 20 | end 21 | 22 | class ::BeforePerformJobFails 23 | def self.before_perform_fail_job(history) 24 | history << :before_perform 25 | raise StandardError 26 | end 27 | def self.perform(history) 28 | history << :perform 29 | end 30 | end 31 | 32 | test "raises an error and does not perform if before_perform fails" do 33 | history = [] 34 | assert_raises StandardError do 35 | perform_job(BeforePerformJobFails, history) 36 | end 37 | assert_equal history, [:before_perform], "Only before_perform was run" 38 | end 39 | 40 | class ::BeforePerformJobAborts 41 | def self.before_perform_abort(history) 42 | history << :before_perform 43 | raise Resque::Job::DontPerform 44 | end 45 | def self.perform(history) 46 | history << :perform 47 | end 48 | end 49 | 50 | test "does not perform if before_perform raises Resque::Job::DontPerform" do 51 | result = perform_job(BeforePerformJobAborts, history=[]) 52 | assert_equal false, result, "perform returned false" 53 | assert_equal history, [:before_perform], "Only before_perform was run" 54 | end 55 | end 56 | 57 | context "Resque::Job after_perform" do 58 | include PerformJob 59 | 60 | class ::AfterPerformJob 61 | def self.perform(history) 62 | history << :perform 63 | end 64 | def self.after_perform_record_history(history) 65 | history << :after_perform 66 | end 67 | end 68 | 69 | test "it runs after_perform after perform" do 70 | result = perform_job(AfterPerformJob, history=[]) 71 | assert_equal true, result, "perform returned true" 72 | assert_equal history, [:perform, :after_perform] 73 | end 74 | 75 | class ::AfterPerformJobFails 76 | def self.perform(history) 77 | history << :perform 78 | end 79 | def self.after_perform_fail_job(history) 80 | history << :after_perform 81 | raise StandardError 82 | end 83 | end 84 | 85 | test "raises an error but has already performed if after_perform fails" do 86 | history = [] 87 | assert_raises StandardError do 88 | perform_job(AfterPerformJobFails, history) 89 | end 90 | assert_equal history, [:perform, :after_perform], "Only after_perform was run" 91 | end 92 | end 93 | 94 | context "Resque::Job around_perform" do 95 | include PerformJob 96 | 97 | class ::AroundPerformJob 98 | def self.perform(history) 99 | history << :perform 100 | end 101 | def self.around_perform_record_history(history) 102 | history << :start_around_perform 103 | yield 104 | history << :finish_around_perform 105 | end 106 | end 107 | 108 | test "it runs around_perform then yields in order to perform" do 109 | result = perform_job(AroundPerformJob, history=[]) 110 | assert_equal true, result, "perform returned true" 111 | assert_equal history, [:start_around_perform, :perform, :finish_around_perform] 112 | end 113 | 114 | class ::AroundPerformJobFailsBeforePerforming 115 | def self.perform(history) 116 | history << :perform 117 | end 118 | def self.around_perform_fail(history) 119 | history << :start_around_perform 120 | raise StandardError 121 | yield 122 | history << :finish_around_perform 123 | end 124 | end 125 | 126 | test "raises an error and does not perform if around_perform fails before yielding" do 127 | history = [] 128 | assert_raises StandardError do 129 | perform_job(AroundPerformJobFailsBeforePerforming, history) 130 | end 131 | assert_equal history, [:start_around_perform], "Only part of around_perform was run" 132 | end 133 | 134 | class ::AroundPerformJobFailsWhilePerforming 135 | def self.perform(history) 136 | history << :perform 137 | raise StandardError 138 | end 139 | def self.around_perform_fail_in_yield(history) 140 | history << :start_around_perform 141 | begin 142 | yield 143 | ensure 144 | history << :ensure_around_perform 145 | end 146 | history << :finish_around_perform 147 | end 148 | end 149 | 150 | test "raises an error but may handle exceptions if perform fails" do 151 | history = [] 152 | assert_raises StandardError do 153 | perform_job(AroundPerformJobFailsWhilePerforming, history) 154 | end 155 | assert_equal history, [:start_around_perform, :perform, :ensure_around_perform], "Only part of around_perform was run" 156 | end 157 | 158 | class ::AroundPerformJobDoesNotHaveToYield 159 | def self.perform(history) 160 | history << :perform 161 | end 162 | def self.around_perform_dont_yield(history) 163 | history << :start_around_perform 164 | history << :finish_around_perform 165 | end 166 | end 167 | 168 | test "around_perform is not required to yield" do 169 | history = [] 170 | result = perform_job(AroundPerformJobDoesNotHaveToYield, history) 171 | assert_equal false, result, "perform returns false" 172 | assert_equal history, [:start_around_perform, :finish_around_perform], "perform was not run" 173 | end 174 | end 175 | 176 | context "Resque::Job on_failure" do 177 | include PerformJob 178 | 179 | class ::FailureJobThatDoesNotFail 180 | def self.perform(history) 181 | history << :perform 182 | end 183 | def self.on_failure_record_failure(exception, history) 184 | history << exception.message 185 | end 186 | end 187 | 188 | test "it does not call on_failure if no failures occur" do 189 | result = perform_job(FailureJobThatDoesNotFail, history=[]) 190 | assert_equal true, result, "perform returned true" 191 | assert_equal history, [:perform] 192 | end 193 | 194 | class ::FailureJobThatFails 195 | def self.perform(history) 196 | history << :perform 197 | raise StandardError, "oh no" 198 | end 199 | def self.on_failure_record_failure(exception, history) 200 | history << exception.message 201 | end 202 | end 203 | 204 | test "it calls on_failure with the exception and then re-raises the exception" do 205 | history = [] 206 | assert_raises StandardError do 207 | perform_job(FailureJobThatFails, history) 208 | end 209 | assert_equal history, [:perform, "oh no"] 210 | end 211 | 212 | class ::FailureJobThatFailsBadly 213 | def self.perform(history) 214 | history << :perform 215 | raise SyntaxError, "oh no" 216 | end 217 | def self.on_failure_record_failure(exception, history) 218 | history << exception.message 219 | end 220 | end 221 | 222 | test "it calls on_failure even with bad exceptions" do 223 | history = [] 224 | assert_raises SyntaxError do 225 | perform_job(FailureJobThatFailsBadly, history) 226 | end 227 | assert_equal history, [:perform, "oh no"] 228 | end 229 | end 230 | 231 | context "Resque::Job after_enqueue" do 232 | include PerformJob 233 | 234 | class ::AfterEnqueueJob 235 | @queue = :jobs 236 | def self.after_enqueue_record_history(history) 237 | history << :after_enqueue 238 | end 239 | 240 | def self.perform(history) 241 | end 242 | end 243 | 244 | test "the after enqueue hook should run" do 245 | history = [] 246 | @worker = Resque::Worker.new(:jobs) 247 | Resque.enqueue(AfterEnqueueJob, history) 248 | @worker.work(0) 249 | assert_equal history, [:after_enqueue], "after_enqueue was not run" 250 | end 251 | end 252 | 253 | context "Resque::Job all hooks" do 254 | include PerformJob 255 | 256 | class ::VeryHookyJob 257 | def self.before_perform_record_history(history) 258 | history << :before_perform 259 | end 260 | def self.around_perform_record_history(history) 261 | history << :start_around_perform 262 | yield 263 | history << :finish_around_perform 264 | end 265 | def self.perform(history) 266 | history << :perform 267 | end 268 | def self.after_perform_record_history(history) 269 | history << :after_perform 270 | end 271 | def self.on_failure_record_history(exception, history) 272 | history << exception.message 273 | end 274 | end 275 | 276 | test "the complete hook order" do 277 | result = perform_job(VeryHookyJob, history=[]) 278 | assert_equal true, result, "perform returned true" 279 | assert_equal history, [ 280 | :before_perform, 281 | :start_around_perform, 282 | :perform, 283 | :finish_around_perform, 284 | :after_perform 285 | ] 286 | end 287 | 288 | class ::VeryHookyJobThatFails 289 | def self.before_perform_record_history(history) 290 | history << :before_perform 291 | end 292 | def self.around_perform_record_history(history) 293 | history << :start_around_perform 294 | yield 295 | history << :finish_around_perform 296 | end 297 | def self.perform(history) 298 | history << :perform 299 | end 300 | def self.after_perform_record_history(history) 301 | history << :after_perform 302 | raise StandardError, "oh no" 303 | end 304 | def self.on_failure_record_history(exception, history) 305 | history << exception.message 306 | end 307 | end 308 | 309 | test "the complete hook order with a failure at the last minute" do 310 | history = [] 311 | assert_raises StandardError do 312 | perform_job(VeryHookyJobThatFails, history) 313 | end 314 | assert_equal history, [ 315 | :before_perform, 316 | :start_around_perform, 317 | :perform, 318 | :finish_around_perform, 319 | :after_perform, 320 | "oh no" 321 | ] 322 | end 323 | end 324 | -------------------------------------------------------------------------------- /test/worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | context "Resque::Worker" do 4 | setup do 5 | Resque.redis.flushall 6 | 7 | Resque.before_first_fork = nil 8 | Resque.before_fork = nil 9 | Resque.after_fork = nil 10 | 11 | @worker = Resque::Worker.new(:jobs) 12 | Resque::Job.create(:jobs, SomeJob, 20, '/tmp') 13 | end 14 | 15 | test "can fail jobs" do 16 | Resque::Job.create(:jobs, BadJob) 17 | @worker.work(0) 18 | assert_equal 1, Resque::Failure.count 19 | end 20 | 21 | test "failed jobs report exception and message" do 22 | Resque::Job.create(:jobs, BadJobWithSyntaxError) 23 | @worker.work(0) 24 | assert_equal('SyntaxError', Resque::Failure.all['exception']) 25 | assert_equal('Extra Bad job!', Resque::Failure.all['error']) 26 | end 27 | 28 | test "does not allow exceptions from failure backend to escape" do 29 | job = Resque::Job.new(:jobs, {}) 30 | with_failure_backend BadFailureBackend do 31 | @worker.perform job 32 | end 33 | end 34 | 35 | test "fails uncompleted jobs on exit" do 36 | job = Resque::Job.new(:jobs, [GoodJob, "blah"]) 37 | @worker.working_on(job) 38 | @worker.unregister_worker 39 | assert_equal 1, Resque::Failure.count 40 | end 41 | 42 | test "can peek at failed jobs" do 43 | 10.times { Resque::Job.create(:jobs, BadJob) } 44 | @worker.work(0) 45 | assert_equal 10, Resque::Failure.count 46 | 47 | assert_equal 10, Resque::Failure.all(0, 20).size 48 | end 49 | 50 | test "can clear failed jobs" do 51 | Resque::Job.create(:jobs, BadJob) 52 | @worker.work(0) 53 | assert_equal 1, Resque::Failure.count 54 | Resque::Failure.clear 55 | assert_equal 0, Resque::Failure.count 56 | end 57 | 58 | test "catches exceptional jobs" do 59 | Resque::Job.create(:jobs, BadJob) 60 | Resque::Job.create(:jobs, BadJob) 61 | @worker.process 62 | @worker.process 63 | @worker.process 64 | assert_equal 2, Resque::Failure.count 65 | end 66 | 67 | test "strips whitespace from queue names" do 68 | queues = "critical, high, low".split(',') 69 | worker = Resque::Worker.new(*queues) 70 | assert_equal %w( critical high low ), worker.queues 71 | end 72 | 73 | test "can work on multiple queues" do 74 | Resque::Job.create(:high, GoodJob) 75 | Resque::Job.create(:critical, GoodJob) 76 | 77 | worker = Resque::Worker.new(:critical, :high) 78 | 79 | worker.process 80 | assert_equal 1, Resque.size(:high) 81 | assert_equal 0, Resque.size(:critical) 82 | 83 | worker.process 84 | assert_equal 0, Resque.size(:high) 85 | end 86 | 87 | test "can work on all queues" do 88 | Resque::Job.create(:high, GoodJob) 89 | Resque::Job.create(:critical, GoodJob) 90 | Resque::Job.create(:blahblah, GoodJob) 91 | 92 | worker = Resque::Worker.new("*") 93 | 94 | worker.work(0) 95 | assert_equal 0, Resque.size(:high) 96 | assert_equal 0, Resque.size(:critical) 97 | assert_equal 0, Resque.size(:blahblah) 98 | end 99 | 100 | test "processes * queues in alphabetical order" do 101 | Resque::Job.create(:high, GoodJob) 102 | Resque::Job.create(:critical, GoodJob) 103 | Resque::Job.create(:blahblah, GoodJob) 104 | 105 | worker = Resque::Worker.new("*") 106 | processed_queues = [] 107 | 108 | worker.work(0) do |job| 109 | processed_queues << job.queue 110 | end 111 | 112 | assert_equal %w( jobs high critical blahblah ).sort, processed_queues 113 | end 114 | 115 | test "has a unique id" do 116 | assert_equal "#{`hostname`.chomp}:#{$$}:jobs", @worker.to_s 117 | end 118 | 119 | test "complains if no queues are given" do 120 | assert_raise Resque::NoQueueError do 121 | Resque::Worker.new 122 | end 123 | end 124 | 125 | test "fails if a job class has no `perform` method" do 126 | worker = Resque::Worker.new(:perform_less) 127 | Resque::Job.create(:perform_less, Object) 128 | 129 | assert_equal 0, Resque::Failure.count 130 | worker.work(0) 131 | assert_equal 1, Resque::Failure.count 132 | end 133 | 134 | test "inserts itself into the 'workers' list on startup" do 135 | @worker.work(0) do 136 | assert_equal @worker, Resque.workers[0] 137 | end 138 | end 139 | 140 | test "removes itself from the 'workers' list on shutdown" do 141 | @worker.work(0) do 142 | assert_equal @worker, Resque.workers[0] 143 | end 144 | 145 | assert_equal [], Resque.workers 146 | end 147 | 148 | test "removes worker with stringified id" do 149 | @worker.work(0) do 150 | worker_id = Resque.workers[0].to_s 151 | Resque.remove_worker(worker_id) 152 | assert_equal [], Resque.workers 153 | end 154 | end 155 | 156 | test "records what it is working on" do 157 | @worker.work(0) do 158 | task = @worker.job 159 | assert_equal({"args"=>[20, "/tmp"], "class"=>"SomeJob"}, task['payload']) 160 | assert task['run_at'] 161 | assert_equal 'jobs', task['queue'] 162 | end 163 | end 164 | 165 | test "clears its status when not working on anything" do 166 | @worker.work(0) 167 | assert_equal Hash.new, @worker.job 168 | end 169 | 170 | test "knows when it is working" do 171 | @worker.work(0) do 172 | assert @worker.working? 173 | end 174 | end 175 | 176 | test "knows when it is idle" do 177 | @worker.work(0) 178 | assert @worker.idle? 179 | end 180 | 181 | test "knows who is working" do 182 | @worker.work(0) do 183 | assert_equal [@worker], Resque.working 184 | end 185 | end 186 | 187 | test "keeps track of how many jobs it has processed" do 188 | Resque::Job.create(:jobs, BadJob) 189 | Resque::Job.create(:jobs, BadJob) 190 | 191 | 3.times do 192 | job = @worker.reserve 193 | @worker.process job 194 | end 195 | assert_equal 3, @worker.processed 196 | end 197 | 198 | test "keeps track of how many failures it has seen" do 199 | Resque::Job.create(:jobs, BadJob) 200 | Resque::Job.create(:jobs, BadJob) 201 | 202 | 3.times do 203 | job = @worker.reserve 204 | @worker.process job 205 | end 206 | assert_equal 2, @worker.failed 207 | end 208 | 209 | test "stats are erased when the worker goes away" do 210 | @worker.work(0) 211 | assert_equal 0, @worker.processed 212 | assert_equal 0, @worker.failed 213 | end 214 | 215 | test "knows when it started" do 216 | time = Time.now 217 | @worker.work(0) do 218 | assert_equal time.to_s, @worker.started.to_s 219 | end 220 | end 221 | 222 | test "knows whether it exists or not" do 223 | @worker.work(0) do 224 | assert Resque::Worker.exists?(@worker) 225 | assert !Resque::Worker.exists?('blah-blah') 226 | end 227 | end 228 | 229 | test "sets $0 while working" do 230 | @worker.work(0) do 231 | ver = Resque::Version 232 | assert_equal "resque-#{ver}: Processing jobs since #{Time.now.to_i}", $0 233 | end 234 | end 235 | 236 | test "can be found" do 237 | @worker.work(0) do 238 | found = Resque::Worker.find(@worker.to_s) 239 | assert_equal @worker.to_s, found.to_s 240 | assert found.working? 241 | assert_equal @worker.job, found.job 242 | end 243 | end 244 | 245 | test "doesn't find fakes" do 246 | @worker.work(0) do 247 | found = Resque::Worker.find('blah-blah') 248 | assert_equal nil, found 249 | end 250 | end 251 | 252 | test "cleans up dead worker info on start (crash recovery)" do 253 | # first we fake out two dead workers 254 | workerA = Resque::Worker.new(:jobs) 255 | workerA.instance_variable_set(:@to_s, "#{`hostname`.chomp}:1:jobs") 256 | workerA.register_worker 257 | 258 | workerB = Resque::Worker.new(:high, :low) 259 | workerB.instance_variable_set(:@to_s, "#{`hostname`.chomp}:2:high,low") 260 | workerB.register_worker 261 | 262 | assert_equal 2, Resque.workers.size 263 | 264 | # then we prune them 265 | @worker.work(0) do 266 | assert_equal 1, Resque.workers.size 267 | end 268 | end 269 | 270 | test "Processed jobs count" do 271 | @worker.work(0) 272 | assert_equal 1, Resque.info[:processed] 273 | end 274 | 275 | test "Will call a before_first_fork hook only once" do 276 | Resque.redis.flushall 277 | $BEFORE_FORK_CALLED = 0 278 | Resque.before_first_fork = Proc.new { $BEFORE_FORK_CALLED += 1 } 279 | workerA = Resque::Worker.new(:jobs) 280 | Resque::Job.create(:jobs, SomeJob, 20, '/tmp') 281 | 282 | assert_equal 0, $BEFORE_FORK_CALLED 283 | 284 | workerA.work(0) 285 | assert_equal 1, $BEFORE_FORK_CALLED 286 | 287 | # TODO: Verify it's only run once. Not easy. 288 | # workerA.work(0) 289 | # assert_equal 1, $BEFORE_FORK_CALLED 290 | end 291 | 292 | test "Will call a before_fork hook before forking" do 293 | Resque.redis.flushall 294 | $BEFORE_FORK_CALLED = false 295 | Resque.before_fork = Proc.new { $BEFORE_FORK_CALLED = true } 296 | workerA = Resque::Worker.new(:jobs) 297 | 298 | assert !$BEFORE_FORK_CALLED 299 | Resque::Job.create(:jobs, SomeJob, 20, '/tmp') 300 | workerA.work(0) 301 | assert $BEFORE_FORK_CALLED 302 | end 303 | 304 | test "very verbose works in the afternoon" do 305 | require 'time' 306 | $last_puts = "" 307 | $fake_time = Time.parse("15:44:33 2011-03-02") 308 | singleton = class << @worker; self end 309 | singleton.send :define_method, :puts, lambda { |thing| $last_puts = thing } 310 | 311 | @worker.very_verbose = true 312 | @worker.log("some log text") 313 | 314 | assert_match /\*\* \[15:44:33 2011-03-02\] \d+: some log text/, $last_puts 315 | end 316 | 317 | test "Will call an after_fork hook after forking" do 318 | Resque.redis.flushall 319 | $AFTER_FORK_CALLED = false 320 | Resque.after_fork = Proc.new { $AFTER_FORK_CALLED = true } 321 | workerA = Resque::Worker.new(:jobs) 322 | 323 | assert !$AFTER_FORK_CALLED 324 | Resque::Job.create(:jobs, SomeJob, 20, '/tmp') 325 | workerA.work(0) 326 | assert $AFTER_FORK_CALLED 327 | end 328 | 329 | test "returns PID of running process" do 330 | assert_equal @worker.to_s.split(":")[1].to_i, @worker.pid 331 | end 332 | end 333 | -------------------------------------------------------------------------------- /lib/resque.rb: -------------------------------------------------------------------------------- 1 | require 'redis/namespace' 2 | 3 | require 'resque/version' 4 | 5 | require 'resque/errors' 6 | 7 | require 'resque/failure' 8 | require 'resque/failure/base' 9 | 10 | require 'resque/helpers' 11 | require 'resque/stat' 12 | require 'resque/job' 13 | require 'resque/worker' 14 | require 'resque/plugin' 15 | 16 | module Resque 17 | include Helpers 18 | extend self 19 | 20 | # Accepts: 21 | # 1. A 'hostname:port' String 22 | # 2. A 'hostname:port:db' String (to select the Redis db) 23 | # 3. A 'hostname:port/namespace' String (to set the Redis namespace) 24 | # 4. A Redis URL String 'redis://host:port' 25 | # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`, 26 | # or `Redis::Namespace`. 27 | def redis=(server) 28 | case server 29 | when String 30 | if server =~ /redis\:\/\// 31 | redis = Redis.connect(:url => server, :thread_safe => true) 32 | else 33 | server, namespace = server.split('/', 2) 34 | host, port, db = server.split(':') 35 | redis = Redis.new(:host => host, :port => port, 36 | :thread_safe => true, :db => db) 37 | end 38 | namespace ||= :resque 39 | 40 | @redis = Redis::Namespace.new(namespace, :redis => redis) 41 | when Redis::Namespace 42 | @redis = server 43 | else 44 | @redis = Redis::Namespace.new(:resque, :redis => server) 45 | end 46 | end 47 | 48 | # Returns the current Redis connection. If none has been created, will 49 | # create a new one. 50 | def redis 51 | return @redis if @redis 52 | self.redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379" 53 | self.redis 54 | end 55 | 56 | def redis_id 57 | # support 1.x versions of redis-rb 58 | if redis.respond_to?(:server) 59 | redis.server 60 | elsif redis.respond_to?(:nodes) # distributed 61 | redis.nodes.map { |n| n.id }.join(', ') 62 | else 63 | redis.client.id 64 | end 65 | end 66 | 67 | # The `before_first_fork` hook will be run in the **parent** process 68 | # only once, before forking to run the first job. Be careful- any 69 | # changes you make will be permanent for the lifespan of the 70 | # worker. 71 | # 72 | # Call with a block to set the hook. 73 | # Call with no arguments to return the hook. 74 | def before_first_fork(&block) 75 | block ? (@before_first_fork = block) : @before_first_fork 76 | end 77 | 78 | # Set a proc that will be called in the parent process before the 79 | # worker forks for the first time. 80 | def before_first_fork=(before_first_fork) 81 | @before_first_fork = before_first_fork 82 | end 83 | 84 | # The `before_fork` hook will be run in the **parent** process 85 | # before every job, so be careful- any changes you make will be 86 | # permanent for the lifespan of the worker. 87 | # 88 | # Call with a block to set the hook. 89 | # Call with no arguments to return the hook. 90 | def before_fork(&block) 91 | block ? (@before_fork = block) : @before_fork 92 | end 93 | 94 | # Set the before_fork proc. 95 | def before_fork=(before_fork) 96 | @before_fork = before_fork 97 | end 98 | 99 | # The `after_fork` hook will be run in the child process and is passed 100 | # the current job. Any changes you make, therefore, will only live as 101 | # long as the job currently being processed. 102 | # 103 | # Call with a block to set the hook. 104 | # Call with no arguments to return the hook. 105 | def after_fork(&block) 106 | block ? (@after_fork = block) : @after_fork 107 | end 108 | 109 | # Set the after_fork proc. 110 | def after_fork=(after_fork) 111 | @after_fork = after_fork 112 | end 113 | 114 | def to_s 115 | "Resque Client connected to #{redis_id}" 116 | end 117 | 118 | # If 'inline' is true Resque will call #perform method inline 119 | # without queuing it into Redis and without any Resque callbacks. 120 | # The 'inline' is false Resque jobs will be put in queue regularly. 121 | def inline? 122 | @inline 123 | end 124 | alias_method :inline, :inline? 125 | 126 | def inline=(inline) 127 | @inline = inline 128 | end 129 | 130 | # 131 | # queue manipulation 132 | # 133 | 134 | # Pushes a job onto a queue. Queue name should be a string and the 135 | # item should be any JSON-able Ruby object. 136 | # 137 | # Resque works generally expect the `item` to be a hash with the following 138 | # keys: 139 | # 140 | # class - The String name of the job to run. 141 | # args - An Array of arguments to pass the job. Usually passed 142 | # via `class.to_class.perform(*args)`. 143 | # 144 | # Example 145 | # 146 | # Resque.push('archive', :class => 'Archive', :args => [ 35, 'tar' ]) 147 | # 148 | # Returns nothing 149 | def push(queue, item) 150 | watch_queue(queue) 151 | redis.rpush "queue:#{queue}", encode(item) 152 | end 153 | 154 | # Pops a job off a queue. Queue name should be a string. 155 | # 156 | # Returns a Ruby object. 157 | def pop(queue) 158 | decode redis.lpop("queue:#{queue}") 159 | end 160 | 161 | # Returns an integer representing the size of a queue. 162 | # Queue name should be a string. 163 | def size(queue) 164 | redis.llen("queue:#{queue}").to_i 165 | end 166 | 167 | # Returns an array of items currently queued. Queue name should be 168 | # a string. 169 | # 170 | # start and count should be integer and can be used for pagination. 171 | # start is the item to begin, count is how many items to return. 172 | # 173 | # To get the 3rd page of a 30 item, paginatied list one would use: 174 | # Resque.peek('my_list', 59, 30) 175 | def peek(queue, start = 0, count = 1) 176 | list_range("queue:#{queue}", start, count) 177 | end 178 | 179 | # Does the dirty work of fetching a range of items from a Redis list 180 | # and converting them into Ruby objects. 181 | def list_range(key, start = 0, count = 1) 182 | if count == 1 183 | decode redis.lindex(key, start) 184 | else 185 | Array(redis.lrange(key, start, start+count-1)).map do |item| 186 | decode item 187 | end 188 | end 189 | end 190 | 191 | # Returns an array of all known Resque queues as strings. 192 | def queues 193 | Array(redis.smembers(:queues)) 194 | end 195 | 196 | # Given a queue name, completely deletes the queue. 197 | def remove_queue(queue) 198 | redis.srem(:queues, queue.to_s) 199 | redis.del("queue:#{queue}") 200 | end 201 | 202 | # Used internally to keep track of which queues we've created. 203 | # Don't call this directly. 204 | def watch_queue(queue) 205 | redis.sadd(:queues, queue.to_s) 206 | end 207 | 208 | 209 | # 210 | # job shortcuts 211 | # 212 | 213 | # This method can be used to conveniently add a job to a queue. 214 | # It assumes the class you're passing it is a real Ruby class (not 215 | # a string or reference) which either: 216 | # 217 | # a) has a @queue ivar set 218 | # b) responds to `queue` 219 | # 220 | # If either of those conditions are met, it will use the value obtained 221 | # from performing one of the above operations to determine the queue. 222 | # 223 | # If no queue can be inferred this method will raise a `Resque::NoQueueError` 224 | # 225 | # This method is considered part of the `stable` API. 226 | def enqueue(klass, *args) 227 | Job.create(queue_from_class(klass), klass, *args) 228 | 229 | Plugin.after_enqueue_hooks(klass).each do |hook| 230 | klass.send(hook, *args) 231 | end 232 | end 233 | 234 | # This method can be used to conveniently remove a job from a queue. 235 | # It assumes the class you're passing it is a real Ruby class (not 236 | # a string or reference) which either: 237 | # 238 | # a) has a @queue ivar set 239 | # b) responds to `queue` 240 | # 241 | # If either of those conditions are met, it will use the value obtained 242 | # from performing one of the above operations to determine the queue. 243 | # 244 | # If no queue can be inferred this method will raise a `Resque::NoQueueError` 245 | # 246 | # If no args are given, this method will dequeue *all* jobs matching 247 | # the provided class. See `Resque::Job.destroy` for more 248 | # information. 249 | # 250 | # Returns the number of jobs destroyed. 251 | # 252 | # Example: 253 | # 254 | # # Removes all jobs of class `UpdateNetworkGraph` 255 | # Resque.dequeue(GitHub::Jobs::UpdateNetworkGraph) 256 | # 257 | # # Removes all jobs of class `UpdateNetworkGraph` with matching args. 258 | # Resque.dequeue(GitHub::Jobs::UpdateNetworkGraph, 'repo:135325') 259 | # 260 | # This method is considered part of the `stable` API. 261 | def dequeue(klass, *args) 262 | Job.destroy(queue_from_class(klass), klass, *args) 263 | end 264 | 265 | # Given a class, try to extrapolate an appropriate queue based on a 266 | # class instance variable or `queue` method. 267 | def queue_from_class(klass) 268 | klass.instance_variable_get(:@queue) || 269 | (klass.respond_to?(:queue) and klass.queue) 270 | end 271 | 272 | # This method will return a `Resque::Job` object or a non-true value 273 | # depending on whether a job can be obtained. You should pass it the 274 | # precise name of a queue: case matters. 275 | # 276 | # This method is considered part of the `stable` API. 277 | def reserve(queue) 278 | Job.reserve(queue) 279 | end 280 | 281 | # Validates if the given klass could be a valid Resque job 282 | # 283 | # If no queue can be inferred this method will raise a `Resque::NoQueueError` 284 | # 285 | # If given klass is nil this method will raise a `Resque::NoClassError` 286 | def validate(klass, queue = nil) 287 | queue ||= queue_from_class(klass) 288 | 289 | if !queue 290 | raise NoQueueError.new("Jobs must be placed onto a queue.") 291 | end 292 | 293 | if klass.to_s.empty? 294 | raise NoClassError.new("Jobs must be given a class.") 295 | end 296 | end 297 | 298 | 299 | # 300 | # worker shortcuts 301 | # 302 | 303 | # A shortcut to Worker.all 304 | def workers 305 | Worker.all 306 | end 307 | 308 | # A shortcut to Worker.working 309 | def working 310 | Worker.working 311 | end 312 | 313 | # A shortcut to unregister_worker 314 | # useful for command line tool 315 | def remove_worker(worker_id) 316 | worker = Resque::Worker.find(worker_id) 317 | worker.unregister_worker 318 | end 319 | 320 | # 321 | # stats 322 | # 323 | 324 | # Returns a hash, similar to redis-rb's #info, of interesting stats. 325 | def info 326 | return { 327 | :pending => queues.inject(0) { |m,k| m + size(k) }, 328 | :processed => Stat[:processed], 329 | :queues => queues.size, 330 | :workers => workers.size.to_i, 331 | :working => working.size, 332 | :failed => Stat[:failed], 333 | :servers => [redis_id], 334 | :environment => ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' 335 | } 336 | end 337 | 338 | # Returns an array of all known Resque keys in Redis. Redis' KEYS operation 339 | # is O(N) for the keyspace, so be careful - this can be slow for big databases. 340 | def keys 341 | redis.keys("*").map do |key| 342 | key.sub("#{redis.namespace}:", '') 343 | end 344 | end 345 | end 346 | 347 | -------------------------------------------------------------------------------- /lib/resque/worker.rb: -------------------------------------------------------------------------------- 1 | module Resque 2 | # A Resque Worker processes jobs. On platforms that support fork(2), 3 | # the worker will fork off a child to process each job. This ensures 4 | # a clean slate when beginning the next job and cuts down on gradual 5 | # memory growth as well as low level failures. 6 | # 7 | # It also ensures workers are always listening to signals from you, 8 | # their master, and can react accordingly. 9 | class Worker 10 | include Resque::Helpers 11 | extend Resque::Helpers 12 | 13 | # Whether the worker should log basic info to STDOUT 14 | attr_accessor :verbose 15 | 16 | # Whether the worker should log lots of info to STDOUT 17 | attr_accessor :very_verbose 18 | 19 | # Boolean indicating whether this worker can or can not fork. 20 | # Automatically set if a fork(2) fails. 21 | attr_accessor :cant_fork 22 | 23 | attr_writer :to_s 24 | 25 | # Returns an array of all worker objects. 26 | def self.all 27 | Array(redis.smembers(:workers)).map { |id| find(id) }.compact 28 | end 29 | 30 | # Returns an array of all worker objects currently processing 31 | # jobs. 32 | def self.working 33 | names = all 34 | return [] unless names.any? 35 | 36 | names.map! { |name| "worker:#{name}" } 37 | 38 | reportedly_working = redis.mapped_mget(*names).reject do |key, value| 39 | value.nil? 40 | end 41 | reportedly_working.keys.map do |key| 42 | find key.sub("worker:", '') 43 | end.compact 44 | end 45 | 46 | # Returns a single worker object. Accepts a string id. 47 | def self.find(worker_id) 48 | if exists? worker_id 49 | queues = worker_id.split(':')[-1].split(',') 50 | worker = new(*queues) 51 | worker.to_s = worker_id 52 | worker 53 | else 54 | nil 55 | end 56 | end 57 | 58 | # Alias of `find` 59 | def self.attach(worker_id) 60 | find(worker_id) 61 | end 62 | 63 | # Given a string worker id, return a boolean indicating whether the 64 | # worker exists 65 | def self.exists?(worker_id) 66 | redis.sismember(:workers, worker_id) 67 | end 68 | 69 | # Workers should be initialized with an array of string queue 70 | # names. The order is important: a Worker will check the first 71 | # queue given for a job. If none is found, it will check the 72 | # second queue name given. If a job is found, it will be 73 | # processed. Upon completion, the Worker will again check the 74 | # first queue given, and so forth. In this way the queue list 75 | # passed to a Worker on startup defines the priorities of queues. 76 | # 77 | # If passed a single "*", this Worker will operate on all queues 78 | # in alphabetical order. Queues can be dynamically added or 79 | # removed without needing to restart workers using this method. 80 | def initialize(*queues) 81 | @queues = queues.map { |queue| queue.to_s.strip } 82 | validate_queues 83 | end 84 | 85 | # A worker must be given a queue, otherwise it won't know what to 86 | # do with itself. 87 | # 88 | # You probably never need to call this. 89 | def validate_queues 90 | if @queues.nil? || @queues.empty? 91 | raise NoQueueError.new("Please give each worker at least one queue.") 92 | end 93 | end 94 | 95 | # This is the main workhorse method. Called on a Worker instance, 96 | # it begins the worker life cycle. 97 | # 98 | # The following events occur during a worker's life cycle: 99 | # 100 | # 1. Startup: Signals are registered, dead workers are pruned, 101 | # and this worker is registered. 102 | # 2. Work loop: Jobs are pulled from a queue and processed. 103 | # 3. Teardown: This worker is unregistered. 104 | # 105 | # Can be passed a float representing the polling frequency. 106 | # The default is 5 seconds, but for a semi-active site you may 107 | # want to use a smaller value. 108 | # 109 | # Also accepts a block which will be passed the job as soon as it 110 | # has completed processing. Useful for testing. 111 | def work(interval = 5.0, &block) 112 | interval = Float(interval) 113 | $0 = "resque: Starting" 114 | startup 115 | 116 | loop do 117 | break if shutdown? 118 | 119 | if not paused? and job = reserve 120 | log "got: #{job.inspect}" 121 | run_hook :before_fork, job 122 | working_on job 123 | 124 | if @child = fork 125 | srand # Reseeding 126 | procline "Forked #{@child} at #{Time.now.to_i}" 127 | Process.wait 128 | else 129 | procline "Processing #{job.queue} since #{Time.now.to_i}" 130 | perform(job, &block) 131 | exit! unless @cant_fork 132 | end 133 | 134 | done_working 135 | @child = nil 136 | else 137 | break if interval.zero? 138 | log! "Sleeping for #{interval} seconds" 139 | procline paused? ? "Paused" : "Waiting for #{@queues.join(',')}" 140 | sleep interval 141 | end 142 | end 143 | 144 | ensure 145 | unregister_worker 146 | end 147 | 148 | # DEPRECATED. Processes a single job. If none is given, it will 149 | # try to produce one. Usually run in the child. 150 | def process(job = nil, &block) 151 | return unless job ||= reserve 152 | 153 | working_on job 154 | perform(job, &block) 155 | ensure 156 | done_working 157 | end 158 | 159 | # Processes a given job in the child. 160 | def perform(job) 161 | begin 162 | run_hook :after_fork, job 163 | job.perform 164 | rescue Object => e 165 | log "#{job.inspect} failed: #{e.inspect}" 166 | begin 167 | job.fail(e) 168 | rescue Object => e 169 | log "Received exception when reporting failure: #{e.inspect}" 170 | end 171 | failed! 172 | else 173 | log "done: #{job.inspect}" 174 | ensure 175 | yield job if block_given? 176 | end 177 | end 178 | 179 | # Attempts to grab a job off one of the provided queues. Returns 180 | # nil if no job can be found. 181 | def reserve 182 | queues.each do |queue| 183 | log! "Checking #{queue}" 184 | if job = Resque::Job.reserve(queue) 185 | log! "Found job on #{queue}" 186 | return job 187 | end 188 | end 189 | 190 | nil 191 | rescue Exception => e 192 | log "Error reserving job: #{e.inspect}" 193 | log e.backtrace.join("\n") 194 | raise e 195 | end 196 | 197 | # Returns a list of queues to use when searching for a job. 198 | # A splat ("*") means you want every queue (in alpha order) - this 199 | # can be useful for dynamically adding new queues. 200 | def queues 201 | @queues[0] == "*" ? Resque.queues.sort : @queues 202 | end 203 | 204 | # Not every platform supports fork. Here we do our magic to 205 | # determine if yours does. 206 | def fork 207 | @cant_fork = true if $TESTING 208 | 209 | return if @cant_fork 210 | 211 | begin 212 | # IronRuby doesn't support `Kernel.fork` yet 213 | if Kernel.respond_to?(:fork) 214 | Kernel.fork 215 | else 216 | raise NotImplementedError 217 | end 218 | rescue NotImplementedError 219 | @cant_fork = true 220 | nil 221 | end 222 | end 223 | 224 | # Runs all the methods needed when a worker begins its lifecycle. 225 | def startup 226 | enable_gc_optimizations 227 | register_signal_handlers 228 | prune_dead_workers 229 | run_hook :before_first_fork 230 | register_worker 231 | 232 | # Fix buffering so we can `rake resque:work > resque.log` and 233 | # get output from the child in there. 234 | $stdout.sync = true 235 | end 236 | 237 | # Enables GC Optimizations if you're running REE. 238 | # http://www.rubyenterpriseedition.com/faq.html#adapt_apps_for_cow 239 | def enable_gc_optimizations 240 | if GC.respond_to?(:copy_on_write_friendly=) 241 | GC.copy_on_write_friendly = true 242 | end 243 | end 244 | 245 | # Registers the various signal handlers a worker responds to. 246 | # 247 | # TERM: Shutdown immediately, stop processing jobs. 248 | # INT: Shutdown immediately, stop processing jobs. 249 | # QUIT: Shutdown after the current job has finished processing. 250 | # USR1: Kill the forked child immediately, continue processing jobs. 251 | # USR2: Don't process any new jobs 252 | # CONT: Start processing jobs again after a USR2 253 | def register_signal_handlers 254 | trap('TERM') { shutdown! } 255 | trap('INT') { shutdown! } 256 | 257 | begin 258 | trap('QUIT') { shutdown } 259 | trap('USR1') { kill_child } 260 | trap('USR2') { pause_processing } 261 | trap('CONT') { unpause_processing } 262 | rescue ArgumentError 263 | warn "Signals QUIT, USR1, USR2, and/or CONT not supported." 264 | end 265 | 266 | log! "Registered signals" 267 | end 268 | 269 | # Schedule this worker for shutdown. Will finish processing the 270 | # current job. 271 | def shutdown 272 | log 'Exiting...' 273 | @shutdown = true 274 | end 275 | 276 | # Kill the child and shutdown immediately. 277 | def shutdown! 278 | shutdown 279 | kill_child 280 | end 281 | 282 | # Should this worker shutdown as soon as current job is finished? 283 | def shutdown? 284 | @shutdown 285 | end 286 | 287 | # Kills the forked child immediately, without remorse. The job it 288 | # is processing will not be completed. 289 | def kill_child 290 | if @child 291 | log! "Killing child at #{@child}" 292 | if system("ps -o pid,state -p #{@child}") 293 | Process.kill("KILL", @child) rescue nil 294 | else 295 | log! "Child #{@child} not found, restarting." 296 | shutdown 297 | end 298 | end 299 | end 300 | 301 | # are we paused? 302 | def paused? 303 | @paused 304 | end 305 | 306 | # Stop processing jobs after the current one has completed (if we're 307 | # currently running one). 308 | def pause_processing 309 | log "USR2 received; pausing job processing" 310 | @paused = true 311 | end 312 | 313 | # Start processing jobs again after a pause 314 | def unpause_processing 315 | log "CONT received; resuming job processing" 316 | @paused = false 317 | end 318 | 319 | # Looks for any workers which should be running on this server 320 | # and, if they're not, removes them from Redis. 321 | # 322 | # This is a form of garbage collection. If a server is killed by a 323 | # hard shutdown, power failure, or something else beyond our 324 | # control, the Resque workers will not die gracefully and therefore 325 | # will leave stale state information in Redis. 326 | # 327 | # By checking the current Redis state against the actual 328 | # environment, we can determine if Redis is old and clean it up a bit. 329 | def prune_dead_workers 330 | all_workers = Worker.all 331 | known_workers = worker_pids unless all_workers.empty? 332 | all_workers.each do |worker| 333 | host, pid, queues = worker.id.split(':') 334 | next unless host == hostname 335 | next if known_workers.include?(pid) 336 | log! "Pruning dead worker: #{worker}" 337 | worker.unregister_worker 338 | end 339 | end 340 | 341 | # Registers ourself as a worker. Useful when entering the worker 342 | # lifecycle on startup. 343 | def register_worker 344 | redis.sadd(:workers, self) 345 | started! 346 | end 347 | 348 | # Runs a named hook, passing along any arguments. 349 | def run_hook(name, *args) 350 | return unless hook = Resque.send(name) 351 | msg = "Running #{name} hook" 352 | msg << " with #{args.inspect}" if args.any? 353 | log msg 354 | 355 | args.any? ? hook.call(*args) : hook.call 356 | end 357 | 358 | # Unregisters ourself as a worker. Useful when shutting down. 359 | def unregister_worker 360 | # If we're still processing a job, make sure it gets logged as a 361 | # failure. 362 | if (hash = processing) && !hash.empty? 363 | job = Job.new(hash['queue'], hash['payload']) 364 | # Ensure the proper worker is attached to this job, even if 365 | # it's not the precise instance that died. 366 | job.worker = self 367 | job.fail(DirtyExit.new) 368 | end 369 | 370 | redis.srem(:workers, self) 371 | redis.del("worker:#{self}") 372 | redis.del("worker:#{self}:started") 373 | 374 | Stat.clear("processed:#{self}") 375 | Stat.clear("failed:#{self}") 376 | end 377 | 378 | # Given a job, tells Redis we're working on it. Useful for seeing 379 | # what workers are doing and when. 380 | def working_on(job) 381 | job.worker = self 382 | data = encode \ 383 | :queue => job.queue, 384 | :run_at => Time.now.to_s, 385 | :payload => job.payload 386 | redis.set("worker:#{self}", data) 387 | end 388 | 389 | # Called when we are done working - clears our `working_on` state 390 | # and tells Redis we processed a job. 391 | def done_working 392 | processed! 393 | redis.del("worker:#{self}") 394 | end 395 | 396 | # How many jobs has this worker processed? Returns an int. 397 | def processed 398 | Stat["processed:#{self}"] 399 | end 400 | 401 | # Tell Redis we've processed a job. 402 | def processed! 403 | Stat << "processed" 404 | Stat << "processed:#{self}" 405 | end 406 | 407 | # How many failed jobs has this worker seen? Returns an int. 408 | def failed 409 | Stat["failed:#{self}"] 410 | end 411 | 412 | # Tells Redis we've failed a job. 413 | def failed! 414 | Stat << "failed" 415 | Stat << "failed:#{self}" 416 | end 417 | 418 | # What time did this worker start? Returns an instance of `Time` 419 | def started 420 | redis.get "worker:#{self}:started" 421 | end 422 | 423 | # Tell Redis we've started 424 | def started! 425 | redis.set("worker:#{self}:started", Time.now.to_s) 426 | end 427 | 428 | # Returns a hash explaining the Job we're currently processing, if any. 429 | def job 430 | decode(redis.get("worker:#{self}")) || {} 431 | end 432 | alias_method :processing, :job 433 | 434 | # Boolean - true if working, false if not 435 | def working? 436 | state == :working 437 | end 438 | 439 | # Boolean - true if idle, false if not 440 | def idle? 441 | state == :idle 442 | end 443 | 444 | # Returns a symbol representing the current worker state, 445 | # which can be either :working or :idle 446 | def state 447 | redis.exists("worker:#{self}") ? :working : :idle 448 | end 449 | 450 | # Is this worker the same as another worker? 451 | def ==(other) 452 | to_s == other.to_s 453 | end 454 | 455 | def inspect 456 | "#" 457 | end 458 | 459 | # The string representation is the same as the id for this worker 460 | # instance. Can be used with `Worker.find`. 461 | def to_s 462 | @to_s ||= "#{hostname}:#{Process.pid}:#{@queues.join(',')}" 463 | end 464 | alias_method :id, :to_s 465 | 466 | # chomp'd hostname of this machine 467 | def hostname 468 | @hostname ||= `hostname`.chomp 469 | end 470 | 471 | # Returns Integer PID of running worker 472 | def pid 473 | @pid ||= to_s.split(":")[1].to_i 474 | end 475 | 476 | # Returns an array of string pids of all the other workers on this 477 | # machine. Useful when pruning dead workers on startup. 478 | def worker_pids 479 | `ps -A -o pid,command | grep [r]esque | grep -v "resque-web"`.split("\n").map do |line| 480 | line.split(' ')[0] 481 | end 482 | end 483 | 484 | # Given a string, sets the procline ($0) and logs. 485 | # Procline is always in the format of: 486 | # resque-VERSION: STRING 487 | def procline(string) 488 | $0 = "resque-#{Resque::Version}: #{string}" 489 | log! $0 490 | end 491 | 492 | # Log a message to STDOUT if we are verbose or very_verbose. 493 | def log(message) 494 | if verbose 495 | puts "*** #{message}" 496 | elsif very_verbose 497 | time = Time.now.strftime('%H:%M:%S %Y-%m-%d') 498 | puts "** [#{time}] #$$: #{message}" 499 | end 500 | end 501 | 502 | # Logs a very verbose message to STDOUT. 503 | def log!(message) 504 | log message if very_verbose 505 | end 506 | end 507 | end 508 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Resque 2 | ====== 3 | 4 | Resque (pronounced like "rescue") is a Redis-backed library for creating 5 | background jobs, placing those jobs on multiple queues, and processing 6 | them later. 7 | 8 | Background jobs can be any Ruby class or module that responds to 9 | `perform`. Your existing classes can easily be converted to background 10 | jobs or you can create new classes specifically to do work. Or, you 11 | can do both. 12 | 13 | Resque is heavily inspired by DelayedJob (which rocks) and comprises 14 | three parts: 15 | 16 | 1. A Ruby library for creating, querying, and processing jobs 17 | 2. A Rake task for starting a worker which processes jobs 18 | 3. A Sinatra app for monitoring queues, jobs, and workers. 19 | 20 | Resque workers can be distributed between multiple machines, 21 | support priorities, are resilient to memory bloat / "leaks," are 22 | optimized for REE (but work on MRI and JRuby), tell you what they're 23 | doing, and expect failure. 24 | 25 | Resque queues are persistent; support constant time, atomic push and 26 | pop (thanks to Redis); provide visibility into their contents; and 27 | store jobs as simple JSON packages. 28 | 29 | The Resque frontend tells you what workers are doing, what workers are 30 | not doing, what queues you're using, what's in those queues, provides 31 | general usage stats, and helps you track failures. 32 | 33 | 34 | The Blog Post 35 | ------------- 36 | 37 | For the backstory, philosophy, and history of Resque's beginnings, 38 | please see [the blog post][0]. 39 | 40 | 41 | Overview 42 | -------- 43 | 44 | Resque allows you to create jobs and place them on a queue, then, 45 | later, pull those jobs off the queue and process them. 46 | 47 | Resque jobs are Ruby classes (or modules) which respond to the 48 | `perform` method. Here's an example: 49 | 50 | 51 | ``` ruby 52 | class Archive 53 | @queue = :file_serve 54 | 55 | def self.perform(repo_id, branch = 'master') 56 | repo = Repository.find(repo_id) 57 | repo.create_archive(branch) 58 | end 59 | end 60 | ``` 61 | 62 | The `@queue` class instance variable determines which queue `Archive` 63 | jobs will be placed in. Queues are arbitrary and created on the fly - 64 | you can name them whatever you want and have as many as you want. 65 | 66 | To place an `Archive` job on the `file_serve` queue, we might add this 67 | to our application's pre-existing `Repository` class: 68 | 69 | ``` ruby 70 | class Repository 71 | def async_create_archive(branch) 72 | Resque.enqueue(Archive, self.id, branch) 73 | end 74 | end 75 | ``` 76 | 77 | Now when we call `repo.async_create_archive('masterbrew')` in our 78 | application, a job will be created and placed on the `file_serve` 79 | queue. 80 | 81 | Later, a worker will run something like this code to process the job: 82 | 83 | ``` ruby 84 | klass, args = Resque.reserve(:file_serve) 85 | klass.perform(*args) if klass.respond_to? :perform 86 | ``` 87 | 88 | Which translates to: 89 | 90 | ``` ruby 91 | Archive.perform(44, 'masterbrew') 92 | ``` 93 | 94 | Let's start a worker to run `file_serve` jobs: 95 | 96 | $ cd app_root 97 | $ QUEUE=file_serve rake resque:work 98 | 99 | This starts one Resque worker and tells it to work off the 100 | `file_serve` queue. As soon as it's ready it'll try to run the 101 | `Resque.reserve` code snippet above and process jobs until it can't 102 | find any more, at which point it will sleep for a small period and 103 | repeatedly poll the queue for more jobs. 104 | 105 | Workers can be given multiple queues (a "queue list") and run on 106 | multiple machines. In fact they can be run anywhere with network 107 | access to the Redis server. 108 | 109 | 110 | Jobs 111 | ---- 112 | 113 | What should you run in the background? Anything that takes any time at 114 | all. Slow INSERT statements, disk manipulating, data processing, etc. 115 | 116 | At GitHub we use Resque to process the following types of jobs: 117 | 118 | * Warming caches 119 | * Counting disk usage 120 | * Building tarballs 121 | * Building Rubygems 122 | * Firing off web hooks 123 | * Creating events in the db and pre-caching them 124 | * Building graphs 125 | * Deleting users 126 | * Updating our search index 127 | 128 | As of writing we have about 35 different types of background jobs. 129 | 130 | Keep in mind that you don't need a web app to use Resque - we just 131 | mention "foreground" and "background" because they make conceptual 132 | sense. You could easily be spidering sites and sticking data which 133 | needs to be crunched later into a queue. 134 | 135 | 136 | ### Persistence 137 | 138 | Jobs are persisted to queues as JSON objects. Let's take our `Archive` 139 | example from above. We'll run the following code to create a job: 140 | 141 | ``` ruby 142 | repo = Repository.find(44) 143 | repo.async_create_archive('masterbrew') 144 | ``` 145 | 146 | The following JSON will be stored in the `file_serve` queue: 147 | 148 | ``` javascript 149 | { 150 | 'class': 'Archive', 151 | 'args': [ 44, 'masterbrew' ] 152 | } 153 | ``` 154 | 155 | Because of this your jobs must only accept arguments that can be JSON encoded. 156 | 157 | So instead of doing this: 158 | 159 | ``` ruby 160 | Resque.enqueue(Archive, self, branch) 161 | ``` 162 | 163 | do this: 164 | 165 | ``` ruby 166 | Resque.enqueue(Archive, self.id, branch) 167 | ``` 168 | 169 | This is why our above example (and all the examples in `examples/`) 170 | uses object IDs instead of passing around the objects. 171 | 172 | While this is less convenient than just sticking a marshaled object 173 | in the database, it gives you a slight advantage: your jobs will be 174 | run against the most recent version of an object because they need to 175 | pull from the DB or cache. 176 | 177 | If your jobs were run against marshaled objects, they could 178 | potentially be operating on a stale record with out-of-date information. 179 | 180 | 181 | ### send_later / async 182 | 183 | Want something like DelayedJob's `send_later` or the ability to use 184 | instance methods instead of just methods for jobs? See the `examples/` 185 | directory for goodies. 186 | 187 | We plan to provide first class `async` support in a future release. 188 | 189 | 190 | ### Failure 191 | 192 | If a job raises an exception, it is logged and handed off to the 193 | `Resque::Failure` module. Failures are logged either locally in Redis 194 | or using some different backend. 195 | 196 | For example, Resque ships with Hoptoad support. 197 | 198 | Keep this in mind when writing your jobs: you may want to throw 199 | exceptions you would not normally throw in order to assist debugging. 200 | 201 | 202 | Workers 203 | ------- 204 | 205 | Resque workers are rake tasks that run forever. They basically do this: 206 | 207 | ``` ruby 208 | start 209 | loop do 210 | if job = reserve 211 | job.process 212 | else 213 | sleep 5 214 | end 215 | end 216 | shutdown 217 | ``` 218 | 219 | Starting a worker is simple. Here's our example from earlier: 220 | 221 | $ QUEUE=file_serve rake resque:work 222 | 223 | By default Resque won't know about your application's 224 | environment. That is, it won't be able to find and run your jobs - it 225 | needs to load your application into memory. 226 | 227 | If we've installed Resque as a Rails plugin, we might run this command 228 | from our RAILS_ROOT: 229 | 230 | $ QUEUE=file_serve rake environment resque:work 231 | 232 | This will load the environment before starting a worker. Alternately 233 | we can define a `resque:setup` task with a dependency on the 234 | `environment` rake task: 235 | 236 | ``` ruby 237 | task "resque:setup" => :environment 238 | ``` 239 | 240 | GitHub's setup task looks like this: 241 | 242 | ``` ruby 243 | task "resque:setup" => :environment do 244 | Grit::Git.git_timeout = 10.minutes 245 | end 246 | ``` 247 | 248 | We don't want the `git_timeout` as high as 10 minutes in our web app, 249 | but in the Resque workers it's fine. 250 | 251 | 252 | ### Logging 253 | 254 | Workers support basic logging to STDOUT. If you start them with the 255 | `VERBOSE` env variable set, they will print basic debugging 256 | information. You can also set the `VVERBOSE` (very verbose) env 257 | variable. 258 | 259 | $ VVERBOSE=1 QUEUE=file_serve rake environment resque:work 260 | 261 | ### Process IDs (PIDs) 262 | 263 | There are scenarios where it's helpful to record the PID of a resque 264 | worker process. Use the PIDFILE option for easy access to the PID: 265 | 266 | $ PIDFILE=./resque.pid QUEUE=file_serve rake environment resque:work 267 | 268 | 269 | ### Priorities and Queue Lists 270 | 271 | Resque doesn't support numeric priorities but instead uses the order 272 | of queues you give it. We call this list of queues the "queue list." 273 | 274 | Let's say we add a `warm_cache` queue in addition to our `file_serve` 275 | queue. We'd now start a worker like so: 276 | 277 | $ QUEUES=file_serve,warm_cache rake resque:work 278 | 279 | When the worker looks for new jobs, it will first check 280 | `file_serve`. If it finds a job, it'll process it then check 281 | `file_serve` again. It will keep checking `file_serve` until no more 282 | jobs are available. At that point, it will check `warm_cache`. If it 283 | finds a job it'll process it then check `file_serve` (repeating the 284 | whole process). 285 | 286 | In this way you can prioritize certain queues. At GitHub we start our 287 | workers with something like this: 288 | 289 | $ QUEUES=critical,archive,high,low rake resque:work 290 | 291 | Notice the `archive` queue - it is specialized and in our future 292 | architecture will only be run from a single machine. 293 | 294 | At that point we'll start workers on our generalized background 295 | machines with this command: 296 | 297 | $ QUEUES=critical,high,low rake resque:work 298 | 299 | And workers on our specialized archive machine with this command: 300 | 301 | $ QUEUE=archive rake resque:work 302 | 303 | 304 | ### Running All Queues 305 | 306 | If you want your workers to work off of every queue, including new 307 | queues created on the fly, you can use a splat: 308 | 309 | $ QUEUE=* rake resque:work 310 | 311 | Queues will be processed in alphabetical order. 312 | 313 | 314 | ### Running Multiple Workers 315 | 316 | At GitHub we use god to start and stop multiple workers. A sample god 317 | configuration file is included under `examples/god`. We recommend this 318 | method. 319 | 320 | If you'd like to run multiple workers in development mode, you can do 321 | so using the `resque:workers` rake task: 322 | 323 | $ COUNT=5 QUEUE=* rake resque:workers 324 | 325 | This will spawn five Resque workers, each in its own thread. Hitting 326 | ctrl-c should be sufficient to stop them all. 327 | 328 | 329 | ### Forking 330 | 331 | On certain platforms, when a Resque worker reserves a job it 332 | immediately forks a child process. The child processes the job then 333 | exits. When the child has exited successfully, the worker reserves 334 | another job and repeats the process. 335 | 336 | Why? 337 | 338 | Because Resque assumes chaos. 339 | 340 | Resque assumes your background workers will lock up, run too long, or 341 | have unwanted memory growth. 342 | 343 | If Resque workers processed jobs themselves, it'd be hard to whip them 344 | into shape. Let's say one is using too much memory: you send it a 345 | signal that says "shutdown after you finish processing the current 346 | job," and it does so. It then starts up again - loading your entire 347 | application environment. This adds useless CPU cycles and causes a 348 | delay in queue processing. 349 | 350 | Plus, what if it's using too much memory and has stopped responding to 351 | signals? 352 | 353 | Thanks to Resque's parent / child architecture, jobs that use too much memory 354 | release that memory upon completion. No unwanted growth. 355 | 356 | And what if a job is running too long? You'd need to `kill -9` it then 357 | start the worker again. With Resque's parent / child architecture you 358 | can tell the parent to forcefully kill the child then immediately 359 | start processing more jobs. No startup delay or wasted cycles. 360 | 361 | The parent / child architecture helps us keep tabs on what workers are 362 | doing, too. By eliminating the need to `kill -9` workers we can have 363 | parents remove themselves from the global listing of workers. If we 364 | just ruthlessly killed workers, we'd need a separate watchdog process 365 | to add and remove them to the global listing - which becomes 366 | complicated. 367 | 368 | Workers instead handle their own state. 369 | 370 | 371 | ### Parents and Children 372 | 373 | Here's a parent / child pair doing some work: 374 | 375 | $ ps -e -o pid,command | grep [r]esque 376 | 92099 resque: Forked 92102 at 1253142769 377 | 92102 resque: Processing file_serve since 1253142769 378 | 379 | You can clearly see that process 92099 forked 92102, which has been 380 | working since 1253142769. 381 | 382 | (By advertising the time they began processing you can easily use monit 383 | or god to kill stale workers.) 384 | 385 | When a parent process is idle, it lets you know what queues it is 386 | waiting for work on: 387 | 388 | $ ps -e -o pid,command | grep [r]esque 389 | 92099 resque: Waiting for file_serve,warm_cache 390 | 391 | 392 | ### Signals 393 | 394 | Resque workers respond to a few different signals: 395 | 396 | * `QUIT` - Wait for child to finish processing then exit 397 | * `TERM` / `INT` - Immediately kill child then exit 398 | * `USR1` - Immediately kill child but don't exit 399 | * `USR2` - Don't start to process any new jobs 400 | * `CONT` - Start to process new jobs again after a USR2 401 | 402 | If you want to gracefully shutdown a Resque worker, use `QUIT`. 403 | 404 | If you want to kill a stale or stuck child, use `USR1`. Processing 405 | will continue as normal unless the child was not found. In that case 406 | Resque assumes the parent process is in a bad state and shuts down. 407 | 408 | If you want to kill a stale or stuck child and shutdown, use `TERM` 409 | 410 | If you want to stop processing jobs, but want to leave the worker running 411 | (for example, to temporarily alleviate load), use `USR2` to stop processing, 412 | then `CONT` to start it again. 413 | 414 | ### Mysql::Error: MySQL server has gone away 415 | 416 | If your workers remain idle for too long they may lose their MySQL 417 | connection. If that happens we recommend using [this 418 | Gist](http://gist.github.com/238999). 419 | 420 | 421 | The Front End 422 | ------------- 423 | 424 | Resque comes with a Sinatra-based front end for seeing what's up with 425 | your queue. 426 | 427 | ![The Front End](https://img.skitch.com/20110528-pc67a8qsfapgjxf5gagxd92fcu.png) 428 | 429 | ### Standalone 430 | 431 | If you've installed Resque as a gem running the front end standalone is easy: 432 | 433 | $ resque-web 434 | 435 | It's a thin layer around `rackup` so it's configurable as well: 436 | 437 | $ resque-web -p 8282 438 | 439 | If you have a Resque config file you want evaluated just pass it to 440 | the script as the final argument: 441 | 442 | $ resque-web -p 8282 rails_root/config/initializers/resque.rb 443 | 444 | You can also set the namespace directly using `resque-web`: 445 | 446 | $ resque-web -p 8282 -N myapp 447 | 448 | ### Passenger 449 | 450 | Using Passenger? Resque ships with a `config.ru` you can use. See 451 | Phusion's guide: 452 | 453 | Apache: 454 | Nginx: 455 | 456 | ### Rack::URLMap 457 | 458 | If you want to load Resque on a subpath, possibly alongside other 459 | apps, it's easy to do with Rack's `URLMap`: 460 | 461 | ``` ruby 462 | require 'resque/server' 463 | 464 | run Rack::URLMap.new \ 465 | "/" => Your::App.new, 466 | "/resque" => Resque::Server.new 467 | ``` 468 | 469 | Check `examples/demo/config.ru` for a functional example (including 470 | HTTP basic auth). 471 | 472 | 473 | Resque vs DelayedJob 474 | -------------------- 475 | 476 | How does Resque compare to DelayedJob, and why would you choose one 477 | over the other? 478 | 479 | * Resque supports multiple queues 480 | * DelayedJob supports finer grained priorities 481 | * Resque workers are resilient to memory leaks / bloat 482 | * DelayedJob workers are extremely simple and easy to modify 483 | * Resque requires Redis 484 | * DelayedJob requires ActiveRecord 485 | * Resque can only place JSONable Ruby objects on a queue as arguments 486 | * DelayedJob can place _any_ Ruby object on its queue as arguments 487 | * Resque includes a Sinatra app for monitoring what's going on 488 | * DelayedJob can be queried from within your Rails app if you want to 489 | add an interface 490 | 491 | If you're doing Rails development, you already have a database and 492 | ActiveRecord. DelayedJob is super easy to setup and works great. 493 | GitHub used it for many months to process almost 200 million jobs. 494 | 495 | Choose Resque if: 496 | 497 | * You need multiple queues 498 | * You don't care / dislike numeric priorities 499 | * You don't need to persist every Ruby object ever 500 | * You have potentially huge queues 501 | * You want to see what's going on 502 | * You expect a lot of failure / chaos 503 | * You can setup Redis 504 | * You're not running short on RAM 505 | 506 | Choose DelayedJob if: 507 | 508 | * You like numeric priorities 509 | * You're not doing a gigantic amount of jobs each day 510 | * Your queue stays small and nimble 511 | * There is not a lot failure / chaos 512 | * You want to easily throw anything on the queue 513 | * You don't want to setup Redis 514 | 515 | In no way is Resque a "better" DelayedJob, so make sure you pick the 516 | tool that's best for your app. 517 | 518 | 519 | Installing Redis 520 | ---------------- 521 | 522 | Resque requires Redis 0.900 or higher. 523 | 524 | Resque uses Redis' lists for its queues. It also stores worker state 525 | data in Redis. 526 | 527 | #### Homebrew 528 | 529 | If you're on OS X, Homebrew is the simplest way to install Redis: 530 | 531 | $ brew install redis 532 | $ redis-server /usr/local/etc/redis.conf 533 | 534 | You now have a Redis daemon running on 6379. 535 | 536 | #### Via Resque 537 | 538 | Resque includes Rake tasks (thanks to Ezra's redis-rb) that will 539 | install and run Redis for you: 540 | 541 | $ git clone git://github.com/defunkt/resque.git 542 | $ cd resque 543 | $ rake redis:install dtach:install 544 | $ rake redis:start 545 | 546 | Or, if you don't have admin access on your machine: 547 | 548 | $ git clone git://github.com/defunkt/resque.git 549 | $ cd resque 550 | $ PREFIX= rake redis:install dtach:install 551 | $ rake redis:start 552 | 553 | You now have Redis running on 6379. Wait a second then hit ctrl-\ to 554 | detach and keep it running in the background. 555 | 556 | The demo is probably the best way to figure out how to put the parts 557 | together. But, it's not that hard. 558 | 559 | 560 | Resque Dependencies 561 | ------------------- 562 | 563 | gem install redis redis-namespace yajl-ruby vegas sinatra 564 | 565 | If you cannot install `yajl-ruby` (JRuby?), you can install the `json` 566 | gem and Resque will use it instead. 567 | 568 | When problems arise, make sure you have the newest versions of the 569 | `redis` and `redis-namespace` gems. 570 | 571 | 572 | Installing Resque 573 | ----------------- 574 | 575 | ### In a Rack app, as a gem 576 | 577 | First install the gem. 578 | 579 | $ gem install resque 580 | 581 | Next include it in your application. 582 | 583 | ``` ruby 584 | require 'resque' 585 | ``` 586 | 587 | Now start your application: 588 | 589 | rackup config.ru 590 | 591 | That's it! You can now create Resque jobs from within your app. 592 | 593 | To start a worker, create a Rakefile in your app's root (or add this 594 | to an existing Rakefile): 595 | 596 | ``` ruby 597 | require 'your/app' 598 | require 'resque/tasks' 599 | ``` 600 | 601 | Now: 602 | 603 | $ QUEUE=* rake resque:work 604 | 605 | Alternately you can define a `resque:setup` hook in your Rakefile if you 606 | don't want to load your app every time rake runs. 607 | 608 | 609 | ### In a Rails app, as a gem 610 | 611 | First install the gem. 612 | 613 | $ gem install resque 614 | 615 | Next include it in your application. 616 | 617 | $ cat config/initializers/load_resque.rb 618 | require 'resque' 619 | 620 | Now start your application: 621 | 622 | $ ./script/server 623 | 624 | That's it! You can now create Resque jobs from within your app. 625 | 626 | To start a worker, add this to your Rakefile in `RAILS_ROOT`: 627 | 628 | ``` ruby 629 | require 'resque/tasks' 630 | ``` 631 | 632 | Now: 633 | 634 | $ QUEUE=* rake environment resque:work 635 | 636 | Don't forget you can define a `resque:setup` hook in 637 | `lib/tasks/whatever.rake` that loads the `environment` task every time. 638 | 639 | 640 | ### In a Rails app, as a plugin 641 | 642 | $ ./script/plugin install git://github.com/defunkt/resque 643 | 644 | That's it! Resque will automatically be available when your Rails app 645 | loads. 646 | 647 | To start a worker: 648 | 649 | $ QUEUE=* rake environment resque:work 650 | 651 | Don't forget you can define a `resque:setup` hook in 652 | `lib/tasks/whatever.rake` that loads the `environment` task every time. 653 | 654 | 655 | Configuration 656 | ------------- 657 | 658 | You may want to change the Redis host and port Resque connects to, or 659 | set various other options at startup. 660 | 661 | Resque has a `redis` setter which can be given a string or a Redis 662 | object. This means if you're already using Redis in your app, Resque 663 | can re-use the existing connection. 664 | 665 | String: `Resque.redis = 'localhost:6379'` 666 | 667 | Redis: `Resque.redis = $redis` 668 | 669 | For our rails app we have a `config/initializers/resque.rb` file where 670 | we load `config/resque.yml` by hand and set the Redis information 671 | appropriately. 672 | 673 | Here's our `config/resque.yml`: 674 | 675 | development: localhost:6379 676 | test: localhost:6379 677 | staging: redis1.se.github.com:6379 678 | fi: localhost:6379 679 | production: redis1.ae.github.com:6379 680 | 681 | And our initializer: 682 | 683 | ``` ruby 684 | rails_root = ENV['RAILS_ROOT'] || File.dirname(__FILE__) + '/../..' 685 | rails_env = ENV['RAILS_ENV'] || 'development' 686 | 687 | resque_config = YAML.load_file(rails_root + '/config/resque.yml') 688 | Resque.redis = resque_config[rails_env] 689 | ``` 690 | 691 | Easy peasy! Why not just use `RAILS_ROOT` and `RAILS_ENV`? Because 692 | this way we can tell our Sinatra app about the config file: 693 | 694 | $ RAILS_ENV=production resque-web rails_root/config/initializers/resque.rb 695 | 696 | Now everyone is on the same page. 697 | 698 | Also, you could disable jobs queueing by setting 'inline' attribute. 699 | For example, if you want to run all jobs in the same process for cucumber, try: 700 | 701 | ``` ruby 702 | Resque.inline = ENV['RAILS_ENV'] == "cucumber" 703 | ``` 704 | 705 | 706 | Plugins and Hooks 707 | ----------------- 708 | 709 | For a list of available plugins see 710 | . 711 | 712 | If you'd like to write your own plugin, or want to customize Resque 713 | using hooks (such as `Resque.after_fork`), see 714 | [docs/HOOKS.md](http://github.com/defunkt/resque/blob/master/docs/HOOKS.md). 715 | 716 | 717 | Namespaces 718 | ---------- 719 | 720 | If you're running multiple, separate instances of Resque you may want 721 | to namespace the keyspaces so they do not overlap. This is not unlike 722 | the approach taken by many memcached clients. 723 | 724 | This feature is provided by the [redis-namespace][rs] library, which 725 | Resque uses by default to separate the keys it manages from other keys 726 | in your Redis server. 727 | 728 | Simply use the `Resque.redis.namespace` accessor: 729 | 730 | ``` ruby 731 | Resque.redis.namespace = "resque:GitHub" 732 | ``` 733 | 734 | We recommend sticking this in your initializer somewhere after Redis 735 | is configured. 736 | 737 | 738 | Demo 739 | ---- 740 | 741 | Resque ships with a demo Sinatra app for creating jobs that are later 742 | processed in the background. 743 | 744 | Try it out by looking at the README, found at `examples/demo/README.markdown`. 745 | 746 | 747 | Monitoring 748 | ---------- 749 | 750 | ### god 751 | 752 | If you're using god to monitor Resque, we have provided example 753 | configs in `examples/god/`. One is for starting / stopping workers, 754 | the other is for killing workers that have been running too long. 755 | 756 | ### monit 757 | 758 | If you're using monit, `examples/monit/resque.monit` is provided free 759 | of charge. This is **not** used by GitHub in production, so please 760 | send patches for any tweaks or improvements you can make to it. 761 | 762 | 763 | Questions 764 | --------- 765 | 766 | Please add them to the [FAQ](https://github.com/defunkt/resque/wiki/FAQ) or 767 | ask on the Mailing List. The Mailing List is explained further below 768 | 769 | 770 | Development 771 | ----------- 772 | 773 | Want to hack on Resque? 774 | 775 | First clone the repo and run the tests: 776 | 777 | git clone git://github.com/defunkt/resque.git 778 | cd resque 779 | rake test 780 | 781 | If the tests do not pass make sure you have Redis installed 782 | correctly (though we make an effort to tell you if we feel this is the 783 | case). The tests attempt to start an isolated instance of Redis to 784 | run against. 785 | 786 | Also make sure you've installed all the dependencies correctly. For 787 | example, try loading the `redis-namespace` gem after you've installed 788 | it: 789 | 790 | $ irb 791 | >> require 'rubygems' 792 | => true 793 | >> require 'redis/namespace' 794 | => true 795 | 796 | If you get an error requiring any of the dependencies, you may have 797 | failed to install them or be seeing load path issues. 798 | 799 | Feel free to ping the mailing list with your problem and we'll try to 800 | sort it out. 801 | 802 | 803 | Contributing 804 | ------------ 805 | 806 | Once you've made your great commits: 807 | 808 | 1. [Fork][1] Resque 809 | 2. Create a topic branch - `git checkout -b my_branch` 810 | 3. Push to your branch - `git push origin my_branch` 811 | 4. Create a [Pull Request](http://help.github.com/pull-requests/) from your branch 812 | 5. That's it! 813 | 814 | You might want to checkout our [Contributing][cb] wiki page for information 815 | on coding standards, new features, etc. 816 | 817 | 818 | Mailing List 819 | ------------ 820 | 821 | To join the list simply send an email to . This 822 | will subscribe you and send you information about your subscription, 823 | including unsubscribe information. 824 | 825 | The archive can be found at . 826 | 827 | 828 | Meta 829 | ---- 830 | 831 | * Code: `git clone git://github.com/defunkt/resque.git` 832 | * Home: 833 | * Docs: 834 | * Bugs: 835 | * List: 836 | * Chat: 837 | * Gems: 838 | 839 | This project uses [Semantic Versioning][sv]. 840 | 841 | 842 | Author 843 | ------ 844 | 845 | Chris Wanstrath :: chris@ozmm.org :: @defunkt 846 | 847 | [0]: http://github.com/blog/542-introducing-resque 848 | [1]: http://help.github.com/forking/ 849 | [2]: http://github.com/defunkt/resque/issues 850 | [sv]: http://semver.org/ 851 | [rs]: http://github.com/defunkt/redis-namespace 852 | [cb]: http://wiki.github.com/defunkt/resque/contributing 853 | --------------------------------------------------------------------------------