shoryuken 5.0.4 → 5.2.3

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/specs.yml +62 -0
  3. data/.reek.yml +5 -0
  4. data/.rubocop.yml +1 -1
  5. data/Appraisals +36 -0
  6. data/CHANGELOG.md +63 -0
  7. data/Gemfile +3 -1
  8. data/README.md +24 -2
  9. data/Rakefile +15 -1
  10. data/bin/cli/sqs.rb +50 -5
  11. data/gemfiles/.gitignore +1 -0
  12. data/gemfiles/aws_sdk_core_2.gemfile +21 -0
  13. data/gemfiles/rails_4_2.gemfile +20 -0
  14. data/gemfiles/rails_5_2.gemfile +21 -0
  15. data/gemfiles/rails_6_0.gemfile +21 -0
  16. data/gemfiles/rails_6_1.gemfile +21 -0
  17. data/lib/shoryuken/environment_loader.rb +7 -1
  18. data/lib/shoryuken/extensions/active_job_adapter.rb +25 -18
  19. data/lib/shoryuken/extensions/active_job_extensions.rb +38 -0
  20. data/lib/shoryuken/launcher.rb +1 -0
  21. data/lib/shoryuken/manager.rb +24 -5
  22. data/lib/shoryuken/options.rb +1 -0
  23. data/lib/shoryuken/polling/base.rb +2 -0
  24. data/lib/shoryuken/polling/strict_priority.rb +6 -0
  25. data/lib/shoryuken/polling/weighted_round_robin.rb +11 -0
  26. data/lib/shoryuken/queue.rb +39 -11
  27. data/lib/shoryuken/version.rb +1 -1
  28. data/lib/shoryuken.rb +1 -0
  29. data/shoryuken.gemspec +0 -1
  30. data/spec/integration/launcher_spec.rb +29 -2
  31. data/spec/shared_examples_for_active_job.rb +226 -9
  32. data/spec/shoryuken/environment_loader_spec.rb +22 -2
  33. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +1 -1
  34. data/spec/shoryuken/extensions/active_job_base_spec.rb +84 -0
  35. data/spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb +4 -0
  36. data/spec/shoryuken/extensions/active_job_wrapper_spec.rb +20 -0
  37. data/spec/shoryuken/manager_spec.rb +35 -1
  38. data/spec/shoryuken/polling/strict_priority_spec.rb +10 -0
  39. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +10 -0
  40. data/spec/shoryuken/queue_spec.rb +23 -0
  41. data/spec/spec_helper.rb +5 -9
  42. metadata +20 -22
  43. data/.travis.yml +0 -34
  44. data/Gemfile.aws-sdk-core-v2 +0 -13
@@ -0,0 +1,21 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ group :test do
6
+ gem "activejob", "~> 6.1"
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 "pry-byebug", "3.9.0"
18
+ gem "rubocop"
19
+ end
20
+
21
+ gemspec path: "../"
@@ -70,12 +70,18 @@ module Shoryuken
70
70
  ::Rails.application.config.eager_load = true
71
71
  end
72
72
  end
73
- require 'shoryuken/extensions/active_job_adapter' if Shoryuken.active_job?
73
+ if Shoryuken.active_job?
74
+ require 'shoryuken/extensions/active_job_extensions'
75
+ require 'shoryuken/extensions/active_job_adapter'
76
+ require 'shoryuken/extensions/active_job_concurrent_send_adapter'
77
+ end
74
78
  require File.expand_path('config/environment.rb')
75
79
  end
76
80
  end
77
81
 
78
82
  def prefix_active_job_queue_name(queue_name, weight)
83
+ return [queue_name, weight] if queue_name.start_with?('https://', 'arn:')
84
+
79
85
  queue_name_prefix = ::ActiveJob::Base.queue_name_prefix
80
86
  queue_name_delimiter = ::ActiveJob::Base.queue_name_delimiter
81
87
 
@@ -33,8 +33,12 @@ module ActiveJob
33
33
  def enqueue(job, options = {}) #:nodoc:
34
34
  register_worker!(job)
35
35
 
36
+ job.sqs_send_message_parameters.merge! options
37
+
36
38
  queue = Shoryuken::Client.queues(job.queue_name)
37
- queue.send_message(message(queue, job, options))
39
+ send_message_params = message queue, job
40
+ job.sqs_send_message_parameters = send_message_params
41
+ queue.send_message send_message_params
38
42
  end
39
43
 
40
44
  def enqueue_at(job, timestamp) #:nodoc:
@@ -50,44 +54,47 @@ module ActiveJob
50
54
  delay
51
55
  end
52
56
 
53
- def message(queue, job, options = {})
57
+ def message(queue, job)
54
58
  body = job.serialize
