shoryuken 5.3.0 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +17 -0
  3. data/.devcontainer/base.Dockerfile +43 -0
  4. data/.devcontainer/devcontainer.json +35 -0
  5. data/.github/dependabot.yml +6 -0
  6. data/.github/workflows/specs.yml +5 -2
  7. data/.github/workflows/stale.yml +20 -0
  8. data/Appraisals +6 -0
  9. data/CHANGELOG.md +144 -0
  10. data/Gemfile +5 -2
  11. data/README.md +19 -1
  12. data/bin/cli/sqs.rb +1 -1
  13. data/gemfiles/aws_sdk_core_2.gemfile +1 -1
  14. data/gemfiles/rails_7_0.gemfile +22 -0
  15. data/lib/shoryuken/default_exception_handler.rb +10 -0
  16. data/lib/shoryuken/environment_loader.rb +22 -3
  17. data/lib/shoryuken/extensions/active_job_adapter.rb +5 -2
  18. data/lib/shoryuken/extensions/active_job_extensions.rb +1 -1
  19. data/lib/shoryuken/launcher.rb +18 -3
  20. data/lib/shoryuken/logging.rb +2 -2
  21. data/lib/shoryuken/manager.rb +24 -9
  22. data/lib/shoryuken/middleware/server/active_record.rb +5 -1
  23. data/lib/shoryuken/options.rb +14 -6
  24. data/lib/shoryuken/polling/strict_priority.rb +4 -2
  25. data/lib/shoryuken/polling/weighted_round_robin.rb +3 -5
  26. data/lib/shoryuken/processor.rb +14 -6
  27. data/lib/shoryuken/queue.rb +5 -3
  28. data/lib/shoryuken/runner.rb +0 -3
  29. data/lib/shoryuken/version.rb +1 -1
  30. data/lib/shoryuken.rb +7 -0
  31. data/shoryuken.gemspec +1 -1
  32. data/spec/shared_examples_for_active_job.rb +4 -2
  33. data/spec/shoryuken/default_exception_handler_spec.rb +71 -0
  34. data/spec/shoryuken/environment_loader_spec.rb +42 -9
  35. data/spec/shoryuken/extensions/active_job_base_spec.rb +1 -1
  36. data/spec/shoryuken/fetcher_spec.rb +12 -12
  37. data/spec/shoryuken/launcher_spec.rb +46 -1
  38. data/spec/shoryuken/manager_spec.rb +10 -6
  39. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +31 -6
  40. data/spec/shoryuken/processor_spec.rb +38 -0
  41. data/spec/shoryuken/queue_spec.rb +10 -5
  42. data/spec/shoryuken/util_spec.rb +24 -4
  43. data/spec/shoryuken/worker/default_executor_spec.rb +48 -48
  44. data/spec/spec_helper.rb +2 -0
  45. metadata +16 -7
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- **I'm looking for Shoryuken maintainers, are you interested on helping to maintain Shoryuken? Fill up this form https://forms.gle/8kTso8ixa9Sfp6rJ9**
1
+ **I'm looking for Shoryuken maintainers, are you interested on helping to maintain Shoryuken? [Join our Slack](https://join.slack.com/t/shoryuken/shared_invite/zt-19xjq3iqc-KmoJ6eU6~qvZNqcLzIrjww)**
2
2
 
3
3
  # Shoryuken
4
4
 
@@ -86,3 +86,21 @@ To run integration specs, start a mock SQS server on `localhost:5000`. One such
86
86
  ```sh
87
87
  bundle exec rake spec:integration
88
88
  ```
89
+
90
+ ### To release a new version
91
+
92
+ Compare latest tag with HEAD:
93
+
94
+ ```sh
95
+ git log $(git describe --tags --abbrev=0)..HEAD --oneline
96
+ ```
97
+
98
+ then update CHANGELOG.md.
99
+
100
+ Update version in `lib/shoryuken/version.rb` with the appropriate version number [SEMVER](https://semver.org/).
101
+
102
+ then run:
103
+
104
+ ```sh
105
+ bundle exec rake release
106
+ ```
data/bin/cli/sqs.rb CHANGED
@@ -117,7 +117,7 @@ module Shoryuken
117
117
  max_number_of_messages: batch_size,
118
118
  attribute_names: ['All'],
119
119
  message_attribute_names: ['All']
120
- ).messages
120
+ ).messages || []
121
121
 
