resque 1.26.pre.0 → 1.26.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of resque might be problematic. Click here for more details.

Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.md +29 -16
  3. data/README.markdown +60 -6
  4. data/Rakefile +4 -17
  5. data/bin/resque-web +4 -0
  6. data/lib/resque.rb +116 -16
  7. data/lib/resque/errors.rb +1 -0
  8. data/lib/resque/failure.rb +11 -5
  9. data/lib/resque/failure/multiple.rb +6 -1
  10. data/lib/resque/failure/redis.rb +13 -4
  11. data/lib/resque/failure/redis_multi_queue.rb +14 -6
  12. data/lib/resque/helpers.rb +5 -64
  13. data/lib/resque/job.rb +25 -79
  14. data/lib/resque/logging.rb +1 -1
  15. data/lib/resque/plugin.rb +22 -10
  16. data/lib/resque/server.rb +35 -7
  17. data/lib/resque/server/helpers.rb +1 -1
  18. data/lib/resque/server/views/failed.erb +1 -1
  19. data/lib/resque/server/views/failed_job.erb +3 -3
  20. data/lib/resque/server/views/failed_queues_overview.erb +3 -3
  21. data/lib/resque/server/views/workers.erb +2 -0
  22. data/lib/resque/server/views/working.erb +4 -5
  23. data/lib/resque/tasks.rb +7 -25
  24. data/lib/resque/vendor/utf8_util/utf8_util_19.rb +1 -0
  25. data/lib/resque/version.rb +1 -1
  26. data/lib/resque/worker.rb +225 -116
  27. metadata +68 -90
  28. data/test/airbrake_test.rb +0 -27
  29. data/test/dump.rdb +0 -0
  30. data/test/failure_base_test.rb +0 -15
  31. data/test/job_hooks_test.rb +0 -464
  32. data/test/job_plugins_test.rb +0 -230
  33. data/test/logging_test.rb +0 -24
  34. data/test/plugin_test.rb +0 -116
  35. data/test/redis-test-cluster.conf +0 -115
  36. data/test/redis-test.conf +0 -115
  37. data/test/resque-web_test.rb +0 -59
  38. data/test/resque_failure_redis_test.rb +0 -19
  39. data/test/resque_hook_test.rb +0 -165
  40. data/test/resque_test.rb +0 -278
  41. data/test/test_helper.rb +0 -198
  42. data/test/worker_test.rb +0 -1015
@@ -38,10 +38,14 @@ module Resque
38
38
  end
39
39
 
40
40
  def url_path(*path_parts)
41
- [ path_prefix, path_parts ].join("/").squeeze('/')
41
+ [ url_prefix, path_prefix, path_parts ].join("/").squeeze('/')
42
42
  end
43
43
  alias_method :u, :url_path
44
44
 
45
+ def redirect_url_path(*path_parts)
46
+ [ path_prefix, path_parts ].join("/").squeeze('/')
47
+ end
48
+
45
49
  def path_prefix
46
50
  request.env['SCRIPT_NAME']
47
51
  end
@@ -60,6 +64,10 @@ module Resque
60
64
  Resque::Server.tabs
61
65
  end
62
66
 
67
+ def url_prefix
68
+ Resque::Server.url_prefix
69
+ end
70
+
63
71
  def redis_get_size(key)
64
72
  case Resque.redis.type(key)
65
73
  when 'none'
@@ -150,7 +158,7 @@ module Resque
150
158
 
151
159
  # to make things easier on ourselves
152
160
  get "/?" do
153
- redirect url_path(:overview)
161
+ redirect redirect_url_path(:overview)
154
162
  end
155
163
 
156
164
  %w( overview workers ).each do |page|
@@ -205,15 +213,13 @@ module Resque
205
213
  end
206
214
 
207
215
  post "/failed/requeue/all" do
208
- Resque::Failure.count.times do |num|
209
- Resque::Failure.requeue(num)
210
- end
216
+ Resque::Failure.requeue_all
211
217
  redirect u('failed')
212
218
  end
213
219
 
214
220
  post "/failed/:queue/requeue/all" do
215
221
  Resque::Failure.requeue_queue Resque::Failure.job_queue_name(params[:queue])
216
- redirect url_path("/failed/#{params[:queue]}")
222
+ redirect redirect_url_path("/failed/#{params[:queue]}")
217
223
  end
218
224
 
219
225
  get "/failed/requeue/:index/?" do
@@ -225,13 +231,27 @@ module Resque
225
231
  end
226
232
  end
227
233
 
234
+ get "/failed/:queue/requeue/:index/?" do
235
+ Resque::Failure.requeue(params[:index], params[:queue])
236
+ if request.xhr?
237
+ return Resque::Failure.all(params[:index],1,params[:queue])['retried_at']
238
+ else
239
+ redirect url_path("/failed/#{params[:queue]}")
240
+ end
241
+ end
242
+
228
243
  get "/failed/remove/:index/?" do
229
244
  Resque::Failure.remove(params[:index])
230
245
  redirect u('failed')
231
246
  end
232
247
 
248
+ get "/failed/:queue/remove/:index/?" do
249
+ Resque::Failure.remove(params[:index], params[:queue])
250
+ redirect url_path("/failed/#{params[:queue]}")
251
+ end
252
+
233
253
  get "/stats/?" do
234
- redirect url_path("/stats/resque")
254
+ redirect redirect_url_path("/stats/resque")
235
255
  end
