shoryuken 5.0.4 → 5.2.3

Sign up to get free protection for your applications and to get access to all the features.
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