shoryuken 0.0.5 → 1.0.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.
@@ -0,0 +1,167 @@
1
+ module Shoryuken
2
+ class EnvironmentLoader
3
+ attr_reader :options
4
+
5
+ def self.load(options)
6
+ new(options).load
7
+ end
8
+
9
+ def self.load_for_rails_console
10
+ load(config_file: (Rails.root + 'config' + 'shoryuken.yml'))
11
+ end
12
+
13
+ def initialize options
14
+ @options = options
15
+ end
16
+
17
+ def load
18
+ initialize_logger
19
+ load_rails if options[:rails]
20
+ Shoryuken.options.merge!(config_file_options)
21
+ merge_cli_defined_queues
22
+ Shoryuken.options.merge!(options)
23
+ parse_queues
24
+ require_workers
25
+ initialize_aws
26
+ validate_queues
27
+ validate_workers
28
+ patch_deprecated_workers
29
+ end
30
+
31
+ private
32
+
33
+ def config_file_options
34
+ if (path = options[:config_file])
35
+ unless File.exist?(path)
36
+ Shoryuken.logger.warn "Config file #{path} does not exist"
37
+ path = nil
38
+ end
39
+ end
40
+
41
+ return {} unless path
42
+
43
+ YAML.load(ERB.new(IO.read(path)).result).deep_symbolize_keys
44
+ end
45
+
46
+ def initialize_aws
47
+ # aws-sdk tries to load the credentials from the ENV variables: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
48
+ # when not explicit supplied
49
+ return if Shoryuken.options[:aws].empty?
50
+
51
+ shoryuken_keys = %i(
52
+ account_id
53
+ sns_endpoint
54
+ sqs_endpoint
55
+ receive_message)
56
+
57
+ aws_options = Shoryuken.options[:aws].reject do |k, v|
58
+ shoryuken_keys.include?(k)
59
+ end
60
+
61
+ credentials = Aws::Credentials.new(
62
+ aws_options.delete(:access_key_id),
63
+ aws_options.delete(:secret_access_key))
64
+
65
+ aws_options = aws_options.merge(credentials: credentials)
66
+
67
+ if callback = Shoryuken.aws_initialization_callback
68
+ Shoryuken.logger.info "Calling Shoryuken.on_aws_initialization block"
69
+ callback.call(aws_options)
70
+ end
71
+
72
+ Aws.config = aws_options
73
+ end
74
+
75
+ def initialize_logger
76
+ Shoryuken::Logging.initialize_logger(options[:logfile]) if options[:logfile]
77
+ Shoryuken.logger.level = Logger::DEBUG if options[:verbose]
78
+ end
79
+
80
+ def load_rails
81
+ # Adapted from: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/cli.rb
82
+
83
+ require 'rails'
84
+ if ::Rails::VERSION::MAJOR < 4
85
+ require File.expand_path("config/environment.rb")
86
+ ::Rails.application.eager_load!
87
+ else
88
+ # Painful contortions, see 1791 for discussion
89
+ require File.expand_path("config/application.rb")
90
+ ::Rails::Application.initializer "shoryuken.eager_load" do
91
+ ::Rails.application.config.eager_load = true
92
+ end
93
+ require 'shoryuken/extensions/active_job_adapter' if defined?(::ActiveJob)
94
+ require File.expand_path("config/environment.rb")
95
+ end
96
+
97
+ Shoryuken.logger.info "Rails environment loaded"
98
+ end
99
+
100
+ def merge_cli_defined_queues
101
+ cli_defined_queues = options.delete(:queues) || []
102
+
103
+ cli_defined_queues.each do |cli_defined_queue|
104
+ Shoryuken.options[:queues].delete_if do |config_file_queue|
105
+ config_file_queue[0] == cli_defined_queue[0]
106
+ end
107
+
108
+ Shoryuken.options[:queues] << cli_defined_queue
109
+ end
110
+ end
111
+
112
+ def parse_queue(queue, weight = nil)
113
+ [weight.to_i, 1].max.times { Shoryuken.queues << queue }
114
+ end
115
+
116
+ def parse_queues
117
+ Shoryuken.options[:queues].to_a.each do |queue_and_weight|
118
+ parse_queue(*queue_and_weight)
119
+ end
120
+ end
121
+
122
+ def patch_deprecated_workers
123
+ Shoryuken.worker_registry.queues.each do |queue|
124
+ Shoryuken.worker_registry.workers(queue).each do |worker_class|
125
+ if worker_class.instance_method(:perform).arity == 1
126
+ Shoryuken.logger.warn "[DEPRECATION] #{worker_class.name}#perform(sqs_msg) is deprecated. Please use #{worker_class.name}#perform(sqs_msg, body)"
127
+
128
+ worker_class.class_eval do
129
+ alias_method :deprecated_perform, :perform
130
+
131
+ def perform(sqs_msg, body = nil)
132
+ deprecated_perform(sqs_msg)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def require_workers
141
+ require Shoryuken.options[:require] if Shoryuken.options[:require]
142
+ end
143
+
144
+ def validate_queues
145
+ Shoryuken.logger.warn 'No queues supplied' if Shoryuken.queues.empty?
146
+
147
+ Shoryuken.queues.uniq.each do |queue|
148
+ begin
149
+ Shoryuken::Client.queues queue
150
+ rescue Aws::SQS::Errors::NonExistentQueue
151
+ Shoryuken.logger.warn "AWS Queue '#{queue}' does not exist"
152
+ end
153
+ end
154
+ end
155
+
156
+ def validate_workers
157
+ all_queues = Shoryuken.queues
158
+ queues_with_workers = Shoryuken.worker_registry.queues
159
+
160
+ unless defined?(::ActiveJob)
161
+ (all_queues - queues_with_workers).each do |queue|
162
+ Shoryuken.logger.warn "No worker supplied for '#{queue}'"
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -19,7 +19,8 @@ module ActiveJob
19
19
  def enqueue(job) #:nodoc:
20
20
  register_worker!(job)
21
21
 
22
- Shoryuken::Client.send_message(job.queue_name, job.serialize, message_attributes: message_attributes)
22
+ queue = Shoryuken::Client.queues(job.queue_name)
23
+ queue.send_message(message(job))
23
24
  end
24
25
 
25
26
  def enqueue_at(job, timestamp) #:nodoc:
@@ -28,13 +29,20 @@ module ActiveJob
28
29
  delay = (timestamp - Time.current.to_f).round
29
30
  raise 'The maximum allowed delay is 15 minutes' if delay > 15.minutes
30
31
 
31
- Shoryuken::Client.send_message(job.queue_name, job.serialize, delay_seconds: delay,
32
- message_attributes: message_attributes)
32
+ queue = Shoryuken::Client.queues(job.queue_name)
33
+ queue.send_message(message(job, delay_seconds: delay))
33
34
  end
34
35
 
35
-
36
36
  private
37
37
 
38
+ def message(job, options = {})
39
+ body = job.serialize
40
+ body = JSON.dump(body) if body.is_a?(Hash)
41
+
42
+ { message_body: body,
43
+ message_attributes: message_attributes }.merge(options)
44
+ end
45
+
38
46
  def register_worker!(job)
39
47
  Shoryuken.register_worker(job.queue_name, JobWrapper)
40
48
  end
@@ -9,16 +9,15 @@ module Shoryuken
9
9
  @manager = manager
10
10
  end
11
11
 
12
- def receive_message(queue, limit)
12
+ def receive_messages(queue, limit)
13
13
  # AWS limits the batch size by 10
14
14
  limit = limit > FETCH_LIMIT ? FETCH_LIMIT : limit
15
15
 
16
16
  options = Shoryuken.options[:aws][:receive_message].to_h.dup
17
- options[:limit] = limit
18
- options[:message_attribute_names] ||= []
19
- options[:message_attribute_names] << 'shoryuken_class'
17
+ options[:max_number_of_messages] = limit
18
+ options[:message_attribute_names] = %w(All)
20
19
 
21
- Shoryuken::Client.receive_message queue, options
20
+ Shoryuken::Client.queues(queue).receive_messages options
22
21
  end
23
22
 
24
23
  def fetch(queue, available_processors)
@@ -31,7 +30,7 @@ module Shoryuken
31
30
  batch = Shoryuken.worker_registry.batch_receive_messages?(queue)
32
31
  limit = batch ? FETCH_LIMIT : available_processors
33
32
 
34
- if (sqs_msgs = Array(receive_message(queue, limit))).any?
33
+ if (sqs_msgs = Array(receive_messages(queue, limit))).any?
35
34
  logger.info "Found #{sqs_msgs.size} messages for '#{queue}'"
36
35
 
37
36
  if batch
@@ -63,7 +62,7 @@ module Shoryuken
63
62
 