236
256
 
237
257
  get "/stats/:id/?" do
@@ -267,5 +287,13 @@ module Resque
267
287
  def self.tabs
268
288
  @tabs ||= ["Overview", "Working", "Failed", "Queues", "Workers", "Stats"]
269
289
  end
290
+
291
+ def self.url_prefix=(url_prefix)
292
+ @url_prefix = url_prefix
293
+ end
294
+
295
+ def self.url_prefix
296
+ (@url_prefix.nil? || @url_prefix.empty?) ? '' : @url_prefix + '/'
297
+ end
270
298
  end
271
299
  end
@@ -32,7 +32,7 @@ Resque::Server.helpers do
32
32
  if failed_start_at + failed_per_page > failed_size
33
33
  failed_size
34
34
  else
35
- failed_start_at + failed_per_page
35
+ failed_start_at + failed_per_page - 1
36
36
  end
37
37
  end
38
38
 
@@ -21,7 +21,7 @@
21
21
 
22
22
  <ul class='failed'>
23
23
  <% Resque::Failure.each(failed_start_at, failed_per_page, params[:queue], params[:class], failed_order) do |id, job| %>
24
- <%= partial :failed_job, :id => id, :job => job %>
24
+ <%= partial :failed_job, :id => id, :job => job, :queue => "failed#{'/' + params[:queue] if params[:queue]}" %>
25
25
  <% end %>
26
26
  </ul>
27
27
 
@@ -10,13 +10,13 @@
10
10
  <% if job['retried_at'] %>
11
11
  <div class='retried'>
12
12
  Retried <b><span class="time"><%= Time.parse(job['retried_at']).strftime(failed_date_format) %></span></b>
13
- <a href="<%= u "failed/remove/#{id}" %>" class="remove" rel="remove">Remove</a>
13
+ <a href="<%= u "#{queue}/remove/#{id}" %>" class="remove" rel="remove">Remove</a>
14
14
  </div>
15
15
  <% else %>
16
16
  <div class='controls'>
17
- <a href="<%= u "failed/requeue/#{id}" %>" rel="retry">Retry</a>
17
+ <a href="<%= u "#{queue}/requeue/#{id}" %>" rel="retry">Retry</a>
18
18
  or
19
- <a href="<%= u "failed/remove/#{id}" %>" rel="remove">Remove</a>
19
+ <a href="<%= u "#{queue}/remove/#{id}" %>" rel="remove">Remove</a>
20
20
  </div>
21
21
  <% end %>
22
22
  </dd>
@@ -7,18 +7,18 @@
7
7
 
8
8
  <% Resque::Failure.queues.sort.each do |queue| %>
9
9
  <tr>
10
- <th><b class="queue-tag"><a href="/failed/<%= queue %>"><%= queue %></a></b></th>
10
+ <th><b class="queue-tag"><a href="<%= u "/failed/#{queue}" %>"><%= queue %></a></b></th>
11
11
  <th style="width:75px;" class="center"><%= Resque::Failure.count(queue) %></th>
12
12
  </tr>
13
13
 
14
14
  <% failed_class_counts(queue).sort_by { |name,_| name }.each do |k, v| %>
15
15
  <tr id="<%= k %>">
16
16
  <td>
17
- <a href="/failed/<%= "#{queue}?class=#{k}" %>"><span class="failed failed_class"><%= k %></span></a>
17
+ <a href="<%= u "/failed/#{queue}?class=#{k}" %>"><span class="failed failed_class"><%= k %></span></a>
18
18
  </td>
19
19
  <td style="text-align: center;" class="failed<%= (v.to_i > 1000) ? '_many' : '' %>"><%= v %></td>
20
20
  </tr>
21
21
  <% end %>
22
22
  <% end %>
23
23
  </tbody>
24
- </table>
24
+ </table>
@@ -9,6 +9,7 @@
9
9
  <th>Host</th>
10
10
  <th>Pid</th>
11
11
  <th>Started</th>
12
+ <th>Heartbeat</th>
12
13
  <th>Queues</th>
13
14
  <th>Processed</th>
14
15
  <th>Failed</th>
@@ -21,6 +22,7 @@
21
22
  <td><%= host %></td>
22
23
  <td><%= pid %></td>
23
24
  <td><span class="time"><%= worker.started %></span></td>
25
+ <td><span class="time"><%= worker.heartbeat %></span></td>
24
26
  <td class='queues'><%= queues.split(',').map { |q| '<a class="queue-tag" href="' + u("/queues/#{q}") + '">' + q + '</a>'}.join('') %></td>
25
27
  <td><%= worker.processed %></td>
26
28
  <td><%= worker.failed %></td>
@@ -1,4 +1,4 @@
1
- <% if params[:id] && (worker = Resque::Worker.find(params[:id])) && worker.job %>
1
+ <% if params[:id] && (worker = Resque::Worker.find(params[:id])) && (data = worker.job) %>
2
2
  <h1><%= worker %>'s job</h1>
3
3
 
4
4
  <table>
@@ -14,14 +14,13 @@
14
14
  <td><img src="<%=u 'working.png' %>" alt="working" title="working"></td>
15
15
  <% host, pid, _ = worker.to_s.split(':') %>
16
16
  <td><a href="<%=u "/workers/#{worker}" %>"><%= host %>:<%= pid %></a></td>