122
122
  messages.each { |m| yield m }
123
123
 
@@ -14,7 +14,7 @@ end
14
14
 
15
15
  group :development do
16
16
  gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
17
- gem "pry-byebug", "3.9.0"
17
+ gem "pry-byebug"
18
18
  gem "rubocop"
19
19
  end
20
20
 
@@ -0,0 +1,22 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ group :test do
6
+ gem "activejob", "~> 7.0"
7
+ gem "aws-sdk-core", "~> 3"
8
+ gem "aws-sdk-sqs"
9
+ gem "codeclimate-test-reporter", require: nil
10
+ gem "httparty"
11
+ gem "multi_xml"
12
+ gem "simplecov"
13
+ end
14
+
15
+ group :development do
16
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
17
+ gem "rubocop"
18
+ gem "pry", ">= 0.14.2"
19
+ gem "pry-byebug", ">= 3.10.1"
20
+ end
21
+
22
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ module Shoryuken
2
+ class DefaultExceptionHandler
3
+ extend Util
4
+
5
+ def self.call(exception, _queue, _sqs_msg)
6
+ logger.error { "Processor failed: #{exception.message}" }
7
+ logger.error { exception.backtrace.join("\n") } if exception.backtrace
8
+ end
9
+ end
10
+ end
@@ -18,12 +18,12 @@ module Shoryuken
18
18
  end
19
19
 
20
20
  def setup_options
21
+ initialize_rails if load_rails?
21
22
  initialize_options
22
23
  initialize_logger
23
24
  end
24
25
 
25
26
  def load
26
- load_rails if Shoryuken.options[:rails]
27
27
  prefix_active_job_queue_names
28
28
  parse_queues
29
29
  require_workers
@@ -55,7 +55,7 @@ module Shoryuken
55
55
  Shoryuken.logger.level = Logger::DEBUG if Shoryuken.options[:verbose]
56
56
  end
57
57
 
58
- def load_rails
58
+ def initialize_rails
59
59
  # Adapted from: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/cli.rb
60
60
 
61
61
  require 'rails'
@@ -70,6 +70,13 @@ module Shoryuken
70
70
  ::Rails.application.config.eager_load = true
71
71
  end
72
72
  end
73
+ ::Rails::Application.initializer 'shoryuken.set_reloader_hook' do |app|
74
+ Shoryuken.reloader = proc do |&block|
75
+ app.reloader.wrap do
76
+ block.call
77
+ end
78
+ end
79
+ end
73
80
  if Shoryuken.active_job?
74
81
  require 'shoryuken/extensions/active_job_extensions'
75
82
  require 'shoryuken/extensions/active_job_adapter'
@@ -79,6 +86,10 @@ module Shoryuken
79
86
  end
80
87
  end
81
88
 
89
+ def load_rails?
90
+ options[:rails]
91
+ end
92
+
82
93
  def prefix_active_job_queue_name(queue_name, weight)
83
94
  return [queue_name, weight] if queue_name.start_with?('https://', 'arn:')
84
95
 
@@ -159,9 +170,17 @@ module Shoryuken
159
170
 
160
171
  return if non_existent_queues.none?
161
172
 
173
+ # NOTE: HEREDOC's ~ operator removes indents, but is only available Ruby 2.3+
174
+ # See github PR: https://github.com/ruby-shoryuken/shoryuken/pull/691#issuecomment-1007653595
175
+ error_msg = <<-MSG.gsub(/^\s+/, '')
176
+ The specified queue(s) #{non_existent_queues.join(', ')} do not exist.
177
+ Try 'shoryuken sqs create QUEUE-NAME' for creating a queue with default settings.
178
+ It's also possible that you don't have permission to access the specified queues.
179
+ MSG
180
+
162
181
  fail(
163
182
  ArgumentError,
164
- "The specified queue(s) #{non_existent_queues.join(', ')} do not exist.\nTry 'shoryuken sqs create QUEUE-NAME' for creating a queue with default settings"
183
+ error_msg
165
184
  )
166
185
  end
167
186
 
@@ -66,8 +66,11 @@ module ActiveJob
66
66
  }
67
67
 
68
68
  if queue.fifo?