64
63
  def patch_sqs_msgs!(sqs_msgs)
65
64
  sqs_msgs.instance_eval do
66
- def id
65
+ def message_id
67
66
  "batch-with-#{size}-messages"
68
67
  end
69
68
  end
@@ -32,6 +32,11 @@ module Shoryuken
32
32
  watchdog('Manager#stop died') do
33
33
  @done = true
34
34
 
35
+ if callback = Shoryuken.stop_callback
36
+ logger.info "Calling Shoryuken.on_stop block"
37
+ callback.call
38
+ end
39
+
35
40
  @fetcher.terminate if @fetcher.alive?
36
41
 
37
42
  logger.info { "Shutting down #{@ready.size} quiet workers" }
@@ -85,7 +90,7 @@ module Shoryuken
85
90
 
86
91
  def assign(queue, sqs_msg)
87
92
  watchdog("Manager#assign died") do
88
- logger.info "Assigning #{sqs_msg.id}"
93
+ logger.info "Assigning #{sqs_msg.message_id}"
89
94
 
90
95
  processor = @ready.pop
91
96
  @busy << processor
@@ -187,7 +192,6 @@ module Shoryuken
187
192
 
188
193
  unless defined?(::ActiveJob) || !Shoryuken.worker_registry.workers(queue).empty?
189
194
  # when no worker registered pause the queue to avoid endless recursion
190
-
191
195
  logger.debug "Pausing '#{queue}' for #{Shoryuken.options[:delay].to_f} seconds, because no workers registered"
192
196
 
193
197
  after(Shoryuken.options[:delay].to_f) { async.restart_queue!(queue) }
@@ -7,7 +7,13 @@ module Shoryuken
7
7
 
8
8
  auto_delete = worker.class.get_shoryuken_options['delete'] || worker.class.get_shoryuken_options['auto_delete']
9
9
 
10
- Shoryuken::Client.queues(queue).batch_delete(*Array(sqs_msg)) if auto_delete
10
+ if auto_delete
11
+ entries = [sqs_msg].flatten.map.with_index do |message, i|
12
+ { id: i.to_s, receipt_handle: message.receipt_handle }
13
+ end
14
+
15
+ Shoryuken::Client.queues(queue).delete_messages(entries: entries)
16
+ end
11
17
  end
12
18
  end
13
19
  end
@@ -5,7 +5,7 @@ module Shoryuken
5
5
  include Util
6
6
 
7
7
  def call(worker, queue, sqs_msg, body)
8
- Shoryuken::Logging.with_context("#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.id}") do
8
+ Shoryuken::Logging.with_context("#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.message_id}") do
9
9
  begin
10
10
  started_at = Time.now
11
11
 
@@ -15,7 +15,7 @@ module Shoryuken
15
15
 
16
16
  total_time = elapsed(started_at)
17
17
 
18
- if (total_time / 1000.0) > (timeout = Shoryuken::Client.visibility_timeout(queue))
18
+ if (total_time / 1000.0) > (timeout = Shoryuken::Client.queues(queue).visibility_timeout)
19
19
  logger.warn "exceeded the queue visibility timeout by #{total_time - (timeout * 1000)} ms"
20
20
  end
21
21
 
@@ -37,13 +37,15 @@ module Shoryuken
37
37
 
38
38
  def auto_visibility_timeout(queue, sqs_msg, worker_class)
39
39
  if worker_class.auto_visibility_timeout?
40
- timer = every(worker_class.visibility_timeout_heartbeat) do
40
+ queue_visibility_timeout = Shoryuken::Client.queues(queue).visibility_timeout
41
+
42
+ timer = every(queue_visibility_timeout - 5) do
41
43
  begin
42
- logger.debug "Extending message #{worker_name(worker_class, sqs_msg)}/#{queue}/#{sqs_msg.id} visibility timeout to #{worker_class.extended_visibility_timeout}"
44
+ logger.debug "Extending message #{worker_name(worker_class, sqs_msg)}/#{queue}/#{sqs_msg.message_id} visibility timeout by #{queue_visibility_timeout}s."
43
45
 
44
- sqs_msg.visibility_timeout = worker_class.extended_visibility_timeout
46
+ sqs_msg.visibility_timeout = queue_visibility_timeout
45
47
  rescue => e