17
- <% data = worker.job %>
18
17
  <% queue = data['queue'] %>
19
18
  <td><a class="queue" href="<%=u "/queues/#{queue}" %>"><%= queue %></a></td>
20
19
  <td><span class="time"><%= data['run_at'] %></span></td>
21
20
  <td>
22
- <code><%= data['payload']['class'] %></code>
21
+ <code><%= data['payload'].to_h['class'] %></code>
23
22
  </td>
24
- <td><%=h data['payload']['args'].inspect %></td>
23
+ <td><%=h data['payload'].to_h['args'].inspect %></td>
25
24
  </tr>
26
25
  </table>
27
26
 
@@ -49,7 +48,7 @@
49
48
  </tr>
50
49
  <% end %>
51
50
 
52
- <% worker_jobs.sort_by {|w, j| j['run_at'] ? j['run_at'] : '' }.each do |worker, job| %>
51
+ <% worker_jobs.sort_by {|w, j| j['run_at'] ? j['run_at'].to_s() : '' }.each do |worker, job| %>
53
52
  <tr>
54
53
  <td class='icon'><img src="<%=u state = worker.state %>.png" alt="<%= state %>" title="<%= state %>"></td>
55
54
  <% host, pid, queues = worker.to_s.split(':') %>
@@ -1,6 +1,7 @@
1
1
  # require 'resque/tasks'
2
2
  # will give you the resque tasks
3
3
 
4
+
4
5
  namespace :resque do
5
6
  task :setup
6
7
 
@@ -8,36 +9,13 @@ namespace :resque do
8
9
  task :work => [ :preload, :setup ] do
9
10
  require 'resque'
10
11
 
11
- queues = (ENV['QUEUES'] || ENV['QUEUE']).to_s.split(',')
12
-
13
12
  begin
14
- worker = Resque::Worker.new(*queues)
15
- if ENV['LOGGING'] || ENV['VERBOSE']
16
- worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
17
- end
18
- if ENV['VVERBOSE']
19
- worker.very_verbose = ENV['VVERBOSE']
20
- end
21
- worker.term_timeout = ENV['RESQUE_TERM_TIMEOUT'] || 4.0
22
- worker.term_child = ENV['TERM_CHILD']
23
- worker.run_at_exit_hooks = ENV['RUN_AT_EXIT_HOOKS']
13
+ worker = Resque::Worker.new
24
14
  rescue Resque::NoQueueError
25
15
  abort "set QUEUE env var, e.g. $ QUEUE=critical,high rake resque:work"
26
16
  end
27
17
 
28
- if ENV['BACKGROUND']
29
- unless Process.respond_to?('daemon')
30
- abort "env var BACKGROUND is set, which requires ruby >= 1.9"
31
- end
32
- Process.daemon(true, true)
33
- end
34
-
35
- if ENV['PIDFILE']
36
- File.open(ENV['PIDFILE'], 'w') { |f| f << worker.pid }
37
- end
38
-
39
- worker.log "Starting worker #{worker}"
40
-
18
+ worker.log "Starting worker #{self}"
41
19
  worker.work(ENV['INTERVAL'] || 5) # interval, will block
42
20
  end
43
21
 
@@ -45,6 +23,10 @@ namespace :resque do
45
23
  task :workers do
46
24
  threads = []
47
25
 
26
+ if ENV['COUNT'].to_i < 1
27
+ abort "set COUNT env var, e.g. $ COUNT=2 rake resque:workers"
28
+ end
29
+
48
30
  ENV['COUNT'].to_i.times do
49
31
  threads << Thread.new do
50
32
  system "rake resque:work"
@@ -1,5 +1,6 @@
1
1
  module UTF8Util
2
2
  def self.clean!(str)
3
+ return str if str.encoding.to_s == "UTF-8"
3
4
  str.force_encoding("binary").encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => REPLACEMENT_CHAR)
4
5
  end
5
6
  end
@@ -1,3 +1,3 @@
1
1
  module Resque
2
- Version = VERSION = '1.26.pre.0'
2
+ Version = VERSION = '1.26.0'
3
3
  end
@@ -10,8 +10,12 @@ module Resque
10
10
  # It also ensures workers are always listening to signals from you,
11
11
  # their master, and can react accordingly.
12
12
  class Worker
13
+ include Resque::Helpers
14
+ extend Resque::Helpers
13
15
  include Resque::Logging
14
16
 
17
+ WORKER_HEARTBEAT_KEY = "workers:heartbeat"
18
+
15
19
  def redis
16
20
  Resque.redis
17
21
  end
@@ -23,46 +27,33 @@ module Resque
23
27
  # Given a Ruby object, returns a string suitable for storage in a
24
28
  # queue.
25
29
  def encode(object)
26
- if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
27
- MultiJson.dump object
28
- else
29
- MultiJson.encode object
30
- end
30
+ Resque.encode(object)
31
31
  end
32
32
 
33
33
  # Given a string, returns a Ruby object.
34
34
  def decode(object)
35
- return unless object
36
-
37
- begin
38
- if MultiJson.respond_to?(:dump) && MultiJson.respond_to?(:load)
39
- MultiJson.load object
40
- else
41
- MultiJson.decode object
42
- end
43
- rescue ::MultiJson::DecodeError => e
44
- raise DecodeException, e.message, e.backtrace
45
- end
35
+ Resque.decode(object)
46
36
  end
