├── .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 |
7 | <%= redis_get_value_as_array(key) %>
8 |
9 |
10 |
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 |
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 |
12 | <%= row %>
13 |
14 |
15 | <% end %>
16 |
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 << ''
18 |
19 | 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 |
25 |
26 | <% if @subtabs %>
27 |
28 | <% for subtab in @subtabs %>
29 | ><%= subtab %>
30 | <% end %>
31 |
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 |
14 | <%= key %>
15 |
16 |
17 | <%= value %>
18 |
19 |
20 | <% end %>
21 |
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 |
30 | <%= key %>
31 |
32 |
33 | <%= value %>
34 |
35 |
36 | <% end %>
37 |
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 | key
46 | type
47 | size
48 |
49 | <% for key in resque.keys.sort %>
50 |
51 |
52 | "><%= key %>
53 |
54 | <%= resque.redis.type key %>
55 | <%= redis_get_size key %>
56 |
57 | <% end %>
58 |
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 |
9 | Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <%=size = resque.size(queue)%> jobs
10 |
11 |
12 | Class
13 | Args
14 |
15 | <% for job in (jobs = resque.peek(queue, start, 20)) %>
16 |
17 | <%= job['class'] %>
18 | <%=h job['args'].inspect %>
19 |
20 | <% end %>
21 | <% if jobs.empty? %>
22 |
23 | There are no pending jobs in this queue
24 |
25 | <% end %>
26 |
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 | Name
35 | Jobs
36 |
37 | <% for queue in resque.queues.sort_by { |q| q.to_s } %>
38 |
39 | "><%= queue %>
40 | <%= resque.size queue %>
41 |
42 | <% end %>
43 | ">
44 | failed
45 | <%= Resque::Failure.count %>
46 |
47 |
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 |
11 | <%end%>
12 |
13 | Showing <%=start%> to <%= start + 20 %> of <%= size = Resque::Failure.count %> jobs
14 |
15 |
16 | <%for job in failed%>
17 | <% index += 1 %>
18 |
19 |
20 | <% if job.nil? %>
21 | Error
22 | Job <%= index%> could not be parsed; perhaps it contains invalid JSON?
23 | <% else %>
24 | Worker
25 |
26 | <%= job['worker'].split(':')[0...2].join(':') %> on <%= job['queue'] %> at <%= Time.parse(job['failed_at']).strftime(date_format) %>
27 | <% if job['retried_at'] %>
28 |
32 | <% else %>
33 |
38 | <% end %>
39 |
40 | Class
41 | <%= job['payload'] ? job['payload']['class'] : 'nil' %>
42 | Arguments
43 | <%=h job['payload'] ? show_args(job['payload']['args']) : 'nil' %>
44 | Exception
45 | <%= job['exception'] %>
46 | Error
47 |
48 | <% if job['backtrace'] %>
49 | <%= h(job['error']) %>
50 | <%=h job['backtrace'].join("\n") %>
51 | <% else %>
52 | <%=h job['error'] %>
53 | <% end %>
54 |
55 | <% end %>
56 |
57 |
58 |
59 |
60 | <%end%>
61 |
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 | Where
8 | Queue
9 | Started
10 | Class
11 | Args
12 |
13 |
14 |
15 | <% host, pid, _ = worker.to_s.split(':') %>
16 | "><%= host %>:<%= pid %>
17 | <% data = worker.job %>
18 | <% queue = data['queue'] %>
19 | "><%= queue %>
20 | <%= data['run_at'] %>
21 |
22 | <%= data['payload']['class'] %>
23 |
24 | <%=h data['payload']['args'].inspect %>
25 |
26 |
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 | Where
43 | Queue
44 | Processing
45 |
46 | <% if worker_jobs.empty? %>
47 |
48 | Nothing is happening right now...
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 | "><%= host %>:<%= pid %>
57 |
58 | "><%= job['queue'] %>
59 |
60 |
61 | <% if job['queue'] %>
62 | <%= job['payload']['class'] %>
63 | "><%= job['run_at'] %>
64 | <% else %>
65 | Waiting for a job...
66 | <% end %>
67 |
68 |
69 | <% end %>
70 |
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 | Host
10 | Pid
11 | Started
12 | Queues
13 | Processed
14 | Failed
15 | Processing
16 |
17 |
18 |
19 |
20 | <% host, pid, queues = worker.to_s.split(':') %>
21 | <%= host %>
22 | <%= pid %>
23 | <%= worker.started %>
24 | <%= queues.split(',').map { |q| '' + q + ' '}.join('') %>
25 | <%= worker.processed %>
26 | <%= worker.failed %>
27 |
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 |
36 |
37 |
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 | Where
57 | Queues
58 | Processing
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 | "><%= host %>:<%= pid %>
66 | <%= queues.split(',').map { |q| '' + q + ' '}.join('') %>
67 |
68 |
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 |
77 |
78 | <% end %>
79 | <% if workers.empty? %>
80 |
81 | There are no registered workers
82 |
83 | <% end %>
84 |
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 | Hostname
94 | Workers
95 |
96 | <% for hostname, workers in worker_hosts.sort_by { |h,w| h } %>
97 |
98 | "><%= hostname %>
99 | <%= workers.size %>
100 |
101 | <% end %>
102 |
103 | ">all workers
104 | <%= Resque.workers.size %>
105 |
106 |
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 | 
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 |
--------------------------------------------------------------------------------