46
- logger.error "Could not auto extend the message #{worker_class}/#{queue}/#{sqs_msg.id} visibility timeout. Error: #{e.message}"
48
+ logger.error "Could not auto extend the message #{worker_class}/#{queue}/#{sqs_msg.message_id} visibility timeout. Error: #{e.message}"
47
49
  end
48
50
  end
49
51
  end
@@ -0,0 +1,27 @@
1
+ module Shoryuken
2
+ class SnsArn
3
+ def initialize topic
4
+ @topic = topic
5
+ end
6
+
7
+ def to_s
8
+ @arn ||= "arn:aws:sns:#{region}:#{account_id}:#{@topic}"
9
+ end
10
+
11
+ private
12
+
13
+ def account_id
14
+ Shoryuken::Client.account_id.tap do |account_id|
15
+ if account_id.to_s.empty?
16
+ fail "To generate SNS ARNs, you must assign an :account_id in your Shoryuken::Client."
17
+ end
18
+ end
19
+ end
20
+
21
+ def region
22
+ Aws.config.fetch(:region) do
23
+ fail "To generate SNS ARNs, you must include a :region in your AWS config."
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ module Shoryuken
2
+ class Topic
3
+ def initialize(name, sns)
4
+ @name, @sns = name, sns
5
+ end
6
+
7
+ def arn
8
+ @arn ||= Client.sns_arn.new(@name).to_s
9
+ end
10
+
11
+ def send_message(body, options = {})
12
+ body = JSON.dump(body) if body.is_a?(Hash)
13
+
14
+ @sns.publish(topic_arn: arn, message: body)
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Shoryuken
2
- VERSION = '0.0.5'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -13,7 +13,10 @@ module Shoryuken
13
13
  data_type: 'String'
14
14
  }
15
15
 
16
- Shoryuken::Client.send_message(get_shoryuken_options['queue'], body, options)
16
+ body = JSON.dump(body) if body.is_a?(Hash)
17
+ options[:message_body] = body
18
+
19
+ Shoryuken::Client.queues(get_shoryuken_options['queue']).send_message(options)
17
20
  end
18
21
 
19
22
  def perform_in(interval, body, options = {})
@@ -51,14 +54,6 @@ module Shoryuken
51
54
  !!get_shoryuken_options['auto_visibility_timeout']
52
55
  end
53
56
 
54
- def visibility_timeout_heartbeat
55
- extended_visibility_timeout - 5
56
- end
57
-
58
- def extended_visibility_timeout
59
- Shoryuken::Client.visibility_timeout(get_shoryuken_options['queue'])
60
- end
61
-
62
57
  def get_shoryuken_options # :nodoc:
63
58
  @shoryuken_options || Shoryuken.default_worker_options
64
59
  end
data/shoryuken.gemspec CHANGED
@@ -21,7 +21,10 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency "rake"
22
22
  spec.add_development_dependency "rspec"
23
23
  spec.add_development_dependency "pry-byebug"
24
+ spec.add_development_dependency "nokogiri"
25
+ spec.add_development_dependency "dotenv"
24
26
 
25
- spec.add_dependency "aws-sdk-v1"
27
+ spec.add_dependency "aws-sdk-core", "2.0.21"
28
+ spec.add_dependency "aws-sdk-resources", "2.0.21.pre"
26
29
  spec.add_dependency "celluloid", "~> 0.16.0"
27
30
  end
@@ -5,83 +5,41 @@ require 'shoryuken/launcher'
5
5
  describe Shoryuken::Launcher do
6
6
  describe 'Consuming messages', slow: :true do
7
7
  before do
8
- Shoryuken.options[:aws] ||= {}
9
- Shoryuken.options[:aws][:receive_message] ||= {}
10
- Shoryuken.options[:aws][:receive_message][:wait_time_seconds] = 5
11
-
12
- Shoryuken.queues << 'shoryuken'
13
- Shoryuken.queues << 'shoryuken_command'
14
-
15
- Shoryuken.register_worker 'shoryuken', StandardWorker
16
- Shoryuken.register_worker 'shoryuken_command', CommandWorker
17
-
18
- subject.run
8
+ Shoryuken.options[:aws][:receive_message] = { wait_time_seconds: 5 }
19
9
 
20
10
  StandardWorker.received_messages = 0
21
- end
22
-
23
- after { subject.stop }
24
-
25
- class CommandWorker
26
- include Shoryuken::Worker
27
11
 
28
- @@received_messages = 0
12
+ queue = "test_shoryuken#{StandardWorker}_#{SecureRandom.uuid}"
29
13
 