47
37
 
48
- # Boolean indicating whether this worker can or can not fork.
49
- # Automatically set if a fork(2) fails.
50
- attr_accessor :cant_fork
51
-
52
38
  attr_accessor :term_timeout
53
39
 
54
40
  # decide whether to use new_kill_child logic
55
41
  attr_accessor :term_child
56
42
 
43
+ # should term kill workers gracefully (vs. immediately)
44
+ # Makes SIGTERM work like SIGQUIT
45
+ attr_accessor :graceful_term
46
+
57
47
  # When set to true, forked workers will exit with `exit`, calling any `at_exit` code handlers that have been
58
48
  # registered in the application. Otherwise, forked workers exit with `exit!`
59
49
  attr_accessor :run_at_exit_hooks
60
50
 
61
51
  attr_writer :to_s
52
+ attr_writer :pid
62
53
 
63
54
  # Returns an array of all worker objects.
64
55
  def self.all
65
- Array(redis.smembers(:workers)).map { |id| find(id) }.compact
56
+ Array(redis.smembers(:workers)).map { |id| find(id, :skip_exists => true) }.compact
66
57
  end
67
58
 
68
59
  # Returns an array of all worker objects currently processing
@@ -87,16 +78,22 @@ module Resque
87
78
  end
88
79
 
89
80
  reportedly_working.keys.map do |key|
90
- find key.sub("worker:", '')
81
+ worker = find(key.sub("worker:", ''), :skip_exists => true)
82
+ worker.job = worker.decode(reportedly_working[key])
83
+ worker
91
84
  end.compact
92
85
  end
93
86
 
94
87
  # Returns a single worker object. Accepts a string id.
95
- def self.find(worker_id)
96
- if exists? worker_id
97
- queues = worker_id.split(':')[-1].split(',')
88
+ def self.find(worker_id, options = {})
89
+ skip_exists = options[:skip_exists]
90
+
91
+ if skip_exists || exists?(worker_id)
92
+ host, pid, queues_raw = worker_id.split(':')
93
+ queues = queues_raw.split(',')
98
94
  worker = new(*queues)
99
95
  worker.to_s = worker_id
96
+ worker.pid = pid.to_i
100
97
  worker
101
98
  else
102
99
  nil
@@ -126,9 +123,38 @@ module Resque
126
123
  # in alphabetical order. Queues can be dynamically added or
127
124
  # removed without needing to restart workers using this method.
128
125
  def initialize(*queues)
129
- @queues = queues.map { |queue| queue.to_s.strip }
130
126
  @shutdown = nil
131
127
  @paused = nil
128
+ @before_first_fork_hook_ran = false
129
+
130
+ self.verbose = ENV['LOGGING'] || ENV['VERBOSE']
131
+ self.very_verbose = ENV['VVERBOSE']
132
+ self.term_timeout = ENV['RESQUE_TERM_TIMEOUT'] || 4.0
133
+ self.term_child = ENV['TERM_CHILD']
134
+ self.graceful_term = ENV['GRACEFUL_TERM']
135
+ self.run_at_exit_hooks = ENV['RUN_AT_EXIT_HOOKS']
136
+
137
+ if ENV['BACKGROUND']
138
+ unless Process.respond_to?('daemon')
139
+ abort "env var BACKGROUND is set, which requires ruby >= 1.9"
140
+ end
141
+ Process.daemon(true)
142
+ self.reconnect
143
+ end
144
+
145
+ if ENV['PIDFILE']
146
+ File.open(ENV['PIDFILE'], 'w') { |f| f << pid }
147
+ end
148
+
149
+ self.queues = queues
150
+ end
151
+
152
+ def queues=(queues)
153
+ queues = queues.empty? ? (ENV["QUEUES"] || ENV['QUEUE']).to_s.split(',') : queues
154
+ @queues = queues.map { |queue| queue.to_s.strip }
155
+ unless ['*', '?', '{', '}', '[', ']'].any? {|char| @queues.join.include?(char) }
156
+ @static_queues = @queues.flatten.uniq
157
+ end
132
158
  validate_queues
133
159
  end
134
160
 
@@ -142,6 +168,20 @@ module Resque
142
168
  end
143
169
  end
144
170
 
171
+ # Returns a list of queues to use when searching for a job.
172
+ # A splat ("*") means you want every queue (in alpha order) - this
173
+ # can be useful for dynamically adding new queues.
174
+ def queues
175
+ return @static_queues if @static_queues
176
+ @queues.map { |queue| glob_match(queue) }.flatten.uniq
177
+ end
178
+
179
+ def glob_match(pattern)
180
+ Resque.queues.select do |queue|
181
+ File.fnmatch?(pattern, queue)
182
+ end.sort
183
+ end
184
+
145
185
  # This is the main workhorse method. Called on a Worker instance,
146
186
  # it begins the worker life cycle.
147
187
  #
@@ -167,7 +207,7 @@ module Resque
167
207
  break if shutdown?
168
208
 
169
209
  if not paused? and job = reserve
170
- log "got: #{job.inspect}"
210
+ log_with_severity :info, "got: #{job.inspect}"
171
211
  job.worker = self
172
212
  working_on job
173
213
 
@@ -180,12 +220,12 @@ module Resque
180
220
  rescue SystemCallError
181
221
  nil
182
222
  end