59
+ job_params = job.sqs_send_message_parameters
60
+
61
+ attributes = job_params[:message_attributes] || {}
55
62
 
56
- msg = {}
63
+ msg = {
64
+ message_body: body,
65
+ message_attributes: attributes.merge(MESSAGE_ATTRIBUTES)
66
+ }
57
67
 
58
68
  if queue.fifo?
59
69
  # See https://github.com/phstc/shoryuken/issues/457
60
70
  msg[:message_deduplication_id] = Digest::SHA256.hexdigest(JSON.dump(body.except('job_id')))
61
71
  end
62
72
 
63
- msg[:message_body] = body
64
- msg[:message_attributes] = message_attributes
65
-
66
- msg.merge(options)
73
+ msg.merge(job_params.except(:message_attributes))
67
74
  end
68
75
 
69
76
  def register_worker!(job)
70
77
  Shoryuken.register_worker(job.queue_name, JobWrapper)
71
78
  end
72
79
 
73
- def message_attributes
74
- @message_attributes ||= {
75
- 'shoryuken_class' => {
76
- string_value: JobWrapper.to_s,
77
- data_type: 'String'
78
- }
79
- }
80
- end
81
-
82
80
  class JobWrapper #:nodoc:
83
81
  include Shoryuken::Worker
84
82
 
85
83
  shoryuken_options body_parser: :json, auto_delete: true
86
84
 
87
- def perform(_sqs_msg, hash)
88
- Base.execute hash
85
+ def perform(sqs_msg, hash)
86
+ receive_count = sqs_msg.attributes['ApproximateReceiveCount'].to_i
87
+ past_receives = receive_count - 1
88
+ Base.execute hash.merge({ 'executions' => past_receives })
89
89
  end
90
90
  end
91
+
92
+ MESSAGE_ATTRIBUTES = {
93
+ 'shoryuken_class' => {
94
+ string_value: JobWrapper.to_s,
95
+ data_type: 'String'
96
+ }
97
+ }.freeze
91
98
  end
92
99
  end
93
100
  end
@@ -0,0 +1,38 @@
1
+ module Shoryuken
2
+ module ActiveJobExtensions
3
+ # Adds an accessor for SQS SendMessage parameters on ActiveJob jobs
4
+ # (instances of ActiveJob::Base). Shoryuken ActiveJob queue adapters use
5
+ # these parameters when enqueueing jobs; other adapters can ignore them.
6
+ module SQSSendMessageParametersAccessor
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ attr_accessor :sqs_send_message_parameters
11
+ end
12
+ end
13
+
14
+ # Initializes SQS SendMessage parameters on instances of ActiveJobe::Base
15
+ # to the empty hash, and populates it whenever `#enqueue` is called, such
16
+ # as when using ActiveJob::Base.set.
17
+ module SQSSendMessageParametersSupport
18
+ def initialize(*arguments)
19
+ super(*arguments)
20
+ self.sqs_send_message_parameters = {}
21
+ end
22
+ ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
23
+
24
+ def enqueue(options = {})
25
+ sqs_options = options.extract! :message_attributes,
26
+ :message_system_attributes,
27
+ :message_deduplication_id,
28
+ :message_group_id
29
+ sqs_send_message_parameters.merge! sqs_options
30
+
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ ActiveJob::Base.include Shoryuken::ActiveJobExtensions::SQSSendMessageParametersAccessor
38
+ ActiveJob::Base.prepend Shoryuken::ActiveJobExtensions::SQSSendMessageParametersSupport
@@ -71,6 +71,7 @@ module Shoryuken
71
71
  def create_managers
72
72
  Shoryuken.groups.map do |group, options|