30
- shoryuken_options queue: 'shoryuken_command', auto_delete: true
14
+ Shoryuken::Client.sqs.create_queue queue_name: queue
31
15
 
32
- def perform(sqs_msg, body)
33
- @@received_messages = Array(sqs_msg).size
34
- end
16
+ Shoryuken.queues << queue
35
17
 
36
- def self.received_messages
37
- @@received_messages
38
- end
18
+ StandardWorker.get_shoryuken_options['queue'] = queue
39
19
 
40
- def self.received_messages=(received_messages)
41
- @@received_messages = received_messages
42
- end
20
+ Shoryuken.register_worker queue, StandardWorker
43
21
  end
44
22
 
45
- class StandardWorker
46
- include Shoryuken::Worker
47
-
48
- @@received_messages = 0
49
-
50
- shoryuken_options queue: 'shoryuken', auto_delete: true
23
+ after do
24
+ queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: StandardWorker.get_shoryuken_options['queue']).queue_url
51
25
 
52
- def perform(sqs_msg, body)
53
- @@received_messages = Array(sqs_msg).size
54
- end
55
-
56
- def self.received_messages
57
- @@received_messages
58
- end
59
-
60
- def self.received_messages=(received_messages)
61
- @@received_messages = received_messages
62
- end
26
+ Shoryuken::Client.sqs.delete_queue queue_url: queue_url
63
27
  end
64
28
 
65
29
  it 'consumes as a command worker' do
66
- CommandWorker.perform_async('Yo')
30
+ StandardWorker.perform_async('Yo')
67
31
 
68
- 10.times do
69
- break if CommandWorker.received_messages > 0
70
- sleep 1
71
- end
32
+ poll_queues_until { StandardWorker.received_messages > 0 }
72
33
 
73
- expect(CommandWorker.received_messages).to eq 1
34
+ expect(StandardWorker.received_messages).to eq 1
74
35
  end
75
36
 
76
37
  it 'consumes a message' do
77
38
  StandardWorker.get_shoryuken_options['batch'] = false
78
39
 
79
- Shoryuken::Client.queues('shoryuken').send_message('Yo')
40
+ Shoryuken::Client.queues(StandardWorker.get_shoryuken_options['queue']).send_message(message_body: 'Yo')
80
41
 
81
- 10.times do
82
- break if StandardWorker.received_messages > 0
83
- sleep 1
84
- end
42
+ poll_queues_until { StandardWorker.received_messages > 0 }
85
43
 
86
44
  expect(StandardWorker.received_messages).to eq 1
87
45
  end
@@ -89,15 +47,48 @@ describe Shoryuken::Launcher do
89
47
  it 'consumes a batch' do
90
48
  StandardWorker.get_shoryuken_options['batch'] = true
91
49
 
92
- Shoryuken::Client.queues('shoryuken').batch_send *(['Yo'] * 10)
50
+ entries = 10.times.map { |i| { id: SecureRandom.uuid, message_body: i.to_s } }
93
51
 
94
- 10.times do
95
- break if StandardWorker.received_messages > 0
96
- sleep 1
97
- end
52
+ Shoryuken::Client.queues(StandardWorker.get_shoryuken_options['queue']).send_messages(entries: entries)
53
+
54
+ # Give the messages a chance to hit the queue so they are all available at the same time
55
+ sleep 2
56
+
57
+ poll_queues_until { StandardWorker.received_messages > 0 }
98
58
 
99
- # the fetch result is uncertain, should be greater than 1, but hard to tell the exact size
100
59
  expect(StandardWorker.received_messages).to be > 1
101
60
  end
61
+
62
+ def poll_queues_until
63
+ subject.run
64
+
65
+ Timeout::timeout(10) do
66
+ begin
67
+ sleep 0.5
68
+ end until yield
69
+ end
70
+ ensure
71
+ subject.stop
72
+ end
73
+
74
+ class StandardWorker
75
+ include Shoryuken::Worker
76
+
77
+ @@received_messages = 0
78
+
79
+ shoryuken_options auto_delete: true
80
+
81
+ def perform(sqs_msg, body)
82
+ @@received_messages += Array(sqs_msg).size
83
+ end
84
+
85
+ def self.received_messages
86
+ @@received_messages
87
+ end
88
+
89
+ def self.received_messages=(received_messages)
90
+ @@received_messages = received_messages
91
+ end
92
+ end
102
93
  end
103
94
  end