backburner 1.4.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 174517c1affdf586cdd239cbb8ac805e1fcd1f92
4
- data.tar.gz: e26c736cdb790119a3a4b58a3cb267d1bad9edb1
3
+ metadata.gz: 9dfd7f06d2b08cb31ed2cf463506169544e0920d
4
+ data.tar.gz: 5d7585331fa5ccc082901017e9149d9982cb9de3
5
5
  SHA512:
6
- metadata.gz: 1c106cf2657f606b65ad6817197650582104bb58d22160bbda9a5bbfdae85f58c6bdd5453ffc0df491a9d88d8d6f3db7de4b4a1a87de2f6a4a73cc8561c53209
7
- data.tar.gz: b739580e0c435c2038bea4751f5e1efc964ff23381986bfbd051873f95165cb9fc0089d0e4c203854785b383ee4e17def330b0455060a68dc7ae186263aa3b64
6
+ metadata.gz: 38f1deb4da6faef87915b8fca99d99209adf76c5059f3478a75883b38a5e7abf89a9c0c9cec439641530f2b3e272362bfb0b8cc3343b9c1f9fcabf256b5bee24
7
+ data.tar.gz: d881653ea35fa55df59d53c5e104bc6dea13d857cabebacd8ecb5ba8ff845b9e736263ac7d1c386bbd6ff1d062e9abe0137ff8cf995262accd7231c7e4dc5a0b
@@ -2,6 +2,11 @@
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3
8
+ - 2.4
9
+ - 2.5
5
10
  - rbx-2
6
11
  before_install:
7
12
  - curl -L https://github.com/kr/beanstalkd/archive/v1.9.tar.gz | tar xz -C /tmp
@@ -11,11 +16,10 @@ before_install:
11
16
  - cd $TRAVIS_BUILD_DIR
12
17
  - gem update --system
13
18
  - gem update bundler
14
- install:
15
- - bundle install
16
19
  matrix:
17
20
  allow_failures:
18
21
  - rvm: rbx-2
22
+ - rvm: 2.0.0
19
23
  script:
20
24
  - bundle exec rake test
21
25
  gemfile: Gemfile
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Version 1.5.0 (September 10 2018)
4
+
5
+ * TBD
6
+
3
7
  ## Version 1.4.1 (June 10 2017)
4
8
 
5
9
  * Fix warning for constant ::Fixnum is deprecated (@amatsuda)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Backburner
1
+ # Backburner [![Build Status](https://travis-ci.org/nesquena/backburner.svg?branch=master)](https://travis-ci.org/nesquena/backburner)
2
2
 
