shoryuken 0.0.5 → 1.0.0

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