shoryuken 5.3.0 → 6.2.1

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