3
3
  Backburner is a [beanstalkd](http://kr.github.com/beanstalkd/)-powered job queue that can handle a very high volume of jobs.
4
4
  You create background jobs and place them on multiple work queues to be processed later.
@@ -34,7 +34,7 @@ The real question then is... "Why Beanstalk?".
34
34
 
35
35
  Illya has an excellent blog post
36
36
  [Scalable Work Queues with Beanstalk](http://www.igvita.com/2010/05/20/scalable-work-queues-with-beanstalk/) and
37
- Adam Wiggins posted [an excellent comparison](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/).
37
+ Adam Wiggins posted [an excellent comparison](http://adam.herokuapp.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/).
38
38
 
39
39
  You will quickly see that **beanstalkd** is an underrated but incredible project that is extremely well-suited as a job queue.
40
40
  Significantly better suited for this task than Redis or a database. Beanstalk is a simple,
@@ -513,6 +513,9 @@ end
513
513
 
514
514
  Now all backburner queue errors will appear on airbrake for deeper inspection.
515
515
 
516
+ If you wish to retry a job without logging an error (for example when handling transient issues in a cloud or service oriented environment),
517
+ simply raise a `Backburner::Job::RetryJob` error.
518
+
516
519
  ### Logging
517
520
 
518
521
  Logging in backburner is rather simple. When a job is run, the log records that. When a job
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
18
18
 
19
19
  s.add_runtime_dependency 'beaneater', '~> 1.0'
20
20
  s.add_runtime_dependency 'dante', '> 0.1.5'
21
- s.add_runtime_dependency 'concurrent-ruby', '~> 1.0.1'
21
+ s.add_runtime_dependency 'concurrent-ruby', '~> 1.0', '>= 1.0.1'
22
22
 
23
23
  s.add_development_dependency 'rake'
24
24
  s.add_development_dependency 'minitest', '3.2.0'
@@ -34,7 +34,7 @@ module Backburner
34
34
  def connected?
35
35
  begin
36
36
  !!(@beanstalk && @beanstalk.connection && @beanstalk.connection.connection && !@beanstalk.connection.connection.closed?) # Would be nice if beaneater provided a connected? method
37
- rescue => e
37
+ rescue
38
38
  false
39
39
  end
40
40
  end
@@ -7,6 +7,7 @@ module Backburner
7
7
  class JobTimeout < RuntimeError; end
8
8
  class JobNotFound < RuntimeError; end
9
9
  class JobFormatInvalid < RuntimeError; end
10
+ class RetryJob < RuntimeError; end
10
11
 
11
12
  attr_accessor :task, :body, :name, :args
12
13
 
@@ -43,10 +44,10 @@ module Backburner
43
44
  #
44
45
  def process
45
46
  # Invoke before hook and stop if false
46
- res = @hooks.invoke_hook_events(job_class, :before_perform, *args)
47
+ res = @hooks.invoke_hook_events(job_name, :before_perform, *args)
47
48
  return false unless res
48
49
  # Execute the job
49
- @hooks.around_hook_events(job_class, :around_perform, *args) do
50
+ @hooks.around_hook_events(job_name, :around_perform, *args) do
50
51
  # We subtract one to ensure we timeout before beanstalkd does, except if:
51
52
  # a) ttr == 0, to support never timing out
52
53
  # b) ttr == 1, so that we don't accidentally set it to never time out
@@ -56,19 +57,19 @@ module Backburner
56
57
  end
57
58
  task.delete
58
59
  # Invoke after perform hook
59
- @hooks.invoke_hook_events(job_class, :after_perform, *args)
60
+ @hooks.invoke_hook_events(job_name, :after_perform, *args)
60
61
  rescue => e
61
- @hooks.invoke_hook_events(job_class, :on_failure, e, *args)
62
+ @hooks.invoke_hook_events(job_name, :on_failure, e, *args)
62
63
  raise e
63
64
  end
64
65
 
65
66
  def bury
66
- @hooks.invoke_hook_events(job_class, :on_bury, *args)
67
+ @hooks.invoke_hook_events(job_name, :on_bury, *args)
67
68
  task.bury
68
69
  end
69
70
 
70
71
  def retry(count, delay)
71
- @hooks.invoke_hook_events(job_class, :on_retry, count, delay, *args)
72
+ @hooks.invoke_hook_events(job_name, :on_retry, count, delay, *args)
72
73
  task.release(delay: delay)
73
74
  end
74
75
 
@@ -80,11 +81,26 @@ module Backburner
80
81
  # job_class # => NewsletterSender
81
82
  #
82
83
  def job_class
83
- handler = constantize(self.name) rescue nil
84
+ handler = try_job_class
84
85
  raise(JobNotFound, self.name) unless handler
85
86
  handler
86
87
  end
87
88
 
89
+ # Attempts to return a constantized job name, otherwise reverts to the name string
90
+ #
91
+ # @example
92
+ # job_name # => "SomeUnknownJob"
93
+ def job_name
94
+ handler = try_job_class
95
+ handler ? handler : self.name
96
+ end
97
+
98
+ def try_job_class
99
+ constantize(self.name)
100
+ rescue NameError
101
+ nil
102
+ end
103
+
88
104
  # Timeout job within specified block after given time.
89
105
  #
90
106
  # @example
@@ -10,7 +10,7 @@ module Backburner
10
10
  # Print out when a job is about to begin
11
11
  def log_job_begin(name, args)
12
12
  log_info "Work job #{name} with #{args.inspect}"
13
- @job_started_at = Time.now
13
+ Thread.current[:job_started_at] = Time.now
14
14
  end
15
15
 
16
16
  # Print out when a job completed
@@ -24,7 +24,7 @@ module Backburner
24
24
 
25
25
  # Returns true if the job logging started
26
26
  def job_started_at
27
- @job_started_at
27
+ Thread.current[:job_started_at]
28
28
  end
29
29
 
30
30
  # Print a message to stdout
@@ -50,4 +50,4 @@ module Backburner
50
50
  Backburner.configuration.logger
51
51
  end
52
52
  end
53
- end
53
+ end
@@ -1,3 +1,3 @@
1
1
  module Backburner
2
- VERSION = "1.4.1"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -140,7 +140,7 @@ module Backburner
140
140
  rescue Backburner::Job::JobFormatInvalid => e
141
141
  self.log_error self.exception_message(e)
142
142
  rescue => e # Error occurred processing job
143
- self.log_error self.exception_message(e)
143
+ self.log_error self.exception_message(e) unless e.is_a?(Backburner::Job::RetryJob)
144
144
 
145
145
  unless job
146
146
  self.log_error "Error occurred before we were able to assign a job. Giving up without retrying!"
@@ -3,9 +3,13 @@ require 'concurrent'
3
3
  module Backburner
4
4
  module Workers
5
5
  class Threading < Worker
6
+ attr_accessor :self_read, :self_write, :exit_on_shutdown
7
+
8
+ @shutdown_timeout = 10
9
+
6
10
  class << self
7
- attr_accessor :shutdown
8
11
  attr_accessor :threads_number
12
+ attr_accessor :shutdown_timeout
9
13
  end
10
14
 
11
15
  # Custom initializer just to set @tubes_data
@@ -13,6 +17,7 @@ module Backburner
13
17
  @tubes_data = {}
14
18
  super
15
19
  self.process_tube_options
20
+ @exit_on_shutdown = true
16
21
  end
17
22
 
18
23
  # Used to prepare job queues before processing jobs.
@@ -48,16 +53,19 @@ module Backburner
48
53
  connection.on_reconnect = lambda { |conn| conn.tubes.watch!(tube_name) }
49
54
 
50
55
  # Make it work jobs using its own connection per thread
51
- pool.post(connection) { |connection|
52
- loop {
56
+ pool.post(connection) do |memo_connection|
57
+ # TODO: use read-write lock?
58
+ loop do
53
59
  begin
54
- work_one_job(connection)
55
-
60
+ break if @in_shutdown
61
+ work_one_job(memo_connection)
56
62
  rescue => e
57
63
  log_error("Exception caught in thread pool loop. Continuing. -> #{e.message}\nBacktrace: #{e.backtrace}")
58
64
  end
59
- }
60
- }
65
+ end
66
+
67
+ connection.close
68
+ end
61
69
  end
62
70
  end
63
71
 
@@ -111,18 +119,44 @@ module Backburner
111
119
 
112
120
  # Wait for the shutdown signel
113
121
  def wait_for_shutdown!
114
- while !self.class.shutdown do
115
- sleep 0.5
122
+ raise Interrupt while IO.select([self_read])
123
+ rescue Interrupt
124
+ shutdown
125
+ end
126
+
127
+ def shutdown_threadpools
128
+ @thread_pools.each { |_name, pool| pool.shutdown }
129
+ shutdown_time = Time.now
130
+ @in_shutdown = true
131
+ all_shutdown = @thread_pools.all? do |_name, pool|
132
+ time_to_wait = self.class.shutdown_timeout - (Time.now - shutdown_time).to_i
133
+ pool.wait_for_termination(time_to_wait) if time_to_wait > 0
116
134
  end
135
+ rescue Interrupt
136
+ log_info "graceful shutdown aborted, shutting down immediately"
137
+ ensure
138
+ kill unless all_shutdown
139
+ end
117
140
 
118
- # Shutting down
119
- # FIXME: Shut down each thread's connection
120
- @thread_pools.each { |name, pool| pool.kill }
141
+ def kill
142
+ @thread_pools.each { |_name, pool| pool.kill unless pool.shutdown? }
121
143
  end
122
144
 
123
145
  def shutdown
124
- Backburner::Workers::Threading.shutdown = true
125
- super
146
+ log_info "beginning graceful worker shutdown"
147
+ shutdown_threadpools
148
+ super if @exit_on_shutdown
149
+ end
150
+
151
+ # Registers signal handlers TERM and INT to trigger
152
+ def register_signal_handlers!
153
+ @self_read, @self_write = IO.pipe
154
+ %w[TERM INT].each do |sig|
155
+ trap(sig) do
156
+ raise Interrupt if @in_shutdown
157
+ self_write.puts(sig)
158
+ end
159
+ end
126
160
  end
127
161
  end # Threading
128
162
  end # Workers
@@ -13,6 +13,24 @@ class TestJob
13
13
  def self.perform(x, y); $worker_test_count += x + y; end
14
14
  end
15
15
 
16
+ class TestSlowJob
17
+ include Backburner::Queue
18
+ queue_priority :medium
19
+ queue_respond_timeout 300
20
+ def self.perform(x, y); sleep 1; $worker_test_count += x + y; end
21
+ end
22
+
23
+ class TestStuckJob
24
+ include Backburner::Queue
25
+ queue_priority :medium
26
+ queue_respond_timeout 300
27
+ def self.perform(_x, _y)
28
+ loop do
29
+ sleep 0.5
30
+ end
31
+ end
32
+ end
33
+
16
34
  class TestFailJob
17
35
  include Backburner::Queue
18
36
  def self.perform(x, y); raise RuntimeError; end
@@ -113,16 +113,41 @@ describe "Backburner::Job module" do
113
113
  end # process
114
114
 
115
115
  describe "for simple delegation method" do
116
- before do
117
- @task_body = { "class" => "NestedDemo::TestJobC", "args" => [56] }
118
- @task = stub(:body => @task_body, :ttr => 120, :delete => true, :bury => true)
119
- @task.expects(:bury).once
116
+ describe "with valid class" do
117
+ before do
118
+ @task_body = { "class" => "NestedDemo::TestJobC", "args" => [56] }
119
+ @task = stub(:body => @task_body, :ttr => 120, :delete => true, :bury => true)
120
+ @task.expects(:bury).once
121
+ end
122
+
123
+ it "should call bury for task" do
124
+ @job = Backburner::Job.new(@task)
125
+ @job.bury
126
+ end # bury
120
127
  end
121
128
 
122
- it "should call bury for task" do
123
- @job = Backburner::Job.new(@task)
124
- @job.bury
125
- end # bury
129
+ describe "with invalid class" do
130
+ before do
131
+ @task_body = { "class" => "AnUnknownClass", "args" => [] }
132
+ @task = stub(:body => @task_body, :ttr => 120, :delete => true, :bury => true, :release => true)
133
+ end
134
+
135
+ it "should call bury for task" do
136
+ @task.expects(:bury).once
137
+ @job = Backburner::Job.new(@task)
138
+ Backburner::Hooks.expects(:invoke_hook_events)
139
+ .with("AnUnknownClass", :on_bury, anything)
140
+ @job.bury
141
+ end
142
+
143
+ it "should call retry for task" do
144
+ @task.expects(:release).once
145
+ @job = Backburner::Job.new(@task)
146
+ Backburner::Hooks.expects(:invoke_hook_events)
147
+ .with("AnUnknownClass", :on_retry, 0, is_a(Integer), anything)
148
+ @job.retry(0, 0)
149
+ end
150
+ end
126
151
  end # simple delegation
127
152
 
128
153
  describe "timing out for various values of ttr" do
@@ -6,7 +6,7 @@ begin
6
6
  rescue LoadError
7
7
  require 'mocha'
8
8
  end
9
- $:.unshift File.expand_path("../../lib")
9
+ $LOAD_PATH.unshift File.expand_path("lib")
10
10
  require 'backburner'
11
11
  require File.expand_path('../helpers/templogger', __FILE__)
12
12
 
@@ -11,7 +11,7 @@ describe "Backburner::Workers::Forking module" do
11
11
  describe "for prepare method" do
12
12
  it "should make tube names array always unique to avoid duplication" do
13
13
  worker = @worker_class.new(["foo", "demo.test.foo"])
14
- worker.prepare
14
+ capture_stdout { worker.prepare }
15
15
  assert_equal ["demo.test.foo"], worker.tube_names
16
16
  end
17
17
 
@@ -11,7 +11,7 @@ describe "Backburner::Workers::Simple module" do
11
11
  describe "for prepare method" do
12
12
  it "should make tube names array always unique to avoid duplication" do
13
13
  worker = @worker_class.new(["foo", "demo.test.foo"])
14
- worker.prepare
14
+ capture_stdout { worker.prepare }
15
15
  assert_equal ["demo.test.foo"], worker.tube_names
16
16
  end
17
17
 
@@ -309,7 +309,7 @@ describe "Backburner::Workers::Simple module" do
309
309
  worker = @worker_class.new('foo.bar')
310
310
  connection = mock('connection')
311
311
  worker.expects(:reserve_job).with(connection).returns(stub_everything('job'))
312
- worker.work_one_job(connection)
312
+ capture_stdout { worker.work_one_job(connection) }
313
313
  end
314
314
 
315
315
  after do
@@ -6,24 +6,25 @@ describe "Backburner::Workers::Threading worker" do
6
6
  before do
7
7
  Backburner.default_queues.clear
8
8
  @worker_class = Backburner::Workers::Threading
9
+ @worker_class.shutdown_timeout = 2
9
10
  end
10
11
 
11
12
  describe "for prepare method" do
12
13
  it "should make tube names array always unique to avoid duplication" do
13
14
  worker = @worker_class.new(["foo", "demo.test.foo"])
14
- worker.prepare
15
+ capture_stdout { worker.prepare }
15
16
  assert_equal ["demo.test.foo"], worker.tube_names
16
17
  end
17
18
 
18
19
  it 'creates a thread pool per queue' do
19
20
  worker = @worker_class.new(%w(foo bar))
20
- out = capture_stdout { worker.prepare }
21
+ capture_stdout { worker.prepare }
21
22
  assert_equal 2, worker.instance_variable_get("@thread_pools").keys.size
22
23
  end
23
24
 
24
25
  it 'uses Concurrent.processor_count if no custom thread count is provided' do
25
26
  worker = @worker_class.new("foo")
26
- out = capture_stdout { worker.prepare }
27
+ capture_stdout { worker.prepare }
27
28
  assert_equal ::Concurrent.processor_count, worker.instance_variable_get("@thread_pools")["demo.test.foo"].max_length
28
29
  end
29
30
  end # prepare
@@ -61,10 +62,43 @@ describe "Backburner::Workers::Threading worker" do
61
62
  it 'runs work_on_job per thread' do
62
63
  clear_jobs!("foo")
63
64
  job_count=10
64
- job_count.times { @worker_class.enqueue TestJob, [1, 0], :queue => "foo" } # TestJob adds the given arguments together and then to $worker_test_count
65
- @worker.start(false) # don't wait for shutdown
66
- sleep 0.5 # Wait for threads to do their work
65
+ # TestJob adds the given arguments together and then to $worker_test_count
66
+ job_count.times { @worker_class.enqueue TestJob, [1, 0], :queue => "foo" }
67
+ capture_stdout do
68
+ @worker.start(false) # don't wait for shutdown
69
+ sleep 0.5 # Wait for threads to do their work
70
+ end
67
71
  assert_equal job_count, $worker_test_count
68
72
  end
69
73
  end # working a queue
74
+
75
+ describe 'shutting down' do
76
+ before do
77
+ @thread_count = 3
78
+ @worker = @worker_class.new(["threaded-shutdown:#{@thread_count}"])
79
+ @worker.exit_on_shutdown = false
80
+ $worker_test_count = 0
81
+ clear_jobs!("threaded-shutdown")
82
+ end
83
+
84
+ it 'gracefully exits and completes all in-flight jobs' do
85
+ 6.times { @worker_class.enqueue TestSlowJob, [1, 0], :queue => "threaded-shutdown" }
86
+ Thread.new { sleep 0.1; @worker.self_write.puts("TERM") }
87
+ capture_stdout do
88
+ @worker.start
89
+ end
90
+
91
+ assert_equal @thread_count, $worker_test_count
92
+ end
93
+
94
+ it 'forces an exit when a job is stuck' do
95
+ 6.times { @worker_class.enqueue TestStuckJob, [1, 0], :queue => "threaded-shutdown" }
96
+ Thread.new { sleep 0.1; @worker.self_write.puts("TERM") }
97
+ capture_stdout do
98
+ @worker.start
99
+ end
100
+
101
+ assert_equal 0, $worker_test_count
102
+ end
103
+ end
70
104
  end
@@ -70,7 +70,7 @@ describe "Backburner::Workers::ThreadsOnFork module" do
70
70
 
71
71
  it "should make tube names array always unique to avoid duplication" do
72
72
  worker = @worker_class.new(["foo", "demo.test.foo"])
73
- worker.prepare
73
+ capture_stdout { worker.prepare }
74
74
  assert_equal ["demo.test.foo"], worker.tube_names
75
75
  end
76
76
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backburner
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Esquenazi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-10 00:00:00.000000000 Z
11
+ date: 2018-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: beaneater
@@ -43,6 +43,9 @@ dependencies:
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ - - ">="
46
49
  - !ruby/object:Gem::Version
47
50
  version: 1.0.1
48
51
  type: :runtime
@@ -50,6 +53,9 @@ dependencies:
50
53
  version_requirements: !ruby/object:Gem::Requirement
51
54
  requirements:
52
55
  - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '1.0'
58
+ - - ">="
53
59
  - !ruby/object:Gem::Version
54
60
  version: 1.0.1
55
61
  - !ruby/object:Gem::Dependency