resque-mongo 1.4.0 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/CONTRIBUTORS +24 -6
  2. data/HISTORY.md +65 -0
  3. data/README.markdown +34 -5
  4. data/Rakefile +1 -1
  5. data/bin/resque +2 -2
  6. data/bin/resque-web +6 -1
  7. data/deps.rip +2 -2
  8. data/docs/HOOKS.md +121 -0
  9. data/docs/PLUGINS.md +93 -0
  10. data/examples/demo/Rakefile +5 -0
  11. data/examples/monit/resque.monit +6 -0
  12. data/lib/resque.rb +94 -7
  13. data/lib/resque/errors.rb +3 -0
  14. data/lib/resque/failure.rb +3 -0
  15. data/lib/resque/failure/base.rb +3 -0
  16. data/lib/resque/failure/hoptoad.rb +29 -19
  17. data/lib/resque/failure/mongo.rb +10 -1
  18. data/lib/resque/helpers.rb +8 -2
  19. data/lib/resque/job.rb +107 -2
  20. data/lib/resque/plugin.rb +46 -0
  21. data/lib/resque/server.rb +30 -11
  22. data/lib/resque/server/public/ranger.js +50 -7
  23. data/lib/resque/server/public/style.css +8 -1
  24. data/lib/resque/server/test_helper.rb +19 -0
  25. data/lib/resque/server/views/failed.erb +17 -3
  26. data/lib/resque/server/views/key_sets.erb +20 -0
  27. data/lib/resque/server/views/{key.erb → key_string.erb} +2 -8
  28. data/lib/resque/server/views/queues.erb +5 -2
  29. data/lib/resque/server/views/stats.erb +2 -2
  30. data/lib/resque/server/views/workers.erb +1 -1
  31. data/lib/resque/server/views/working.erb +2 -0
  32. data/lib/resque/tasks.rb +1 -1
  33. data/lib/resque/version.rb +1 -1
  34. data/lib/resque/worker.rb +54 -15
  35. data/tasks/redis.rake +53 -29
  36. data/test/job_hooks_test.rb +302 -0
  37. data/test/job_plugins_test.rb +209 -0
  38. data/test/plugin_test.rb +116 -0
  39. data/test/resque-mongo_benchmark.rb +62 -0
  40. data/test/resque-web_test.rb +54 -0
  41. data/test/resque_test.rb +34 -0
  42. data/test/test_helper.rb +15 -0
  43. data/test/worker_test.rb +62 -2
  44. metadata +58 -23
@@ -1,3 +1,8 @@
1
1
  $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib'
2
2
  require 'resque/tasks'
3
3
  require 'job'
4
+
5
+ desc "Start the demo using `rackup`"
6
+ task :start do
7
+ exec "rackup config.ru"
8
+ end
@@ -0,0 +1,6 @@
1
+ check process resque_worker_QUEUE
2
+ with pidfile /data/APP_NAME/current/tmp/pids/resque_worker_QUEUE.pid
3
+ start program = "/bin/sh -c 'cd /data/APP_NAME/current; RAILS_ENV=production QUEUE=queue_name VERBOSE=1 nohup rake resque:work& &> log/resque_worker_QUEUE.log && echo $! > tmp/pids/resque_worker_QUEUE.pid'" 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
@@ -6,6 +6,8 @@ rescue LoadError
6
6
  require 'json'
7
7
  end
8
8
 
9
+ require 'resque/version'
10
+
9
11
  require 'resque/errors'
10
12
 
11
13
  require 'resque/failure'
@@ -15,6 +17,7 @@ require 'resque/helpers'
15
17
  require 'resque/stat'
16
18
  require 'resque/job'
17
19
  require 'resque/worker'
20
+ require 'resque/plugin'
18
21
 
19
22
  module Resque
20
23
  include Helpers
@@ -65,6 +68,53 @@ module Resque
65
68
  @stats
66
69
  end
67
70
 
