resque 1.21.0 → 1.22.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.

data/HISTORY.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 1.22.0 (2012-08-21)
2
+
3
+ * unregister signal handlers in child process when ENV["TERM_CHILD"] is set (@dylanasmith, #621)
4
+ * new signal handling for TERM. See http://hone.heroku.com/resque/2012/08/21/resque-signals.html. (@wuputah, @yaaule, #638)
5
+ * supports calling perform hooks when using Resque.inline (@jonhyman, #506)
6
+
1
7
  ## 1.21.0 (2012-07-02)
2
8
 
3
9
  * Add a flag to make sure failure hooks are only ran once (jakemack, #546)
@@ -7,4 +7,7 @@ module Resque
7
7
 
8
8
  # Raised when a worker was killed while processing a job.
9
9
  class DirtyExit < RuntimeError; end
10
+
11
+ # Raised when child process is TERM'd so job can rescue this to do shutdown work.
12
+ class TermException < SignalException; end
10
13
  end
@@ -44,7 +44,9 @@ module Resque
44
44
  Resque.validate(klass, queue)
45
45
 
46
46
  if Resque.inline?
47
- constantize(klass).perform(*decode(encode(args)))
47
+ # Instantiating a Resque::Job and calling perform on it so callbacks run
48
+ # decode(encode(args)) to ensure that args are normalized in the same manner as a non-inline job
49
+ new(:inline, {'class' => klass, 'args' => decode(encode(args))}).perform
48
50
  else
49
51
  Resque.push(queue, :class => klass.to_s, :args => args)
50
52
  end
@@ -14,6 +14,8 @@ namespace :resque do
14
14
  worker = Resque::Worker.new(*queues)
15
15
  worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
16
16
  worker.very_verbose = ENV['VVERBOSE']
17
+ worker.term_timeout = ENV['RESQUE_TERM_TIMEOUT'] || 4.0
18
+ worker.term_child = ENV['TERM_CHILD']
17
19
  rescue Resque::NoQueueError
18
20
  abort "set QUEUE env var, e.g. $ QUEUE=critical,high rake resque:work"
19
21
  end
@@ -1,3 +1,3 @@
1
1
  module Resque
2
- Version = VERSION = '1.21.0'
2
+ Version = VERSION = '1.22.0'
3
3
  end
@@ -20,6 +20,11 @@ module Resque
20
20
  # Automatically set if a fork(2) fails.
21
21
  attr_accessor :cant_fork
22
22
 
23
+ attr_accessor :term_timeout
24
+
25
+ # decide whether to use new_kill_child logic
26
+ attr_accessor :term_child
27
+
23
28
  attr_writer :to_s
24
29
 
25
30
  # Returns an array of all worker objects.
@@ -137,8 +142,13 @@ module Resque
137
142
  if @child = fork
138
143
  srand # Reseeding
139
144
  procline "Forked #{@child} at #{Time.now.to_i}"
140
- Process.wait(@child)
145
+ begin
146
+ Process.waitpid(@child)
147
+ rescue SystemCallError
148
+ nil
149
+ end
141
150
  else
151
+ unregister_signal_handlers if !@cant_fork && term_child
142
152
  procline "Processing #{job.queue} since #{Time.now.to_i}"
143
153
  redis.client.reconnect # Don't share connection with parent
144
154
  perform(job, &block)
@@ -238,6 +248,7 @@ module Resque
238
248
 
239
249
  # Runs all the methods needed when a worker begins its lifecycle.
240
250
  def startup
251
+ 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
241
252
  enable_gc_optimizations
242
253
  register_signal_handlers
243
254
  prune_dead_workers
@@ -271,7 +282,11 @@ module Resque
271
282
 
272
283
  begin
273
284
  trap('QUIT') { shutdown }
274
- trap('USR1') { kill_child }
285
+ if term_child
286
+ trap('USR1') { new_kill_child }
287
+ else
288
+ trap('USR1') { kill_child }
289
+ end
275
290
  trap('USR2') { pause_processing }
276
291
  trap('CONT') { unpause_processing }
277
292
  rescue ArgumentError
@@ -281,6 +296,18 @@ module Resque
281
296
  log! "Registered signals"
282
297
  end
283
298
 
299
+ def unregister_signal_handlers
300
+ trap('TERM') { raise TermException.new("SIGTERM") }
301
+ trap('INT', 'DEFAULT')
302
+
303
+ begin
304
+ trap('QUIT', 'DEFAULT')
305
+ trap('USR1', 'DEFAULT')
306
+ trap('USR2', 'DEFAULT')
307
+ rescue ArgumentError
308
+ end
309
+ end
310
+
284
311
  # Schedule this worker for shutdown. Will finish processing the
285
312
  # current job.
286
313
  def shutdown
@@ -291,7 +318,11 @@ module Resque
291
318
  # Kill the child and shutdown immediately.
292
319
  def shutdown!
293
320
  shutdown
294
- kill_child
321
+ if term_child
322
+ new_kill_child
323
+ else
324
+ kill_child
325
+ end
295
326
  end
296
327
 
297
328
  # Should this worker shutdown as soon as current job is finished?
@@ -313,6 +344,28 @@ module Resque
313
344
  end
314
345
  end
315
346
 
347
+ # Kills the forked child immediately with minimal remorse. The job it
348
+ # is processing will not be completed. Send the child a TERM signal,
349
+ # wait 5 seconds, and then a KILL signal if it has not quit
350
+ def new_kill_child
351
+ if @child
352
+ unless Process.waitpid(@child, Process::WNOHANG)
353
+ log! "Sending TERM signal to child #{@child}"
354
+ Process.kill("TERM", @child)
355
+ (term_timeout.to_f * 10).round.times do |i|
356
+ sleep(0.1)
357
+ return if Process.waitpid(@child, Process::WNOHANG)
358
+ end
359
+ log! "Sending KILL signal to child #{@child}"
360
+ Process.kill("KILL", @child)
361
+ else
362
+ log! "Child #{@child} already quit."
363
+ end
364
+ end
365
+ rescue SystemCallError
366
+ log! "Child #{@child} already quit and reaped."
367
+ end
368
+
316
369
  # are we paused?
317
370
  def paused?
318
371
  @paused
Binary file
@@ -420,4 +420,45 @@ context "Resque::Job all hooks" do
420
420
  "oh no"
421
421
  ]
422
422
  end
423
+
424
+ class ::CallbacksInline
425
+ @queue = :callbacks_inline
426
+
427
+ def self.before_perform_record_history(history, count)
428
+ history << :before_perform
429
+ count['count'] += 1
430
+ end
431
+
432
+ def self.after_perform_record_history(history, count)
433
+ history << :after_perform
434
+ count['count'] += 1
435
+ end
436
+
437
+ def self.around_perform_record_history(history, count)
438
+ history << :start_around_perform
439
+ count['count'] += 1
440
+ yield
441
+ history << :finish_around_perform
442
+ count['count'] += 1
443
+ end
444
+
445
+ def self.perform(history, count)
446
+ history << :perform
447
+ $history = history
448
+ $count = count
449
+ end
450
+ end
451
+
452
+ test "it runs callbacks when inline is true" do
453
+ begin
454
+ Resque.inline = true
455
+ # Sending down two parameters that can be passed and updated by reference
456
+ result = Resque.enqueue(CallbacksInline, [], {'count' => 0})
457
+ assert_equal true, result, "perform returned true"
458
+ assert_equal $history, [:before_perform, :start_around_perform, :perform, :finish_around_perform, :after_perform]
459
+ assert_equal 4, $count['count']
460
+ ensure
461
+ Resque.inline = false
462
+ end
463
+ end
423
464
  end
@@ -1,7 +1,7 @@
1
1
  require 'rubygems'
2
2
 
3
- dir = File.dirname(File.expand_path(__FILE__))
4
- $LOAD_PATH.unshift dir + '/../lib'
3
+ $dir = File.dirname(File.expand_path(__FILE__))
4
+ $LOAD_PATH.unshift $dir + '/../lib'
5
5
  $TESTING = true
6
6
  require 'test/unit'
7
7
 
@@ -42,21 +42,21 @@ at_exit do
42
42
  processes = `ps -A -o pid,command | grep [r]edis-test`.split("\n")
43
43
  pids = processes.map { |process| process.split(" ")[0] }
44
44
  puts "Killing test redis server..."
45
- `rm -f #{dir}/dump.rdb #{dir}/dump-cluster.rdb`
46
- pids.each { |pid| Process.kill("KILL", pid.to_i) }
45
+ pids.each { |pid| Process.kill("TERM", pid.to_i) }
46
+ system("rm -f #{$dir}/dump.rdb #{$dir}/dump-cluster.rdb")
47
47
  exit exit_code
48
48
  end
49
49
 
50
50
  if ENV.key? 'RESQUE_DISTRIBUTED'
51
51
  require 'redis/distributed'
52
52
  puts "Starting redis for testing at localhost:9736 and localhost:9737..."
53
- `redis-server #{dir}/redis-test.conf`
54
- `redis-server #{dir}/redis-test-cluster.conf`
53
+ `redis-server #{$dir}/redis-test.conf`
54
+ `redis-server #{$dir}/redis-test-cluster.conf`
55
55
  r = Redis::Distributed.new(['redis://localhost:9736', 'redis://localhost:9737'])
56
56
  Resque.redis = Redis::Namespace.new :resque, :redis => r
57
57
  else
58
58
  puts "Starting redis for testing at localhost:9736..."
59
- `redis-server #{dir}/redis-test.conf`
59
+ `redis-server #{$dir}/redis-test.conf`
60
60
  Resque.redis = 'localhost:9736'
61
61
  end
62
62
 
@@ -144,6 +144,8 @@ ensure
144
144
  Resque::Failure.backend = previous_backend
145
145
  end
146
146
 
147
+ require 'time'
148
+
147
149
  class Time
148
150
  # Thanks, Timecop
149
151
  class << self
@@ -158,3 +160,15 @@ class Time
158
160
 
159
161
  self.fake_time = nil
160
162
  end
163
+
164
+ def capture_stderr
165
+ # The output stream must be an IO-like object. In this case we capture it in
166
+ # an in-memory IO object so we can return the string value. You can assign any
167
+ # IO object here.
168
+ previous_stderr, $stderr = $stderr, StringIO.new
169
+ yield
170
+ $stderr.string
171
+ ensure
172
+ # Restore the previous value of stderr (typically equal to STDERR).
173
+ $stderr = previous_stderr
174
+ end
@@ -286,7 +286,7 @@ context "Resque::Worker" do
286
286
  test "knows when it started" do
287
287
  time = Time.now
288
288
  @worker.work(0) do
289
- assert_equal time.to_s, @worker.started.to_s
289
+ assert Time.parse(@worker.started) - time < 0.1
290
290
  end
291
291
  end
292
292
 
@@ -437,4 +437,152 @@ context "Resque::Worker" do
437
437
  @worker.work(0)
438
438
  assert_not_equal original_connection, Resque.redis.client.connection.instance_variable_get("@sock")
439
439
  end
440
+
441
+ if !defined?(RUBY_ENGINE) || defined?(RUBY_ENGINE) && RUBY_ENGINE != "jruby"
442
+ test "old signal handling is the default" do
443
+ rescue_time = nil
444
+
445
+ begin
446
+ class LongRunningJob
447
+ @queue = :long_running_job
448
+
449
+ def self.perform( run_time, rescue_time=nil )
450
+ Resque.redis.client.reconnect # get its own connection
451
+ Resque.redis.rpush( 'sigterm-test:start', Process.pid )
452
+ sleep run_time
453
+ Resque.redis.rpush( 'sigterm-test:result', 'Finished Normally' )
454
+ rescue Resque::TermException => e
455
+ Resque.redis.rpush( 'sigterm-test:result', %Q(Caught SignalException: #{e.inspect}))
456
+ sleep rescue_time unless rescue_time.nil?
457
+ ensure
458
+ puts 'fuuuu'
459
+ Resque.redis.rpush( 'sigterm-test:final', 'exiting.' )
460
+ end
461
+ end
462
+
463
+ Resque.enqueue( LongRunningJob, 5, rescue_time )
464
+
465
+ worker_pid = Kernel.fork do
466
+ # ensure we actually fork
467
+ $TESTING = false
468
+ # reconnect since we just forked
469
+ Resque.redis.client.reconnect
470
+
471
+ worker = Resque::Worker.new(:long_running_job)
472
+
473
+ worker.work(0)
474
+ exit!
475
+ end
476
+
477
+ # ensure the worker is started
478
+ start_status = Resque.redis.blpop( 'sigterm-test:start', 5 )
479
+ assert_not_nil start_status
480
+ child_pid = start_status[1].to_i
481
+ assert_operator child_pid, :>, 0
482
+
483
+ # send signal to abort the worker
484
+ Process.kill('TERM', worker_pid)
485
+ Process.waitpid(worker_pid)
486
+
487
+ # wait to see how it all came down
488
+ result = Resque.redis.blpop( 'sigterm-test:result', 5 )
489
+ assert_nil result
490
+
491
+ # ensure that the child pid is no longer running
492
+ child_still_running = !(`ps -p #{child_pid.to_s} -o pid=`).empty?
493
+ assert !child_still_running
494
+ ensure
495
+ remaining_keys = Resque.redis.keys('sigterm-test:*') || []
496
+ Resque.redis.del(*remaining_keys) unless remaining_keys.empty?
497
+ end
498
+ end
499
+ end
500
+
501
+ if !defined?(RUBY_ENGINE) || defined?(RUBY_ENGINE) && RUBY_ENGINE != "jruby"
502
+ [SignalException, Resque::TermException].each do |exception|
503
+ {
504
+ 'cleanup occurs in allotted time' => nil,
505
+ 'cleanup takes too long' => 2
506
+ }.each do |scenario,rescue_time|
507
+ test "SIGTERM when #{scenario} while catching #{exception}" do
508
+ begin
509
+ eval("class LongRunningJob; @@exception = #{exception}; end")
510
+ class LongRunningJob
511
+ @queue = :long_running_job
512
+
513
+ def self.perform( run_time, rescue_time=nil )
514
+ Resque.redis.client.reconnect # get its own connection
515
+ Resque.redis.rpush( 'sigterm-test:start', Process.pid )
516
+ sleep run_time
517
+ Resque.redis.rpush( 'sigterm-test:result', 'Finished Normally' )
518
+ rescue @@exception => e
519
+ Resque.redis.rpush( 'sigterm-test:result', %Q(Caught SignalException: #{e.inspect}))
520
+ sleep rescue_time unless rescue_time.nil?
521
+ ensure
522
+ Resque.redis.rpush( 'sigterm-test:final', 'exiting.' )
523
+ end
524
+ end
525
+
526
+ Resque.enqueue( LongRunningJob, 5, rescue_time )
527
+
528
+ worker_pid = Kernel.fork do
529
+ # ensure we actually fork
530
+ $TESTING = false
531
+ # reconnect since we just forked
532
+ Resque.redis.client.reconnect
533
+
534
+ worker = Resque::Worker.new(:long_running_job)
535
+ worker.term_timeout = 1
536
+ worker.term_child = 1
537
+
538
+ worker.work(0)
539
+ exit!
540
+ end
541
+
542
+ # ensure the worker is started
543
+ start_status = Resque.redis.blpop( 'sigterm-test:start', 5 )
544
+ assert_not_nil start_status
545
+ child_pid = start_status[1].to_i
546
+ assert_operator child_pid, :>, 0
547
+
548
+ # send signal to abort the worker
549
+ Process.kill('TERM', worker_pid)
550
+ Process.waitpid(worker_pid)
551
+
552
+ # wait to see how it all came down
553
+ result = Resque.redis.blpop( 'sigterm-test:result', 5 )
554
+ assert_not_nil result
555
+ assert !result[1].start_with?('Finished Normally'), 'Job Finished normally. Sleep not long enough?'
556
+ assert result[1].start_with? 'Caught SignalException', 'Signal exception not raised in child.'
557
+
558
+ # ensure that the child pid is no longer running
559
+ child_still_running = !(`ps -p #{child_pid.to_s} -o pid=`).empty?
560
+ assert !child_still_running
561
+
562
+ # see if post-cleanup occurred. This should happen IFF the rescue_time is less than the term_timeout
563
+ post_cleanup_occurred = Resque.redis.lpop( 'sigterm-test:final' )
564
+ assert post_cleanup_occurred, 'post cleanup did not occur. SIGKILL sent too early?' if rescue_time.nil?
565
+ assert !post_cleanup_occurred, 'post cleanup occurred. SIGKILL sent too late?' unless rescue_time.nil?
566
+
567
+ ensure
568
+ remaining_keys = Resque.redis.keys('sigterm-test:*') || []
569
+ Resque.redis.del(*remaining_keys) unless remaining_keys.empty?
570
+ end
571
+ end
572
+ end
573
+ end
574
+
575
+ test "displays warning when not using term_child" do
576
+ stderr = capture_stderr { @worker.work(0) }
577
+
578
+ assert stderr.match(/^WARNING:/)
579
+ end
580
+
581
+ test "it does not display warning when using term_child" do
582
+ @worker.term_child = "1"
583
+ stderr = capture_stderr { @worker.work(0) }
584
+
585
+ assert !stderr.match(/^WARNING:/)
586
+ end
587
+ end
440
588
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque
3
3
  version: !ruby/object:Gem::Version
4
- hash: 67
4
+ hash: 79
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
- - 21
8
+ - 22
9
9
  - 0
10
- version: 1.21.0
10
+ version: 1.22.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Chris Wanstrath
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2012-07-03 00:00:00 Z
19
+ date: 2012-08-21 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  name: redis-namespace
@@ -151,6 +151,7 @@ files:
151
151
  - test/test_helper.rb
152
152
  - test/hoptoad_test.rb
153
153
  - test/resque_test.rb
154
+ - test/dump.rdb
154
155
  - test/job_plugins_test.rb
155
156
  homepage: http://github.com/defunkt/resque
156
157
  licenses: []
@@ -181,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
182
  requirements: []
182
183
 
183
184
  rubyforge_project:
184
- rubygems_version: 1.8.21
185
+ rubygems_version: 1.8.24
185
186
  signing_key:
186
187
  specification_version: 3
187
188
  summary: Resque is a Redis-backed queueing system.