183
- job.fail(DirtyExit.new($?.to_s)) if $?.signaled?
223
+ job.fail(DirtyExit.new("Child process received unhandled signal #{$?.stopsig}")) if $?.signaled?
184
224
  else
185
225
  unregister_signal_handlers if will_fork? && term_child
186
226
  begin
187
227
 
188
- reconnect
228
+ reconnect if will_fork?
189
229
  perform(job, &block)
190
230
 
191
231
  rescue Exception => exception
@@ -201,8 +241,8 @@ module Resque
201
241
  @child = nil
202
242
  else
203
243
  break if interval.zero?
204
- log! "Sleeping for #{interval} seconds"
205
- procline paused? ? "Paused" : "Waiting for #{@queues.join(',')}"
244
+ log_with_severity :debug, "Sleeping for #{interval} seconds"
245
+ procline paused? ? "Paused" : "Waiting for #{queues.join(',')}"
206
246
  sleep interval
207
247
  end
208
248
  end
@@ -210,7 +250,7 @@ module Resque
210
250
  unregister_worker
211
251
  rescue Exception => exception
212
252
  unless exception.class == SystemExit && !@child && run_at_exit_hooks
213
- log "Failed to start worker : #{exception.inspect}"
253
+ log_with_severity :error, "Failed to start worker : #{exception.inspect}"
214
254
 
215
255
  unregister_worker(exception)
216
256
  end
@@ -230,16 +270,16 @@ module Resque
230
270
 
231
271
  # Reports the exception and marks the job as failed
232
272
  def report_failed_job(job,exception)
233
- log "#{job.inspect} failed: #{exception.inspect}"
273
+ log_with_severity :error, "#{job.inspect} failed: #{exception.inspect}"
234
274
  begin
235
275
  job.fail(exception)
236
276
  rescue Object => exception
237
- log "Received exception when reporting failure: #{exception.inspect}"
277
+ log_with_severity :error, "Received exception when reporting failure: #{exception.inspect}"
238
278
  end
239
279
  begin
240
280
  failed!
241
281
  rescue Object => exception
242
- log "Received exception when increasing failed jobs counter (redis issue) : #{exception.inspect}"
282
+ log_with_severity :error, "Received exception when increasing failed jobs counter (redis issue) : #{exception.inspect}"
243
283
  end
244
284
  end
245
285
 
@@ -251,7 +291,7 @@ module Resque
251
291
  rescue Object => e
252
292
  report_failed_job(job,e)
253
293
  else
254
- log "done: #{job.inspect}"
294
+ log_with_severity :info, "done: #{job.inspect}"
255
295
  ensure
256
296
  yield job if block_given?
257
297
  end
@@ -261,17 +301,17 @@ module Resque
261
301
  # nil if no job can be found.
262
302
  def reserve
263
303
  queues.each do |queue|
264
- log! "Checking #{queue}"
304
+ log_with_severity :debug, "Checking #{queue}"
265
305
  if job = Resque.reserve(queue)
266
- log! "Found job on #{queue}"
306
+ log_with_severity :debug, "Found job on #{queue}"
267
307
  return job
268
308
  end
269
309
  end
270
310
 
271
311
  nil
272
312
  rescue Exception => e
273
- log "Error reserving job: #{e.inspect}"
274
- log e.backtrace.join("\n")
313
+ log_with_severity :error, "Error reserving job: #{e.inspect}"
314
+ log_with_severity :error, e.backtrace.join("\n")
275
315
  raise e
276
316
  end
277
317
 
@@ -283,49 +323,29 @@ module Resque
283
323
  redis.client.reconnect
284
324
  rescue Redis::BaseConnectionError
285
325
  if (tries += 1) <= 3
286
- log "Error reconnecting to Redis; retrying"
326
+ log_with_severity :error, "Error reconnecting to Redis; retrying"
287
327
  sleep(tries)
288
328
  retry
289
329
  else
290
- log "Error reconnecting to Redis; quitting"
330
+ log_with_severity :error, "Error reconnecting to Redis; quitting"
291
331
  raise
292
332
  end
293
333
  end
294
334
  end
295
335
 
296
- # Returns a list of queues to use when searching for a job.
297
- # A splat ("*") means you want every queue (in alpha order) - this
298
- # can be useful for dynamically adding new queues.
299
- def queues
300
- @queues.map do |queue|
301
- queue.strip!
302
- if (matched_queues = glob_match(queue)).empty?
303
- queue
304
- else
305
- matched_queues
306
- end
307
- end.flatten.uniq
308
- end
309
-
310
- def glob_match(pattern)
311
- Resque.queues.select do |queue|
312
- File.fnmatch?(pattern, queue)
313
- end.sort
314
- end
315
-
316
336
  # Not every platform supports fork. Here we do our magic to
317
337
  # determine if yours does.
318
338
  def fork(job)
319
- return if @cant_fork
339
+ return unless will_fork?
320
340
 
321
341
  # Only run before_fork hooks if we're actually going to fork
322
- # (after checking @cant_fork)
342
+ # (after checking will_fork?)
323
343
  run_hook :before_fork, job
324
344
 
325
345
  begin
326
346
  # IronRuby doesn't support `Kernel.fork` yet
327
347
  if Kernel.respond_to?(:fork)
328
- Kernel.fork if will_fork?
348
+ Kernel.fork
329
349
  else
330
350
  raise NotImplementedError
