scotttam-resque 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/.kick +26 -0
- data/CONTRIBUTORS +23 -0
- data/HISTORY.md +80 -0
- data/LICENSE +20 -0
- data/README.markdown +767 -0
- data/Rakefile +66 -0
- data/bin/resque +57 -0
- data/bin/resque-web +18 -0
- data/config.ru +14 -0
- data/deps.rip +6 -0
- data/examples/async_helper.rb +31 -0
- data/examples/demo/README.markdown +71 -0
- data/examples/demo/Rakefile +3 -0
- data/examples/demo/app.rb +38 -0
- data/examples/demo/config.ru +19 -0
- data/examples/demo/job.rb +22 -0
- data/examples/god/resque.god +53 -0
- data/examples/god/stale.god +26 -0
- data/examples/instance.rb +11 -0
- data/examples/simple.rb +30 -0
- data/init.rb +1 -0
- data/lib/resque/errors.rb +7 -0
- data/lib/resque/failure/base.rb +58 -0
- data/lib/resque/failure/hoptoad.rb +121 -0
- data/lib/resque/failure/multiple.rb +44 -0
- data/lib/resque/failure/redis.rb +33 -0
- data/lib/resque/failure.rb +63 -0
- data/lib/resque/helpers.rb +57 -0
- data/lib/resque/job.rb +146 -0
- data/lib/resque/server/public/idle.png +0 -0
- data/lib/resque/server/public/jquery-1.3.2.min.js +19 -0
- data/lib/resque/server/public/jquery.relatize_date.js +95 -0
- data/lib/resque/server/public/poll.png +0 -0
- data/lib/resque/server/public/ranger.js +24 -0
- data/lib/resque/server/public/reset.css +48 -0
- data/lib/resque/server/public/style.css +76 -0
- data/lib/resque/server/public/working.png +0 -0
- data/lib/resque/server/views/error.erb +1 -0
- data/lib/resque/server/views/failed.erb +35 -0
- data/lib/resque/server/views/key.erb +17 -0
- data/lib/resque/server/views/layout.erb +38 -0
- data/lib/resque/server/views/next_more.erb +10 -0
- data/lib/resque/server/views/overview.erb +4 -0
- data/lib/resque/server/views/queues.erb +46 -0
- data/lib/resque/server/views/stats.erb +62 -0
- data/lib/resque/server/views/workers.erb +78 -0
- data/lib/resque/server/views/working.erb +69 -0
- data/lib/resque/server.rb +187 -0
- data/lib/resque/stat.rb +53 -0
- data/lib/resque/tasks.rb +39 -0
- data/lib/resque/version.rb +3 -0
- data/lib/resque/worker.rb +453 -0
- data/lib/resque.rb +246 -0
- data/tasks/redis.rake +135 -0
- data/tasks/resque.rake +2 -0
- data/test/redis-test.conf +132 -0
- data/test/resque_test.rb +220 -0
- data/test/test_helper.rb +96 -0
- data/test/worker_test.rb +260 -0
- metadata +172 -0
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
load 'tasks/redis.rake'
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift 'lib'
|
4
|
+
require 'resque/tasks'
|
5
|
+
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc "Run tests"
|
9
|
+
task :test do
|
10
|
+
# Don't use the rake/testtask because it loads a new
|
11
|
+
# Ruby interpreter - we want to run tests with the current
|
12
|
+
# `rake` so our library manager still works
|
13
|
+
Dir['test/*_test.rb'].each do |f|
|
14
|
+
require f
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Activate kicker - gem install kicker"
|
19
|
+
task :kick do
|
20
|
+
exec "kicker -e rake lib test"
|
21
|
+
end
|
22
|
+
|
23
|
+
task :install => [ 'redis:install', 'dtach:install' ]
|
24
|
+
|
25
|
+
desc "Build a gem"
|
26
|
+
task :gem => [ :test, :gemspec, :build ]
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'jeweler'
|
30
|
+
require 'resque/version'
|
31
|
+
|
32
|
+
Jeweler::Tasks.new do |gemspec|
|
33
|
+
gemspec.name = "scotttam-resque"
|
34
|
+
gemspec.summary = %Q{scotttam-resque is an extension to the resque queue system that has pre-fork hooks and ability to turn off forking.}
|
35
|
+
gemspec.description = %Q{scotttam-resque is an extension to the resque queue system that has pre-fork hooks and ability to turn off forking. Add more description here.}
|
36
|
+
gemspec.email = "tamosunas@gmail.com"
|
37
|
+
gemspec.homepage = "http://github.com/scotttam/resque"
|
38
|
+
gemspec.authors = ["Chris Wanstrath, Scott Tamosunas"]
|
39
|
+
gemspec.version = Resque::Version
|
40
|
+
|
41
|
+
gemspec.add_dependency "redis"
|
42
|
+
gemspec.add_dependency "redis-namespace"
|
43
|
+
gemspec.add_dependency "vegas", ">=0.1.2"
|
44
|
+
gemspec.add_dependency "sinatra", ">=0.9.2"
|
45
|
+
gemspec.add_development_dependency "jeweler"
|
46
|
+
end
|
47
|
+
rescue LoadError
|
48
|
+
puts "Jeweler not available. Install it with: "
|
49
|
+
puts "gem install jeweler"
|
50
|
+
end
|
51
|
+
|
52
|
+
begin
|
53
|
+
require 'sdoc_helpers'
|
54
|
+
rescue LoadError
|
55
|
+
puts "sdoc support not enabled. Please gem install sdoc-helpers."
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Push a new version to Gemcutter"
|
59
|
+
task :publish => [ :test, :gemspec, :build ] do
|
60
|
+
system "git tag v#{Resque::Version}"
|
61
|
+
system "git push origin v#{Resque::Version}"
|
62
|
+
system "git push origin master"
|
63
|
+
system "gem push pkg/scotttam-resque-#{Resque::Version}.gem"
|
64
|
+
system "git clean -fd"
|
65
|
+
exec "rake pages"
|
66
|
+
end
|
data/bin/resque
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'resque'
|
5
|
+
|
6
|
+
def kill(worker)
|
7
|
+
abort "** resque kill WORKER_ID" if worker.nil?
|
8
|
+
pid = worker.split(':')[1].to_i
|
9
|
+
|
10
|
+
begin
|
11
|
+
Process.kill("KILL", pid)
|
12
|
+
puts "** killed #{worker}"
|
13
|
+
rescue Errno::ESRCH
|
14
|
+
puts "** worker #{worker} not running"
|
15
|
+
end
|
16
|
+
|
17
|
+
remove worker
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove(worker)
|
21
|
+
abort "** resque remove WORKER_ID" if worker.nil?
|
22
|
+
|
23
|
+
Resque.remove_worker(worker)
|
24
|
+
puts "** removed #{worker}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def list
|
28
|
+
if Resque.workers.any?
|
29
|
+
Resque.workers.each do |worker|
|
30
|
+
puts "#{worker} (#{worker.state})"
|
31
|
+
end
|
32
|
+
else
|
33
|
+
puts "None"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if (i = ARGV.index('-r')) && ARGV[i+1]
|
38
|
+
Resque.redis = ARGV[i+1]
|
39
|
+
ARGV.delete_at(i)
|
40
|
+
ARGV.delete_at(i+1)
|
41
|
+
end
|
42
|
+
|
43
|
+
case ARGV[0]
|
44
|
+
when 'kill'
|
45
|
+
kill ARGV[1]
|
46
|
+
when 'remove'
|
47
|
+
remove ARGV[1]
|
48
|
+
when 'list'
|
49
|
+
list
|
50
|
+
else
|
51
|
+
puts "Usage: resque [-r redis_host:redis_port] COMMAND [option]"
|
52
|
+
puts
|
53
|
+
puts "Commands:"
|
54
|
+
puts " remove WORKER Removes a worker"
|
55
|
+
puts " kill WORKER Kills a worker"
|
56
|
+
puts " list Lists known workers"
|
57
|
+
end
|
data/bin/resque-web
ADDED
@@ -0,0 +1,18 @@
|
|
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
|
+
})
|
data/config.ru
ADDED
@@ -0,0 +1,14 @@
|
|
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
|
data/deps.rip
ADDED
@@ -0,0 +1,31 @@
|
|
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)
|
@@ -0,0 +1,71 @@
|
|
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.
|
@@ -0,0 +1,38 @@
|
|
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 = "<html><head><title>Resque Demo</title></head><body>"
|
10
|
+
out << "<p>"
|
11
|
+
out << "There are #{info[:pending]} pending and "
|
12
|
+
out << "#{info[:processed]} processed jobs across #{info[:queues]} queues."
|
13
|
+
out << "</p>"
|
14
|
+
out << '<form method="POST">'
|
15
|
+
out << '<input type="submit" value="Create New Job"/>'
|
16
|
+
out << ' <a href="/resque/">View Resque</a>'
|
17
|
+
out << '</form>'
|
18
|
+
|
19
|
+
out << "<form action='/failing' method='POST''>"
|
20
|
+
out << '<input type="submit" value="Create Failing New Job"/>'
|
21
|
+
out << ' <a href="/resque/">View Resque</a>'
|
22
|
+
out << '</form>'
|
23
|
+
|
24
|
+
out << "</body></html>"
|
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
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,22 @@
|
|
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
|
@@ -0,0 +1,53 @@
|
|
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.name = "resque-#{num}"
|
8
|
+
w.group = 'resque'
|
9
|
+
w.interval = 30.seconds
|
10
|
+
w.env = {"QUEUE"=>"critical,high,low", "RAILS_ENV"=>rails_env}
|
11
|
+
w.start = "/usr/bin/rake -f #{rails_root}/Rakefile environment resque:work"
|
12
|
+
|
13
|
+
w.uid = 'git'
|
14
|
+
w.gid = 'git'
|
15
|
+
|
16
|
+
# retart if memory gets too high
|
17
|
+
w.transition(:up, :restart) do |on|
|
18
|
+
on.condition(:memory_usage) do |c|
|
19
|
+
c.above = 350.megabytes
|
20
|
+
c.times = 2
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# determine the state on startup
|
25
|
+
w.transition(:init, { true => :up, false => :start }) do |on|
|
26
|
+
on.condition(:process_running) do |c|
|
27
|
+
c.running = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# determine when process has finished starting
|
32
|
+
w.transition([:start, :restart], :up) do |on|
|
33
|
+
on.condition(:process_running) do |c|
|
34
|
+
c.running = true
|
35
|
+
c.interval = 5.seconds
|
36
|
+
end
|
37
|
+
|
38
|
+
# failsafe
|
39
|
+
on.condition(:tries) do |c|
|
40
|
+
c.times = 5
|
41
|
+
c.transition = :start
|
42
|
+
c.interval = 5.seconds
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# start if process is not running
|
47
|
+
w.transition(:up, :start) do |on|
|
48
|
+
on.condition(:process_running) do |c|
|
49
|
+
c.running = false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,26 @@
|
|
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
|
data/examples/simple.rb
ADDED
@@ -0,0 +1,30 @@
|
|
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')
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'resque'
|
@@ -0,0 +1,58 @@
|
|
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
|
+
# Logging!
|
53
|
+
def log(message)
|
54
|
+
@worker.log(message)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'builder'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Resque
|
6
|
+
module Failure
|
7
|
+
# A Failure backend that sends exceptions raised by jobs to Hoptoad.
|
8
|
+
#
|
9
|
+
# To use it, put this code in an initializer, Rake task, or wherever:
|
10
|
+
#
|
11
|
+
# Resque::Failure::Hoptoad.configure do |config|
|
12
|
+
# config.api_key = 'blah'
|
13
|
+
# config.secure = true
|
14
|
+
#
|
15
|
+
# # optional proxy support
|
16
|
+
# config.proxy_host = 'x.y.z.t'
|
17
|
+
# config.proxy_port = 8080
|
18
|
+
# end
|
19
|
+
class Hoptoad < Base
|
20
|
+
# From the hoptoad plugin
|
21
|
+
INPUT_FORMAT = /^([^:]+):(\d+)(?::in `([^']+)')?$/
|
22
|
+
|
23
|
+
class << self
|
24
|
+
attr_accessor :secure, :api_key, :proxy_host, :proxy_port
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.count
|
28
|
+
# We can't get the total # of errors from Hoptoad so we fake it
|
29
|
+
# by asking Resque how many errors it has seen.
|
30
|
+
Stat[:failed]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.configure
|
34
|
+
yield self
|
35
|
+
Resque::Failure.backend = self
|
36
|
+
end
|
37
|
+
|
38
|
+
def save
|
39
|
+
http = use_ssl? ? :https : :http
|
40
|
+
url = URI.parse("#{http}://hoptoadapp.com/notifier_api/v2/notices")
|
41
|
+
|
42
|
+
request = Net::HTTP::Proxy(self.class.proxy_host, self.class.proxy_port)
|
43
|
+
http = request.new(url.host, url.port)
|
44
|
+
headers = {
|
45
|
+
'Content-type' => 'text/xml',
|
46
|
+
'Accept' => 'text/xml, application/xml'
|
47
|
+
}
|
48
|
+
|
49
|
+
http.read_timeout = 5 # seconds
|
50
|
+
http.open_timeout = 2 # seconds
|
51
|
+
|
52
|
+
http.use_ssl = use_ssl?
|
53
|
+
|
54
|
+
begin
|
55
|
+
response = http.post(url.path, xml, headers)
|
56
|
+
rescue TimeoutError => e
|
57
|
+
log "Timeout while contacting the Hoptoad server."
|
58
|
+
end
|
59
|
+
|
60
|
+
case response
|
61
|
+
when Net::HTTPSuccess then
|
62
|
+
log "Hoptoad Success: #{response.class}"
|
63
|
+
else
|
64
|
+
body = response.body if response.respond_to? :body
|
65
|
+
log "Hoptoad Failure: #{response.class}\n#{body}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def xml
|
70
|
+
x = Builder::XmlMarkup.new
|
71
|
+
x.instruct!
|
72
|
+
x.notice :version=>"2.0" do
|
73
|
+
x.tag! "api-key", api_key
|
74
|
+
x.notifier do
|
75
|
+
x.name "Resqueue"
|
76
|
+
x.version "0.1"
|
77
|
+
x.url "http://github.com/defunkt/resque"
|
78
|
+
end
|
79
|
+
x.error do
|
80
|
+
x.class exception.class.name
|
81
|
+
x.message "#{exception.class.name}: #{exception.message}"
|
82
|
+
x.backtrace do
|
83
|
+
fill_in_backtrace_lines(x)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
x.request do
|
87
|
+
x.url queue.to_s
|
88
|
+
x.component worker.to_s
|
89
|
+
x.params do
|
90
|
+
x.var :key=>"payload_class" do
|
91
|
+
x.text! payload["class"].to_s
|
92
|
+
end
|
93
|
+
x.var :key=>"payload_args" do
|
94
|
+
x.text! payload["args"].to_s
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
x.tag!("server-environment") do
|
99
|
+
x.tag!("environment-name",RAILS_ENV)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def fill_in_backtrace_lines(x)
|
106
|
+
exception.backtrace.each do |unparsed_line|
|
107
|
+
_, file, number, method = unparsed_line.match(INPUT_FORMAT).to_a
|
108
|
+
x.line :file=>file,:number=>number
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def use_ssl?
|
113
|
+
self.class.secure
|
114
|
+
end
|
115
|
+
|
116
|
+
def api_key
|
117
|
+
self.class.api_key
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|