backburner 1.4.1 → 1.5.0

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.
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