331
351
  end
@@ -337,9 +357,9 @@ module Resque
337
357
 
338
358
  # Runs all the methods needed when a worker begins its lifecycle.
339
359
  def startup
340
- Kernel.warn "WARNING: This way of doing signal handling is now deprecated. Please see http://hone.heroku.com/resque/2012/08/21/resque-signals.html for more info." unless term_child or $TESTING
341
360
  enable_gc_optimizations
342
361
  register_signal_handlers
362
+ start_heartbeat
343
363
  prune_dead_workers
344
364
  run_hook :before_first_fork
345
365
  register_worker
@@ -366,7 +386,7 @@ module Resque
366
386
  # USR2: Don't process any new jobs
367
387
  # CONT: Start processing jobs again after a USR2
368
388
  def register_signal_handlers
369
- trap('TERM') { shutdown! }
389
+ trap('TERM') { graceful_term ? shutdown : shutdown! }
370
390
  trap('INT') { shutdown! }
371
391
 
372
392
  begin
@@ -379,19 +399,19 @@ module Resque
379
399
  trap('USR2') { pause_processing }
380
400
  trap('CONT') { unpause_processing }
381
401
  rescue ArgumentError
382
- warn "Signals QUIT, USR1, USR2, and/or CONT not supported."
402
+ log_with_severity :warn, "Signals QUIT, USR1, USR2, and/or CONT not supported."
383
403
  end
384
404
 
385
- log! "Registered signals"
405
+ log_with_severity :debug, "Registered signals"
386
406
  end
387
407
 
388
408
  def unregister_signal_handlers
389
409
  trap('TERM') do
390
- trap ('TERM') do
391
- # ignore subsequent terms
392
- end
393
- raise TermException.new("SIGTERM")
394
- end
410
+ trap ('TERM') do
411
+ # ignore subsequent terms
412
+ end
413
+ raise TermException.new("SIGTERM")
414
+ end
395
415
  trap('INT', 'DEFAULT')
396
416
 
397
417
  begin
@@ -405,15 +425,24 @@ module Resque
405
425
  # Schedule this worker for shutdown. Will finish processing the
406
426
  # current job.
407
427
  def shutdown
408
- log 'Exiting...'
428
+ log_with_severity :info, 'Exiting...'
409
429
  @shutdown = true
410
430
  end
411
431
 
412
432
  # Kill the child and shutdown immediately.
433
+ # If not forking, abort this process.
413
434
  def shutdown!
414
435
  shutdown
415
436
  if term_child
416
- new_kill_child
437
+ if fork_per_job?
438
+ new_kill_child
439
+ else
440
+ # Raise TermException in the same process
441
+ trap('TERM') do
442
+ # ignore subsequent terms
443
+ end
444
+ raise TermException.new("SIGTERM")
445
+ end
417
446
  else
418
447
  kill_child
419
448
  end
@@ -428,36 +457,78 @@ module Resque
428
457
  # is processing will not be completed.
429
458
  def kill_child
430
459
  if @child
431
- log! "Killing child at #{@child}"
460
+ log_with_severity :debug, "Killing child at #{@child}"
432
461
  if `ps -o pid,state -p #{@child}`
433
462
  Process.kill("KILL", @child) rescue nil
434
463
  else
435
- log! "Child #{@child} not found, restarting."
464
+ log_with_severity :debug, "Child #{@child} not found, restarting."
436
465
  shutdown
437
466
  end
438
467
  end
439
468
  end
440
469
 
470
+ def heartbeat
471
+ heartbeat = redis.hget(WORKER_HEARTBEAT_KEY, to_s)
472
+ heartbeat && Time.parse(heartbeat)
473
+ end
474
+
475
+ def self.all_heartbeats
476
+ redis.hgetall(WORKER_HEARTBEAT_KEY)
477
+ end
478
+
479
+ # Returns a list of workers that have sent a heartbeat in the past, but which
480
+ # already expired (does NOT include workers that have never sent a heartbeat at all).
481
+ def self.all_workers_with_expired_heartbeats
482
+ workers = Worker.all
483
+ heartbeats = Worker.all_heartbeats
484
+
485
+ workers.select do |worker|
486
+ id = worker.to_s
487
+ heartbeat = heartbeats[id]
488
+
489
+ if heartbeat
490
+ seconds_since_heartbeat = (Time.now - Time.parse(heartbeat)).to_i
491
+ seconds_since_heartbeat > Resque.prune_interval
492
+ else
493
+ false
494
+ end
495
+ end
496
+ end
497
+
498
+ def heartbeat!(time = Time.now)
499
+ redis.hset(WORKER_HEARTBEAT_KEY, to_s, time.iso8601)
500
+ end
501
+
502
+ def start_heartbeat
503
+ heartbeat!
504
+ @heart = Thread.new do
505
+ loop do
506
+ sleep(Resque.heartbeat_interval)
507
+ heartbeat!
508
+ end
509
+ end
510
+ end
511
+
441
512
  # Kills the forked child immediately with minimal remorse. The job it
442
513
  # is processing will not be completed. Send the child a TERM signal,
443
514
  # wait 5 seconds, and then a KILL signal if it has not quit
444
515
  def new_kill_child
445
516
  if @child
446
517
  unless Process.waitpid(@child, Process::WNOHANG)