71
+ # The `before_first_fork` hook will be run in the **parent** process
72
+ # only once, before forking to run the first job. Be careful- any
73
+ # changes you make will be permanent for the lifespan of the
74
+ # worker.
75
+ #
76
+ # Call with a block to set the hook.
77
+ # Call with no arguments to return the hook.
78
+ def before_first_fork(&block)
79
+ block ? (@before_first_fork = block) : @before_first_fork
80
+ end
81
+
82
+ # Set a proc that will be called in the parent process before the
83
+ # worker forks for the first time.
84
+ def before_first_fork=(before_first_fork)
85
+ @before_first_fork = before_first_fork
86
+ end
87
+
88
+ # The `before_fork` hook will be run in the **parent** process
89
+ # before every job, so be careful- any changes you make will be
90
+ # permanent for the lifespan of the worker.
91
+ #
92
+ # Call with a block to set the hook.
93
+ # Call with no arguments to return the hook.
94
+ def before_fork(&block)
95
+ block ? (@before_fork = block) : @before_fork
96
+ end
97
+
98
+ # Set the before_fork proc.
99
+ def before_fork=(before_fork)
100
+ @before_fork = before_fork
101
+ end
102
+
103
+ # The `after_fork` hook will be run in the child process and is passed
104
+ # the current job. Any changes you make, therefor, will only live as
105
+ # long as the job currently being processes.
106
+ #
107
+ # Call with a block to set the hook.
108
+ # Call with no arguments to return the hook.
109
+ def after_fork(&block)
110
+ block ? (@after_fork = block) : @after_fork
111
+ end
112
+
113
+ # Set the after_fork proc.
114
+ def after_fork=(after_fork)
115
+ @after_fork = after_fork
116
+ end
117
+
68
118
  def to_s
69
119
  "Mongo Client connected to #{@con.host}"
70
120
  end
@@ -98,9 +148,9 @@ module Resque
98
148
  #
99
149
  # Returns a Ruby object.
100
150
  def pop(queue)
101
- doc = mongo.find_modify( :query => { :queue => queue },
102
- :sort => [:natural, :desc],
103
- :remove => true )
151
+ doc = mongo.find_and_modify( :query => { :queue => queue },
152
+ :sort => [:natural, :desc],
153
+ :remove => true )
104
154
  decode doc['item']
105
155
  rescue Mongo::OperationFailure => e
106
156
  return nil if e.message =~ /No matching object/
@@ -122,6 +172,7 @@ module Resque
122
172
  # To get the 3rd page of a 30 item, paginatied list one would use:
123
173
  # Resque.peek('my_list', 59, 30)
124
174
  def peek(queue, start = 0, count = 1)
175
+ start, count = [start, count].map { |n| Integer(n) }
125
176
  res = mongo.find(:queue => queue).sort([:natural, :desc]).skip(start).limit(count).to_a
126
177
  res.collect! { |doc| decode(doc['item']) }
127
178
 
@@ -165,13 +216,49 @@ module Resque
165
216
  # If either of those conditions are met, it will use the value obtained
166
217
  # from performing one of the above operations to determine the queue.
167
218
  #
168
- # If no queue can be inferred this method will return a non-true value.
219
+ # If no queue can be inferred this method will raise a `Resque::NoQueueError`
169
220
  #
170
221
  # This method is considered part of the `stable` API.
171
222
  def enqueue(klass, *args)
172
- queue = klass.instance_variable_get(:@queue)
173
- queue ||= klass.queue if klass.respond_to?(:queue)
174
- Job.create(queue, klass, *args)
223
+ Job.create(queue_from_class(klass), klass, *args)
224
+ end
225
+
226
+ # This method can be used to conveniently remove a job from a queue.
227
+ # It assumes the class you're passing it is a real Ruby class (not
228
+ # a string or reference) which either:
229
+ #
230
+ # a) has a @queue ivar set
231
+ # b) responds to `queue`
232
+ #
233
+ # If either of those conditions are met, it will use the value obtained
234
+ # from performing one of the above operations to determine the queue.
235
+ #
236
+ # If no queue can be inferred this method will raise a `Resque::NoQueueError`
237
+ #
238
+ # If no args are given, this method will dequeue *all* jobs matching
239
+ # the provided class. See `Resque::Job.destroy` for more
240
+ # information.
241
+ #
242
+ # Returns the number of jobs destroyed.
243
+ #
244
+ # Example:
245
+ #
246
+ # # Removes all jobs of class `UpdateNetworkGraph`
247
+ # Resque.dequeue(GitHub::Jobs::UpdateNetworkGraph)
248
+ #
249
+ # # Removes all jobs of class `UpdateNetworkGraph` with matching args.
250
+ # Resque.dequeue(GitHub::Jobs::UpdateNetworkGraph, 'repo:135325')
251
+ #
252
+ # This method is considered part of the `stable` API.
253
+ def dequeue(klass, *args)
254
+ Job.destroy(queue_from_class(klass), klass, *args)
255
+ end
256
+
257
+ # Given a class, try to extrapolate an appropriate queue based on a
258
+ # class instance variable or `queue` method.
259
+ def queue_from_class(klass)
260
+ klass.instance_variable_get(:@queue) ||
261
+ (klass.respond_to?(:queue) and klass.queue)
175
262
  end