73
73
  Shoryuken::Manager.new(
74
+ group,
74
75
  Shoryuken::Fetcher.new(group),
75
76
  Shoryuken.polling_strategy(group).new(options[:queues], Shoryuken.delay(group)),
76
77
  options[:concurrency],
@@ -6,7 +6,8 @@ module Shoryuken
6
6
  # See https://github.com/phstc/shoryuken/issues/348#issuecomment-292847028
7
7
  MIN_DISPATCH_INTERVAL = 0.1
8
8
 
9
- def initialize(fetcher, polling_strategy, concurrency, executor)
9
+ def initialize(group, fetcher, polling_strategy, concurrency, executor)
10
+ @group = group
10
11
  @fetcher = fetcher
11
12
  @polling_strategy = polling_strategy
12
13
  @max_processors = concurrency
@@ -16,6 +17,7 @@ module Shoryuken
16
17
  end
17
18
 
18
19
  def start
20
+ fire_utilization_update_event
19
21
  dispatch_loop
20
22
  end
21
23
 
@@ -57,8 +59,15 @@ module Shoryuken
57
59
  @max_processors - busy
58
60
  end
59
61
 
60
- def processor_done
62
+ def processor_done(queue)
61
63
  @busy_processors.decrement
64
+ fire_utilization_update_event
65
+
66
+ client_queue = Shoryuken::Client.queues(queue)
67
+ return unless client_queue.fifo?
68
+ return unless @polling_strategy.respond_to?(:message_processed)
69
+
70
+ @polling_strategy.message_processed(queue)
62
71
  end
63
72
 
64
73
  def assign(queue_name, sqs_msg)
@@ -67,10 +76,12 @@ module Shoryuken
67
76
  logger.debug { "Assigning #{sqs_msg.message_id}" }
68
77
 
69
78
  @busy_processors.increment
79
+ fire_utilization_update_event
70
80
 
71
- Concurrent::Promise.execute(
72
- executor: @executor
73
- ) { Processor.process(queue_name, sqs_msg) }.then { processor_done }.rescue { processor_done }
81
+ Concurrent::Promise
82
+ .execute(executor: @executor) { Processor.process(queue_name, sqs_msg) }
83
+ .then { processor_done(queue_name) }
84
+ .rescue { processor_done(queue_name) }
74
85
  end
75
86
 
76
87
  def dispatch_batch(queue)
@@ -108,5 +119,13 @@ module Shoryuken
108
119
 
109
120
  @running.make_false
110
121
  end
122
+
123
+ def fire_utilization_update_event
124
+ fire_event :utilization_update, false, {
125
+ group: @group,
126
+ max_processors: @max_processors,
127
+ busy_processors: busy
128
+ }
129
+ end
111
130
  end
112
131
  end
@@ -9,6 +9,7 @@ module Shoryuken
9
9
  lifecycle_events: {
10
10
  startup: [],
11
11
  dispatch: [],
12
+ utilization_update: [],
12
13
  quiet: [],
13
14
  shutdown: []
14
15
  }
@@ -40,6 +40,8 @@ module Shoryuken
40
40
  fail NotImplementedError
41
41
  end
42
42
 
43
+ def message_processed(_queue); end
44
+
43
45
  def active_queues
44
46
  fail NotImplementedError
45
47
  end
@@ -38,6 +38,11 @@ module Shoryuken
38
38
  .reverse
39
39
  end
40
40
 
41
+ def message_processed(queue)
42
+ logger.debug "Unpausing #{queue}"
43
+ @paused_until[queue] = Time.now
44
+ end
45
+
41
46
  private
42
47
 
43
48
  def next_active_queue
@@ -70,6 +75,7 @@ module Shoryuken
70
75
 
71
76
  def pause(queue)
72
77
  return unless delay > 0
78
+
73
79
  @paused_until[queue] = Time.now + delay
74
80
  logger.debug "Paused #{queue}"
75
81
  end
@@ -35,10 +35,20 @@ module Shoryuken
35
35
  unparse_queues(@queues)
36
36
  end
37
37
 
38
+ def message_processed(queue)
39
+ return if @paused_queues.empty?
40
+
41
+ logger.debug "Unpausing #{queue}"
42
+ @paused_queues.reject! { |_time, name| name == queue }
43
+ @queues << queue
44
+ @queues.uniq!
45
+ end
46
+
38
47
  private
39
48
 
40
49
  def pause(queue)
41
50
  return unless @queues.delete(queue)
51
+
42
52
  @paused_queues << [Time.now + delay, queue]
43
53
  logger.debug "Paused #{queue}"
44
54
  end
@@ -46,6 +56,7 @@ module Shoryuken
46
56
  def unpause_queues
47
57
  return if @paused_queues.empty?
48
58
  return if Time.now < @paused_queues.first[0]
59
+
49
60
  pause = @paused_queues.shift
50
61
  @queues << pause[1]
51
62
  logger.debug "Unpaused #{pause[1]}"
@@ -8,9 +8,9 @@ module Shoryuken
8
8
 
9
9
  attr_accessor :name, :client, :url
10
10
 
11
- def initialize(client, name_or_url)
11
+ def initialize(client, name_or_url_or_arn)
12
12
  self.client = client
13
- set_name_and_url(name_or_url)
13
+ set_name_and_url(name_or_url_or_arn)
14
14
  end
15
15
 
16
16
  def visibility_timeout
@@ -50,32 +50,60 @@ module Shoryuken
50
50
  # Make sure the memoization work with boolean to avoid multiple calls to SQS
51
51
  # see https://github.com/phstc/shoryuken/pull/529
52
52
  return @_fifo if defined?(@_fifo)
53
+
53
54
  @_fifo = queue_attributes.attributes[FIFO_ATTR] == 'true'
55
+ @_fifo
54
56
  end
55
57
 
56
58
  private
57
59
 
58
- def set_by_name(name)
60
+ def initialize_fifo_attribute
61
+ # calling fifo? will also initialize it
62
+ fifo?
63
+ end
64
+
65
+ def set_by_name(name) # rubocop:disable Naming/AccessorMethodName
59
66
  self.name = name
60
67
  self.url = client.get_queue_url(queue_name: name).queue_url
61
68
  end
62
69
 
63
- def set_by_url(url)
70
+ def set_by_url(url) # rubocop:disable Naming/AccessorMethodName
64
71
  self.name = url.split('/').last
65
72
  self.url = url
66
73
  end
67
74
 
68
- def set_name_and_url(name_or_url)
69
- if name_or_url.include?('://')
70
- set_by_url(name_or_url)
75
+ def arn_to_url(arn_str)
76
+ *, region, account_id, resource = arn_str.split(':')
77
+
78
+ required = [region, account_id, resource].map(&:to_s)
79
+ valid = required.none?(&:empty?)
80
+
81
+ abort "Invalid ARN: #{arn_str}. A valid ARN must include: region, account_id and resource." unless valid
82
+
83
+ "https://sqs.#{region}.amazonaws.com/#{account_id}/#{resource}"
84
+ end
85
+
86
+ def set_name_and_url(name_or_url_or_arn) # rubocop:disable Naming/AccessorMethodName
87
+ if name_or_url_or_arn.include?('://')
88
+ set_by_url(name_or_url_or_arn)
89
+
90
+ # anticipate the fifo? checker for validating the queue URL
91
+ initialize_fifo_attribute
92
+ return
93
+ end
94
+
95
+ if name_or_url_or_arn.start_with?('arn:')
96
+ url = arn_to_url(name_or_url_or_arn)
97
+ set_by_url(url)
71
98
 
72
99
  # anticipate the fifo? checker for validating the queue URL
73
- return fifo?
100
+ initialize_fifo_attribute
101
+ return
74
102
  end
75
103
 
76
- set_by_name(name_or_url)
77
- rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue => ex
78
- raise ex, "The specified queue #{name_or_url} does not exist."
104
+ set_by_name(name_or_url_or_arn)
105
+ rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue => e
106
+ raise e, "The specified queue #{name_or_url_or_arn} does not exist."
79
107
  end
80
108
 
81
109
  def queue_attributes
@@ -1,3 +1,3 @@
1
1
  module Shoryuken
2
- VERSION = '5.0.4'.freeze
2
+ VERSION = '5.2.3'.freeze
3
3
  end
data/lib/shoryuken.rb CHANGED
@@ -89,6 +89,7 @@ module Shoryuken
89
89
  end
90
90
 
91
91
  if Shoryuken.active_job?
92
+ require 'shoryuken/extensions/active_job_extensions'
92
93
  require 'shoryuken/extensions/active_job_adapter'
93
94
  require 'shoryuken/extensions/active_job_concurrent_send_adapter'
94
95
  end
data/shoryuken.gemspec CHANGED
@@ -18,7 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ['lib']
19
19
 
20
20
  spec.add_development_dependency 'dotenv'
21
- spec.add_development_dependency 'pry-byebug'
22
21
  spec.add_development_dependency 'rake'
23
22
  spec.add_development_dependency 'rspec'
24
23
 
@@ -4,10 +4,37 @@ require 'shoryuken/launcher'
4
4
  require 'securerandom'
5
5
 
6
6
  RSpec.describe Shoryuken::Launcher do
7
- describe 'Consuming messages', slow: true do
7
+ let(:sqs_client) do
8
+ Aws::SQS::Client.new(
9
+ region: 'us-east-1',
10
+ endpoint: 'http://localhost:5000',
11
+ access_key_id: 'fake',
12
+ secret_access_key: 'fake'
13
+ )
14
+ end
15
+
16
+ let(:executor) do
17
+ # We can't use Concurrent.global_io_executor in these tests since once you
18
+ # shut down a thread pool, you can't start it back up. Instead, we create
19
+ # one new thread pool executor for each spec. We use a new
20
+ # CachedThreadPool, since that most closely resembles
21
+ # Concurrent.global_io_executor
22
+ Concurrent::CachedThreadPool.new auto_terminate: true
23
+ end
24
+
25
+ describe 'Consuming messages' do
8
26
  before do
9
27
  Aws.config[:stub_responses] = false
10
- Aws.config[:region] = 'us-east-1'
28
+
29
+ allow(Shoryuken).to receive(:launcher_executor).and_return(executor)
30
+
31
+ Shoryuken.configure_client do |config|
32
+ config.sqs_client = sqs_client
33
+ end
34
+
35
+ Shoryuken.configure_server do |config|
36
+ config.sqs_client = sqs_client
37
+ end
11
38
 
12
39
  StandardWorker.received_messages = 0
13
40