447
- log! "Sending TERM signal to child #{@child}"
518
+ log_with_severity :debug, "Sending TERM signal to child #{@child}"
448
519
  Process.kill("TERM", @child)
449
520
  (term_timeout.to_f * 10).round.times do |i|
450
521
  sleep(0.1)
451
522
  return if Process.waitpid(@child, Process::WNOHANG)
452
523
  end
453
- log! "Sending KILL signal to child #{@child}"
524
+ log_with_severity :debug, "Sending KILL signal to child #{@child}"
454
525
  Process.kill("KILL", @child)
455
526
  else
456
- log! "Child #{@child} already quit."
527
+ log_with_severity :debug, "Child #{@child} already quit."
457
528
  end
458
529
  end
459
530
  rescue SystemCallError
460
- log! "Child #{@child} already quit and reaped."
531
+ log_with_severity :error, "Child #{@child} already quit and reaped."
461
532
  end
462
533
 
463
534
  # are we paused?
@@ -468,14 +539,16 @@ module Resque
468
539
  # Stop processing jobs after the current one has completed (if we're
469
540
  # currently running one).
470
541
  def pause_processing
471
- log "USR2 received; pausing job processing"
542
+ log_with_severity :info, "USR2 received; pausing job processing"
543
+ run_hook :before_pause, self
472
544
  @paused = true
473
545
  end
474
546
 
475
547
  # Start processing jobs again after a pause
476
548
  def unpause_processing
477
- log "CONT received; resuming job processing"
549
+ log_with_severity :info, "CONT received; resuming job processing"
478
550
  @paused = false
551
+ run_hook :after_pause, self
479
552
  end
480
553
 
481
554
  # Looks for any workers which should be running on this server
@@ -490,21 +563,40 @@ module Resque
490
563
  # environment, we can determine if Redis is old and clean it up a bit.
491
564
  def prune_dead_workers
492
565
  all_workers = Worker.all
493
- known_workers = worker_pids unless all_workers.empty?
566
+
567
+ unless all_workers.empty?
568
+ known_workers = worker_pids
569
+ all_workers_with_expired_heartbeats = Worker.all_workers_with_expired_heartbeats
570
+ end
571
+
494
572
  all_workers.each do |worker|
573
+ # If the worker hasn't sent a heartbeat, remove it from the registry.
574
+ #
575
+ # If the worker hasn't ever sent a heartbeat, we won't remove it since
576
+ # the first heartbeat is sent before the worker is registred it means
577
+ # that this is a worker that doesn't support heartbeats, e.g., another
578
+ # client library or an older version of Resque. We won't touch these.
579
+ if all_workers_with_expired_heartbeats.include?(worker)
580
+ log_with_severity :info, "Pruning dead worker: #{worker}"
581
+ worker.unregister_worker(PruneDeadWorkerDirtyExit.new(worker.to_s))
582
+ next
583
+ end
584
+
495
585
  host, pid, worker_queues_raw = worker.id.split(':')
496
586
  worker_queues = worker_queues_raw.split(",")
497
587
  unless @queues.include?("*") || (worker_queues.to_set == @queues.to_set)
498
588
  # If the worker we are trying to prune does not belong to the queues
499
- # we are listening to, we should not touch it.
589
+ # we are listening to, we should not touch it.
500
590
  # Attempt to prune a worker from different queues may easily result in
501
- # an unknown class exception, since that worker could easily be even
591
+ # an unknown class exception, since that worker could easily be even
502
592
  # written in different language.
503
593
  next
504
594
  end
595
+
505
596
  next unless host == hostname
506
597
  next if known_workers.include?(pid)
507
- log! "Pruning dead worker: #{worker}"
598
+
599
+ log_with_severity :debug, "Pruning dead worker: #{worker}"
508
600
  worker.unregister_worker
509
601
  end
510
602
  end
@@ -521,15 +613,21 @@ module Resque
521
613
  # Runs a named hook, passing along any arguments.
522
614
  def run_hook(name, *args)
523
615
  return unless hooks = Resque.send(name)
616
+ return if name == :before_first_fork && @before_first_fork_hook_ran
524
617
  msg = "Running #{name} hooks"
525
618
  msg << " with #{args.inspect}" if args.any?
526
- log msg
619
+ log_with_severity :info, msg
527
620
 
528
621
  hooks.each do |hook|
529
622
  args.any? ? hook.call(*args) : hook.call
623
+ @before_first_fork_hook_ran = true if name == :before_first_fork
530
624
  end
531
625
  end
532
626
 
627
+ def kill_background_threads
628
+ @heart.kill if @heart
629
+ end
630
+
533
631
  # Unregisters ourself as a worker. Useful when shutting down.
534
632
  def unregister_worker(exception = nil)
535
633
  # If we're still processing a job, make sure it gets logged as a
@@ -539,17 +637,33 @@ module Resque
539
637
  # Ensure the proper worker is attached to this job, even if
540
638
  # it's not the precise instance that died.
541
639
  job.worker = self
542
- job.fail(exception || DirtyExit.new)
640
+ begin
641
+ job.fail(exception || DirtyExit.new("Job still being processed"))
642
+ rescue RuntimeError => e
643
+ log_with_severity :error, e.message
644
+ end
543
645
  end
544
646
 
647
+ kill_background_threads
648
+
545
649
  redis.pipelined do
546
650
  redis.srem(:workers, self)
547
651
  redis.del("worker:#{self}")