176
263
 
177
264
  # This method will return a `Resque::Job` object or a non-true value
@@ -4,4 +4,7 @@ module Resque
4
4
 
5
5
  # Raised when trying to create a job without a class
6
6
  class NoClassError < RuntimeError; end
7
+
8
+ # Raised when a worker was killed while processing a job.
9
+ class DirtyExit < RuntimeError; end
7
10
  end
@@ -59,5 +59,8 @@ module Resque
59
59
  backend.clear
60
60
  end
61
61
 
62
+ def self.requeue(index)
63
+ backend.requeue(index)
64
+ end
62
65
  end
63
66
  end
@@ -48,6 +48,9 @@ module Resque
48
48
  # Clear all failure objects
49
49
  def self.clear
50
50
  end
51
+
52
+ def self.requeue(index)
53
+ end
51
54
 
52
55
  # Logging!
53
56
  def log(message)
@@ -1,5 +1,6 @@
1
- require 'net/http'
1
+ require 'net/https'
2
2
  require 'builder'
3
+ require 'uri'
3
4
 
4
5
  module Resque
5
6
  module Failure
@@ -7,21 +8,26 @@ module Resque
7
8
  #
8
9
  # To use it, put this code in an initializer, Rake task, or wherever:
9
10
  #
11
+ # require 'resque/failure/hoptoad'
12
+ #
10
13
  # Resque::Failure::Hoptoad.configure do |config|
11
14
  # config.api_key = 'blah'
12
15
  # config.secure = true
13
- # config.subdomain = 'your_hoptoad_subdomain'
16
+ #
17
+ # # optional proxy support
18
+ # config.proxy_host = 'x.y.z.t'
19
+ # config.proxy_port = 8080
20
+ #
21
+ # # server env support, defaults to RAILS_ENV or RACK_ENV
22
+ # config.server_environment = "test"
14
23
  # end
15
24
  class Hoptoad < Base