69
- # See https://github.com/phstc/shoryuken/issues/457
70
- msg[:message_deduplication_id] = Digest::SHA256.hexdigest(JSON.dump(body.except('job_id')))
69
+ # See https://github.com/ruby-shoryuken/shoryuken/issues/457 and
70
+ # https://github.com/ruby-shoryuken/shoryuken/pull/750#issuecomment-1781317929
71
+ msg[:message_deduplication_id] = Digest::SHA256.hexdigest(
72
+ JSON.dump(body.except('job_id', 'enqueued_at'))
73
+ )
71
74
  end
72
75
 
73
76
  msg.merge(job_params.except(:message_attributes))
@@ -11,7 +11,7 @@ module Shoryuken
11
11
  end
12
12
  end
13
13
 
14
- # Initializes SQS SendMessage parameters on instances of ActiveJobe::Base
14
+ # Initializes SQS SendMessage parameters on instances of ActiveJob::Base
15
15
  # to the empty hash, and populates it whenever `#enqueue` is called, such
16
16
  # as when using ActiveJob::Base.set.
17
17
  module SQSSendMessageParametersSupport
@@ -16,11 +16,13 @@ module Shoryuken
16
16
  def stop!
17
17
  initiate_stop
18
18
 
19
- executor.shutdown
19
+ # Don't await here so the timeout below is not delayed
20
+ stop_new_dispatching
20
21
 
21
- return if executor.wait_for_termination(Shoryuken.options[:timeout])
22
+ executor.shutdown
23
+ executor.kill unless executor.wait_for_termination(Shoryuken.options[:timeout])
22
24
 
23
- executor.kill
25
+ fire_event(:stopped)
24
26
  end
25
27
 
26
28
  def stop
@@ -28,8 +30,13 @@ module Shoryuken
28
30
 
29
31
  initiate_stop
30
32
 
33
+ stop_new_dispatching
34
+ await_dispatching_in_progress
35
+
31
36
  executor.shutdown
32
37
  executor.wait_for_termination
38
+
39
+ fire_event(:stopped)
33
40
  end
34
41
 
35
42
  def healthy?
@@ -41,6 +48,14 @@ module Shoryuken
41
48
 
42
49
  private
43
50
 
51
+ def stop_new_dispatching
52
+ @managers.each(&:stop_new_dispatching)
53
+ end
54
+
55
+ def await_dispatching_in_progress
56
+ @managers.each(&:await_dispatching_in_progress)
57
+ end
58
+
44
59
  def executor
45
60
  @_executor ||= Shoryuken.launcher_executor || Concurrent.global_io_executor
46
61
  end
@@ -30,11 +30,11 @@ module Shoryuken
30
30
  end
31
31
 
32
32
  def self.logger
33
- @logger || initialize_logger
33
+ @logger ||= initialize_logger
34
34
  end
35
35
 
36
36
  def self.logger=(log)
37
- @logger = (log ? log : Logger.new('/dev/null'))
37
+ @logger = (log || Logger.new('/dev/null'))
38
38
  end
39
39
  end
40
40
  end
@@ -9,13 +9,15 @@ module Shoryuken
9
9
  attr_reader :group
10
10
 
11
11
  def initialize(group, fetcher, polling_strategy, concurrency, executor)
12
- @group = group
13
- @fetcher = fetcher
14
- @polling_strategy = polling_strategy
15
- @max_processors = concurrency
16
- @busy_processors = Concurrent::AtomicFixnum.new(0)
17
- @executor = executor
18
- @running = Concurrent::AtomicBoolean.new(true)
12
+ @group = group
13
+ @fetcher = fetcher
14
+ @polling_strategy = polling_strategy
15
+ @max_processors = concurrency
16
+ @busy_processors = Concurrent::AtomicFixnum.new(0)
17
+ @executor = executor
18
+ @running = Concurrent::AtomicBoolean.new(true)
19
+ @stop_new_dispatching = Concurrent::AtomicBoolean.new(false)
20
+ @dispatching_release_signal = ::Queue.new
19
21
  end
20
22
 
21
23
  def start
@@ -23,6 +25,17 @@ module Shoryuken
23
25
  dispatch_loop
24
26
  end
25
27
 
28
+ def stop_new_dispatching
29
+ @stop_new_dispatching.make_true
30
+ end
31
+
32
+ def await_dispatching_in_progress
33
+ # There might still be a dispatching on-going, as the response from SQS could take some time
34
+ # We don't want to stop the process before processing incoming messages, as they would stay "in-flight" for some time on SQS
35
+ # We use a queue, as the dispatch_loop is running on another thread, and this is a efficient way of communicating between threads.
36
+ @dispatching_release_signal.pop
37
+ end
38
+
26
39
  def running?
