resque-mongo 1.4.0 → 1.8.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.
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.