548
652
  redis.del("worker:#{self}:started")
653
+ redis.hdel(WORKER_HEARTBEAT_KEY, self.to_s)
549
654
 
550
655
  Stat.clear("processed:#{self}")
551
656
  Stat.clear("failed:#{self}")
552
657
  end
658
+ rescue Exception => exception_while_unregistering
659
+ message = exception_while_unregistering.message
660
+ if exception
661
+ message = message + "\nOriginal Exception (#{exception.class}): #{exception.message}\n" +
662
+ " #{exception.backtrace.join(" \n")}"
663
+ end
664
+ fail(exception_while_unregistering.class,
665
+ message,
666
+ exception_while_unregistering.backtrace)
553
667
  end
554
668
 
555
669
  # Given a job, tells Redis we're working on it. Useful for seeing
@@ -604,9 +718,11 @@ module Resque
604
718
  end
605
719
 
606
720
  # Returns a hash explaining the Job we're currently processing, if any.
607
- def job
608
- decode(redis.get("worker:#{self}")) || {}
721
+ def job(reload = true)
722
+ @job = nil if reload
723
+ @job ||= decode(redis.get("worker:#{self}")) || {}
609
724
  end
725
+ attr_writer :job
610
726
  alias_method :processing, :job
611
727
 
612
728
  # Boolean - true if working, false if not
@@ -620,7 +736,11 @@ module Resque
620
736
  end
621
737
 
622
738
  def will_fork?
623
- !@cant_fork && !$TESTING && (ENV["FORK_PER_JOB"] != 'false')
739
+ !@cant_fork && fork_per_job?
740
+ end
741
+
742
+ def fork_per_job?
743
+ ENV["FORK_PER_JOB"] != 'false'
624
744
  end
625
745
 
626
746
  # Returns a symbol representing the current worker state,
@@ -677,7 +797,7 @@ module Resque
677
797
  # Find Resque worker pids on Linux and OS X.
678
798
  #
679
799
  def linux_worker_pids
680
- `ps -A -o pid,command | grep "[r]esque" | grep -v "resque-web"`.split("\n").map do |line|
800
+ `ps -A -o pid,command | grep -E "[r]esque:work|[r]esque-[0-9]" | grep -v "resque-web"`.split("\n").map do |line|
681
801
  line.split(' ')[0]
682
802
  end
683
803
  end
@@ -698,14 +818,12 @@ module Resque
698
818
 
699
819
  # Given a string, sets the procline ($0) and logs.
700
820
  # Procline is always in the format of:
701
- # resque-VERSION: STRING
821
+ # RESQUE_PROCLINE_PREFIXresque-VERSION: STRING
702
822
  def procline(string)
703
- $0 = "resque-#{Resque::Version}: #{string}"
704
- log! $0
823
+ $0 = "#{ENV['RESQUE_PROCLINE_PREFIX']}resque-#{Resque::Version}: #{string}"
824
+ log_with_severity :debug, $0
705
825
  end
706
826
 
707
- # Log a message to Resque.logger
708
- # can't use alias_method since info/debug are private methods
709
827
  def log(message)
710
828
  info(message)
711
829
  end
@@ -714,26 +832,19 @@ module Resque
714
832
  debug(message)
715
833
  end
716
834
 
717
- # Deprecated legacy methods for controlling the logging threshhold
718
- # Use Resque.logger.level now, e.g.:
719
- #
720
- # Resque.logger.level = Logger::DEBUG
721
- #
835
+
722
836
  def verbose
723
- logger_severity_deprecation_warning
724
837
  @verbose
725
838
  end
726
839
 
727
840
  def very_verbose
728
- logger_severity_deprecation_warning
729
841
  @very_verbose
730
842
  end
731
843
 
732
844
  def verbose=(value);
733
- logger_severity_deprecation_warning
734
-
735
845
  if value && !very_verbose
736
846
  Resque.logger.formatter = VerboseFormatter.new
847
+ Resque.logger.level = Logger::INFO
737
848
  elsif !value
738
849
  Resque.logger.formatter = QuietFormatter.new
739
850
  end
@@ -742,11 +853,12 @@ module Resque
742
853
  end
743
854
 
744
855
  def very_verbose=(value)
745
- logger_severity_deprecation_warning
746
856
  if value
747
857
  Resque.logger.formatter = VeryVerboseFormatter.new
858
+ Resque.logger.level = Logger::DEBUG
748
859
  elsif !value && verbose
749
860
  Resque.logger.formatter = VerboseFormatter.new
861
+ Resque.logger.level = Logger::INFO
750
862
  else
751
863
  Resque.logger.formatter = QuietFormatter.new
752
864
  end
@@ -754,13 +866,10 @@ module Resque
754
866
  @very_verbose = value
755
867
  end
756
868
 
757
- def logger_severity_deprecation_warning
758
- return if $TESTING
759
- return if $warned_logger_severity_deprecation
760
- Kernel.warn "*** DEPRECATION WARNING: Resque::Worker#verbose and #very_verbose are deprecated. Please set Resque.logger.level instead"
761
- Kernel.warn "Called from: #{caller[0..5].join("\n\t")}"
762
- $warned_logger_severity_deprecation = true
763
- nil
869
+ private
870
+
871
+ def log_with_severity(severity, message)
872
+ Logging.log(severity, message)
764
873
  end
765
874
  end
766
875
  end