27
40
  @running.true? && @executor.running?
28
41
  end
@@ -30,7 +43,10 @@ module Shoryuken
30
43
  private
31
44
 
32
45
  def dispatch_loop
33
- return unless running?
46
+ if @stop_new_dispatching.true? || !running?
47
+ @dispatching_release_signal << 1
48
+ return
49
+ end
34
50
 
35
51
  @executor.post { dispatch }
36
52
  end
@@ -94,7 +110,6 @@ module Shoryuken
94
110
 
95
111
  def dispatch_single_messages(queue)
96
112
  messages = @fetcher.fetch(queue, ready)
97
-
98
113
  @polling_strategy.messages_found(queue.name, messages.size)
99
114
  messages.each { |message| assign(queue.name, message) }
100
115
  end
@@ -5,7 +5,11 @@ module Shoryuken
5
5
  def call(*_args)
6
6
  yield
7
7
  ensure
8
- ::ActiveRecord::Base.clear_active_connections!
8
+ if ::ActiveRecord.version >= Gem::Version.new('7.1')
9
+ ::ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
10
+ else
11
+ ::ActiveRecord::Base.clear_active_connections!
12
+ end
9
13
  end
10
14
  end
11
15
  end
@@ -11,22 +11,26 @@ module Shoryuken
11
11
  dispatch: [],
12
12
  utilization_update: [],
13
13
  quiet: [],
14
- shutdown: []
14
+ shutdown: [],
15
+ stopped: []
15
16
  }
16
17
  }.freeze
17
18
 
18
19
  attr_accessor :active_job_queue_name_prefixing, :cache_visibility_timeout, :groups,
19
- :launcher_executor,
20
- :start_callback, :stop_callback, :worker_executor, :worker_registry
20
+ :launcher_executor, :reloader, :enable_reloading,
21
+ :start_callback, :stop_callback, :worker_executor, :worker_registry, :exception_handlers
21
22
  attr_writer :default_worker_options, :sqs_client
22
23
  attr_reader :sqs_client_receive_message_opts
23
24
 
24
25
  def initialize
25
26
  self.groups = {}
26
27
  self.worker_registry = DefaultWorkerRegistry.new
28
+ self.exception_handlers = [DefaultExceptionHandler]
27
29
  self.active_job_queue_name_prefixing = false
28
30
  self.worker_executor = Worker::DefaultExecutor
29
31
  self.cache_visibility_timeout = false
32
+ self.reloader = proc { |&block| block.call }
33
+ self.enable_reloading ||= false
30
34
  # this is needed for keeping backward compatibility
31
35
  @sqs_client_receive_message_opts ||= {}
32
36
  end
@@ -63,10 +67,14 @@ module Shoryuken
63
67
  Polling::WeightedRoundRobin
64
68
  when 'StrictPriority'
65
69
  Polling::StrictPriority
70
+ when String
71
+ begin
72
+ Object.const_get(strategy)
73
+ rescue NameError
74
+ raise ArgumentError, "#{strategy} is not a valid polling_strategy"
75
+ end
66
76
  when Class
67
77
  strategy
68
- else
69
- raise ArgumentError, "#{strategy} is not a valid polling_strategy"
70
78
  end
71
79
  end
72
80
 
@@ -134,7 +142,7 @@ module Shoryuken
134
142
  end
135
143
 
136
144
  # Register a block to run at a point in the Shoryuken lifecycle.
137
- # :startup, :quiet or :shutdown are valid events.
145
+ # :startup, :quiet, :shutdown or :stopped are valid events.
138
146
  #
139
147
  # Shoryuken.configure_server do |config|
140
148
  # config.on(:shutdown) do
@@ -39,8 +39,10 @@ module Shoryuken
39
39
  end
40
40
 
41
41
  def message_processed(queue)
42
- logger.debug "Unpausing #{queue}"
43
- @paused_until[queue] = Time.now
42
+ if queue_paused?(queue)
43
+ logger.debug "Unpausing #{queue}"
44
+ @paused_until[queue] = Time.at 0
45
+ end
44
46
  end
45
47
 