16
- #from the hoptoad plugin
17
- INPUT_FORMAT = %r{^([^:]+):(\d+)(?::in `([^']+)')?$}.freeze
18
-
19
- class << self
20
- attr_accessor :secure, :api_key, :subdomain
21
- end
25
+ # From the hoptoad plugin
26
+ INPUT_FORMAT = /^([^:]+):(\d+)(?::in `([^']+)')?$/
22
27
 
23
- def self.url
24
- "http://#{subdomain}.hoptoadapp.com/" if subdomain
28
+ class << self
29
+ attr_accessor :secure, :api_key, :proxy_host, :proxy_port
30
+ attr_accessor :server_environment
25
31
  end
26
32
 
27
33
  def self.count
@@ -35,13 +41,12 @@ module Resque
35
41
  Resque::Failure.backend = self
36
42
  end
37
43
 
38
-
39
-
40
44
  def save
41
45
  http = use_ssl? ? :https : :http
42
46
  url = URI.parse("#{http}://hoptoadapp.com/notifier_api/v2/notices")
43
47
 
44
- http = Net::HTTP.new(url.host, url.port)
48
+ request = Net::HTTP::Proxy(self.class.proxy_host, self.class.proxy_port)
49
+ http = request.new(url.host, url.port)
45
50
  headers = {
46
51
  'Content-type' => 'text/xml',
47
52
  'Accept' => 'text/xml, application/xml'
@@ -49,7 +54,7 @@ module Resque
49
54
 
50
55
  http.read_timeout = 5 # seconds
51
56
  http.open_timeout = 2 # seconds
52
-
57
+
53
58
  http.use_ssl = use_ssl?
54
59
 
55
60
  begin
@@ -66,7 +71,7 @@ module Resque
66
71
  log "Hoptoad Failure: #{response.class}\n#{body}"
67
72
  end
68
73
  end
69
-
74
+
70
75
  def xml
71
76
  x = Builder::XmlMarkup.new
72
77
  x.instruct!
@@ -97,16 +102,16 @@ module Resque
97
102
  end
98
103
  end
99
104
  x.tag!("server-environment") do
100
- x.tag!("environment-name",RAILS_ENV)
105
+ x.tag!("environment-name",server_environment)
101
106
  end
102
-
107
+
103
108
  end
104
109
  end
105
-
110
+
106
111
  def fill_in_backtrace_lines(x)
107
112
  exception.backtrace.each do |unparsed_line|
108
113
  _, file, number, method = unparsed_line.match(INPUT_FORMAT).to_a
109
- x.line :file=>file,:number=>number
114
+ x.line :file => file,:number => number
110
115
  end
111
116
  end
112
117
 
@@ -117,6 +122,11 @@ module Resque
117
122
  def api_key
118
123
  self.class.api_key
119
124
  end
125
+
126
+ def server_environment
127
+ return self.class.server_environment if self.class.server_environment
128
+ defined?(RAILS_ENV) ? RAILS_ENV : (ENV['RACK_ENV'] || 'development')
129
+ end
120
130
  end
121
131
  end
122
132
  end
@@ -7,6 +7,7 @@ module Resque
7
7
  data = {
8
8
  :failed_at => Time.now.strftime("%Y/%m/%d %H:%M:%S"),
9
9
  :payload => payload,
10
+ :exception => exception.class.to_s,
10
11
  :error => exception.to_s,
11
12
  :backtrace => exception.backtrace,
12
13
  :worker => worker.to_s,
@@ -20,13 +21,21 @@ module Resque
20
21
  end
21
22
 
22
23
  def self.all(start = 0, count = 1)
23
- Resque.mongo_failures.find().sort([:natural, :desc]).skip(start).limit(count).to_a
24
+ start, count = [start, count].map { |n| Integer(n) }
25
+ all_failures = Resque.mongo_failures.find().sort([:natural, :desc]).skip(start).limit(count).to_a
26
+ all_failures.size == 1 ? all_failures.first : all_failures
24
27
  end
25
28
 
26
29
  def self.clear
27
30
  Resque.mongo_failures.remove
28
31
  end
29
32
 
33
+ def self.requeue(index)
34
+ item = all(index)
35
+ item['retried_at'] = Time.now.strftime("%Y/%m/%d %H:%M:%S")
36
+ Resque.mongo_failures.update({:_id => item['_id']}, item)
37
+ Job.create(item['queue'], item['payload']['class'], *item['payload']['args'])
38
+ end
30
39
  end
31
40
  end
32
41
  end
@@ -29,9 +29,15 @@ module Resque
29
29
  return unless object
30
30
 
31
31
  if defined? Yajl
32
- Yajl::Parser.parse(object, :check_utf8 => false)
32
+ begin
33
+ Yajl::Parser.parse(object, :check_utf8 => false)
34
+ rescue Yajl::ParseError
35
+ end
33
36
  else
34
- JSON.parse(object)
37
+ begin
38
+ JSON.parse(object)
39
+ rescue JSON::ParserError
40
+ end
35
41
  end
36
42
  end
37
43
 
@@ -15,6 +15,10 @@ module Resque
15
15
  include Helpers
16
16
  extend Helpers
17
17
 
18
+ # Raise Resque::Job::DontPerform from a before_perform hook to
19
+ # abort the job.
20
+ DontPerform = Class.new(StandardError)
21
+
18
22
  # The worker object which is currently processing this job.
19
23
  attr_accessor :worker
20
24
 
@@ -36,7 +40,7 @@ module Resque
36
40
  #
37
41
  # Raises an exception if no queue or class is given.
38
42
  def self.create(queue, klass, *args)
39
- if queue.to_s.empty?
43
+ if !queue
40
44
  raise NoQueueError.new("Jobs must be placed onto a queue.")
41
45
  end
42
46
 
@@ -47,6 +51,50 @@ module Resque
47
51
  Resque.push(queue, :class => klass.to_s, :args => args)
48
52
  end
49
53
 
54
+ # Removes a job from a queue. Expects a string queue name, a
55
+ # string class name, and, optionally, args.
56
+ #
57
+ # Returns the number of jobs destroyed.
58
+ #
59
+ # If no args are provided, it will remove all jobs of the class
60
+ # provided.
61
+ #
62
+ # That is, for these two jobs:
63
+ #
64
+ # { 'class' => 'UpdateGraph', 'args' => ['defunkt'] }
65
+ # { 'class' => 'UpdateGraph', 'args' => ['mojombo'] }
66
+ #
67
+ # The following call will remove both:
68
+ #
69
+ # Resque::Job.destroy(queue, 'UpdateGraph')
70
+ #
71
+ # Whereas specifying args will only remove the 2nd job:
72
+ #
73
+ # Resque::Job.destroy(queue, 'UpdateGraph', 'mojombo')
74
+ #
75
+ # This method can be potentially very slow and memory intensive,
76
+ # depending on the size of your queue, as it loads all jobs into
77
+ # a Ruby array before processing.
78
+ def self.destroy(queue, klass, *args)
79
+ klass = klass.to_s
80
+
81
+ destroyed = 0
82
+
83
+ mongo.find(:queue => queue).each do |rec|
84
+ json = decode(rec['item'])
85
+
86
+ match = json['class'] == klass
87
+ match &= json['args'] == args unless args.empty?
88
+
89
+ if match
90
+ destroyed += 1
91
+ mongo.remove(:_id => rec['_id'])
92
+ end
93
+ end
94
+
95
+ destroyed
96
+ end
97
+
50
98
  # Given a string queue name, returns an instance of Resque::Job
51
99
  # if any jobs are available. If not, returns nil.
52
100
  def self.reserve(queue)
@@ -58,7 +106,64 @@ module Resque
58
106
  # Calls #perform on the class given in the payload with the
59
107
  # arguments given in the payload.
60
108
  def perform
61
- args ? payload_class.perform(*args) : payload_class.perform
109
+ job = payload_class
110
+ job_args = args || []
111
+ job_was_performed = false
112
+
113
+ before_hooks = Plugin.before_hooks(job)
114
+ around_hooks = Plugin.around_hooks(job)
115
+ after_hooks = Plugin.after_hooks(job)
116
+ failure_hooks = Plugin.failure_hooks(job)
117
+
118
+ begin
119
+ # Execute before_perform hook. Abort the job gracefully if
120
+ # Resque::DontPerform is raised.
121
+ begin
122
+ before_hooks.each do |hook|
123
+ job.send(hook, *job_args)
124
+ end
125
+ rescue DontPerform
126
+ return false
127
+ end
128
+
129
+ # Execute the job. Do it in an around_perform hook if available.
130
+ if around_hooks.empty?
131
+ job.perform(*job_args)
132
+ job_was_performed = true
133
+ else
134
+ # We want to nest all around_perform plugins, with the last one
135
+ # finally calling perform
136
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
137
+ if last_hook
138
+ lambda do
139
+ job.send(hook, *job_args) { last_hook.call }
140
+ end
141
+ else
142
+ lambda do
143
+ job.send(hook, *job_args) do
144
+ job.perform(*job_args)
145
+ job_was_performed = true
146
+ end
147
+ end
148
+ end
149
+ end
150
+ stack.call
151
+ end
152
+
153
+ # Execute after_perform hook
154
+ after_hooks.each do |hook|
155
+ job.send(hook, *job_args)
156
+ end
157
+
158
+ # Return true if the job was performed
159
+ return job_was_performed
160
+
161
+ # If an exception occurs during the job execution, look for an
162
+ # on_failure hook then re-raise.
163
+ rescue Object => e
164
+ failure_hooks.each { |hook| job.send(hook, e, *job_args) }
165
+ raise e
166
+ end
62
167
  end
63
168
 
64
169
  # Returns the actual class constant represented in this job's payload.