46
48
  private
@@ -36,12 +36,10 @@ module Shoryuken
36
36
  end
37
37
 
38
38
  def message_processed(queue)
39
- return if @paused_queues.empty?
39
+ paused_queue = @paused_queues.find { |_time, name| name == queue }
40
+ return unless paused_queue
40
41
 
41
- logger.debug "Unpausing #{queue}"
42
- @paused_queues.reject! { |_time, name| name == queue }
43
- @queues << queue
44
- @queues.uniq!
42
+ paused_queue[0] = Time.at 0
45
43
  end
46
44
 
47
45
  private
@@ -14,16 +14,24 @@ module Shoryuken
14
14
  end
15
15
 
16
16
  def process
17
- return logger.error { "No worker found for #{queue}" } unless worker
17
+ worker_perform = proc do
18
+ return logger.error { "No worker found for #{queue}" } unless worker
19
+ Shoryuken::Logging.with_context("#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.message_id}") do
20
+ worker.class.server_middleware.invoke(worker, queue, sqs_msg, body) do
21
+ worker.perform(sqs_msg, body)
22
+ end
23
+ end
24
+ end
18
25
 
19
- Shoryuken::Logging.with_context("#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.message_id}") do
20
- worker.class.server_middleware.invoke(worker, queue, sqs_msg, body) do
21
- worker.perform(sqs_msg, body)
26
+ if Shoryuken.enable_reloading
27
+ Shoryuken.reloader.call do
28
+ worker_perform.call
22
29
  end
30
+ else
31
+ worker_perform.call
23
32
  end
24
33
  rescue Exception => ex
25
- logger.error { "Processor failed: #{ex.message}" }
26
- logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?
34
+ Array(Shoryuken.exception_handlers).each { |handler| handler.call(ex, queue, sqs_msg) }
27
35
 
28
36
  raise
29
37
  end
@@ -21,9 +21,10 @@ module Shoryuken
21
21
  end
22
22
 
23
23
  def delete_messages(options)
24
- client.delete_message_batch(
24
+ failed_messages = client.delete_message_batch(
25
25
  options.merge(queue_url: url)
26
- ).failed.any? do |failure|
26
+ ).failed || []
27
+ failed_messages.any? do |failure|
27
28
  logger.error do
28
29
  "Could not delete #{failure.id}, code: '#{failure.code}', message: '#{failure.message}', sender_fault: #{failure.sender_fault}"
29
30
  end
@@ -43,7 +44,8 @@ module Shoryuken
43
44
  end
44
45
 
45
46
  def receive_messages(options)
46
- client.receive_message(options.merge(queue_url: url)).messages.map { |m| Message.new(client, self, m) }
47
+ messages = client.receive_message(options.merge(queue_url: url)).messages || []
48
+ messages.map { |m| Message.new(client, self, m) }
47
49
  end
48
50
 
49
51
  def fifo?
@@ -30,9 +30,6 @@ module Shoryuken
30
30
 
31
31
  loader = EnvironmentLoader.setup_options(options)
32
32
 
33
- # When cli args exist, override options in config file
34
- Shoryuken.options.merge!(options)
35
-
36
33
  daemonize(Shoryuken.options)
37
34
  write_pid(Shoryuken.options)
38
35
 
@@ -1,3 +1,3 @@
1
1
  module Shoryuken
2
- VERSION = '5.3.0'.freeze
2
+ VERSION = '6.2.1'.freeze
3
3
  end
data/lib/shoryuken.rb CHANGED
@@ -23,6 +23,7 @@ require 'shoryuken/worker/default_executor'
23
23
  require 'shoryuken/worker/inline_executor'
24
24
  require 'shoryuken/worker_registry'
25
25
  require 'shoryuken/default_worker_registry'
26
+ require 'shoryuken/default_exception_handler'
26
27
  require 'shoryuken/middleware/chain'
27
28
  require 'shoryuken/middleware/server/auto_delete'
28
29
  Shoryuken::Middleware::Server.autoload :AutoExtendVisibility, 'shoryuken/middleware/server/auto_extend_visibility'
@@ -73,6 +74,8 @@ module Shoryuken
73
74
  :sqs_client=,
74
75
  :sqs_client_receive_message_opts,
75
76
  :sqs_client_receive_message_opts=,
77
+ :exception_handlers,
78
+ :exception_handlers=,
76
79
  :options,
77
80
  :logger,
78
81
  :register_worker,
@@ -88,6 +91,10 @@ module Shoryuken
88
91
  :on,
89
92
  :cache_visibility_timeout?,
90
93
  :cache_visibility_timeout=,
94
+ :reloader,
95
+ :reloader=,
96
+ :enable_reloading,
97
+ :enable_reloading=,
91
98
  :delay
92
99
  )
93
100
  end
data/shoryuken.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Pablo Cantero']
10
10
  spec.email = ['pablo@pablocantero.com']
11
11
  spec.description = spec.summary = 'Shoryuken is a super efficient AWS SQS thread based message processor'
12
- spec.homepage = 'https://github.com/phstc/shoryuken'
12
+ spec.homepage = 'https://github.com/ruby-shoryuken/shoryuken'
13
13
  spec.license = 'LGPL-3.0'
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0")
@@ -42,9 +42,11 @@ RSpec.shared_examples 'active_job_adapters' do
42
42
  context 'when fifo' do
43
43
  let(:fifo) { true }
44
44
 
45
- it 'does not include job_id in the deduplication_id' do
45
+ it 'does not include job_id and enqueued_at in the deduplication_id' do
46
46
  expect(queue).to receive(:send_message) do |hash|
47
- message_deduplication_id = Digest::SHA256.hexdigest(JSON.dump(job.serialize.except('job_id')))
47
+ message_deduplication_id = Digest::SHA256.hexdigest(
48
+ JSON.dump(job.serialize.except('job_id', 'enqueued_at'))
49
+ )
48
50
 
49
51
  expect(hash[:message_deduplication_id]).to eq(message_deduplication_id)
50
52
  end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ RSpec.describe Shoryuken::DefaultExceptionHandler do
5
+ class CustomErrorHandler
6
+ extend Shoryuken::Util
7
+
8
+ def self.call(_ex, queue, _msg)
9
+ logger.error("#{queue.to_s} failed to process the message")
10
+ end
11
+ end
12
+
13
+ before do
14
+ Shoryuken.worker_executor = Shoryuken::Worker::InlineExecutor
15
+ allow(manager).to receive(:async).and_return(manager)
16
+ allow(manager).to receive(:real_thread)
17
+ allow(Shoryuken::Client).to receive(:queues).with(queue).and_return(sqs_queue)
18
+ end
19
+
20
+ after do
21
+ Shoryuken.worker_executor = Shoryuken::Worker::DefaultExecutor
22
+ end
23
+
24
+ let(:manager) { double Shoryuken::Manager }
25
+ let(:sqs_queue) { double Shoryuken::Queue, visibility_timeout: 30 }
26
+ let(:queue) { 'default' }
27
+
28
+ let(:sqs_msg) do
29
+ double(
30
+ Shoryuken::Message,
31
+ queue_url: queue,
32
+ body: 'test',
33
+ message_attributes: {},
34
+ message_id: SecureRandom.uuid,
35
+ receipt_handle: SecureRandom.uuid
36
+ )
37
+ end
38
+
39
+ subject { Shoryuken::Processor.new(queue, sqs_msg) }
40
+
41
+ context "with default handler" do
42
+ before do
43
+ Shoryuken.exception_handlers = described_class
44
+ end
45
+
46
+ it "logs an error message" do
47
+ expect(Shoryuken::Logging.logger).to receive(:error).twice
48
+
49
+ allow_any_instance_of(TestWorker).to receive(:perform).and_raise(StandardError, "error")
50
+ allow(sqs_msg).to receive(:body)
51
+
52
+ expect { subject.process }.to raise_error(StandardError)
53
+ end
54
+ end
55
+
56
+ context "with custom handler" do
57
+ before do
58
+ Shoryuken.exception_handlers = [described_class, CustomErrorHandler]
59
+ end
60
+
61
+ it "logs default and custom error messages" do
62
+ expect(Shoryuken::Logging.logger).to receive(:error).twice
63
+ expect(Shoryuken::Logging.logger).to receive(:error).with("default failed to process the message").once
64
+
65
+ allow_any_instance_of(TestWorker).to receive(:perform).and_raise(StandardError, "error")
66
+ allow(sqs_msg).to receive(:body)
67
+
68
+ expect { subject.process }.to raise_error(StandardError)
69
+ end
70
+ end
71
+ end