shoryuken 3.0.11 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +13 -2
- data/Gemfile +1 -0
- data/README.md +15 -110
- data/bin/cli/sqs.rb +7 -0
- data/lib/shoryuken.rb +39 -173
- data/lib/shoryuken/default_worker_registry.rb +2 -2
- data/lib/shoryuken/environment_loader.rb +25 -9
- data/lib/shoryuken/fetcher.rb +17 -16
- data/lib/shoryuken/launcher.rb +86 -7
- data/lib/shoryuken/manager.rb +38 -79
- data/lib/shoryuken/options.rb +192 -0
- data/lib/shoryuken/polling/base.rb +67 -0
- data/lib/shoryuken/polling/strict_priority.rb +77 -0
- data/lib/shoryuken/polling/weighted_round_robin.rb +66 -0
- data/lib/shoryuken/processor.rb +21 -18
- data/lib/shoryuken/runner.rb +3 -15
- data/lib/shoryuken/version.rb +1 -1
- data/spec/integration/launcher_spec.rb +12 -6
- data/spec/shoryuken/environment_loader_spec.rb +3 -12
- data/spec/shoryuken/fetcher_spec.rb +30 -15
- data/spec/shoryuken/manager_spec.rb +9 -17
- data/spec/shoryuken/options_spec.rb +100 -0
- data/spec/shoryuken/{polling_spec.rb → polling/strict_priority_spec.rb} +1 -100
- data/spec/shoryuken/polling/weighted_round_robin_spec.rb +99 -0
- data/spec/shoryuken/processor_spec.rb +20 -39
- data/spec/shoryuken/runner_spec.rb +3 -4
- data/spec/shoryuken_spec.rb +0 -59
- data/spec/spec_helper.rb +7 -2
- metadata +12 -5
- data/lib/shoryuken/polling.rb +0 -204
@@ -0,0 +1,67 @@
|
|
1
|
+
module Shoryuken
|
2
|
+
module Polling
|
3
|
+
QueueConfiguration = Struct.new(:name, :options) do
|
4
|
+
def hash
|
5
|
+
name.hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def ==(other)
|
9
|
+
case other
|
10
|
+
when String
|
11
|
+
if options.empty?
|
12
|
+
name == other
|
13
|
+
else
|
14
|
+
false
|
15
|
+
end
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method :eql?, :==
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
if options.empty?
|
25
|
+
name
|
26
|
+
else
|
27
|
+
"#<QueueConfiguration #{name} options=#{options.inspect}>"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class BaseStrategy
|
33
|
+
include Util
|
34
|
+
|
35
|
+
def next_queue
|
36
|
+
fail NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def messages_found(queue, messages_found)
|
40
|
+
fail NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
def active_queues
|
44
|
+
fail NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
case other
|
49
|
+
when Array
|
50
|
+
@queues == other
|
51
|
+
else
|
52
|
+
if other.respond_to?(:active_queues)
|
53
|
+
active_queues == other.active_queues
|
54
|
+
else
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def delay
|
63
|
+
Shoryuken.options[:delay].to_f
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Shoryuken
|
2
|
+
module Polling
|
3
|
+
class StrictPriority < BaseStrategy
|
4
|
+
def initialize(queues)
|
5
|
+
# Priority ordering of the queues, highest priority first
|
6
|
+
@queues = queues
|
7
|
+
.group_by { |q| q }
|
8
|
+
.sort_by { |_, qs| -qs.count }
|
9
|
+
.map(&:first)
|
10
|
+
|
11
|
+
# Pause status of the queues, default to past time (unpaused)
|
12
|
+
@paused_until = queues
|
13
|
+
.each_with_object(Hash.new) { |queue, h| h[queue] = Time.at(0) }
|
14
|
+
|
15
|
+
# Start queues at 0
|
16
|
+
reset_next_queue
|
17
|
+
end
|
18
|
+
|
19
|
+
def next_queue
|
20
|
+
next_queue = next_active_queue
|
21
|
+
next_queue.nil? ? nil : QueueConfiguration.new(next_queue, {})
|
22
|
+
end
|
23
|
+
|
24
|
+
def messages_found(queue, messages_found)
|
25
|
+
if messages_found == 0
|
26
|
+
pause(queue)
|
27
|
+
else
|
28
|
+
reset_next_queue
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def active_queues
|
33
|
+
@queues
|
34
|
+
.reverse
|
35
|
+
.map.with_index(1)
|
36
|
+
.reject { |q, _| queue_paused?(q) }
|
37
|
+
.reverse
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def next_active_queue
|
43
|
+
reset_next_queue if queues_unpaused_since?
|
44
|
+
|
45
|
+
size = @queues.length
|
46
|
+
size.times do
|
47
|
+
queue = @queues[@next_queue_index]
|
48
|
+
@next_queue_index = (@next_queue_index + 1) % size
|
49
|
+
return queue unless queue_paused?(queue)
|
50
|
+
end
|
51
|
+
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def queues_unpaused_since?
|
56
|
+
last = @last_unpause_check
|
57
|
+
now = @last_unpause_check = Time.now
|
58
|
+
|
59
|
+
last && @paused_until.values.any? { |t| t > last && t <= now }
|
60
|
+
end
|
61
|
+
|
62
|
+
def reset_next_queue
|
63
|
+
@next_queue_index = 0
|
64
|
+
end
|
65
|
+
|
66
|
+
def queue_paused?(queue)
|
67
|
+
@paused_until[queue] > Time.now
|
68
|
+
end
|
69
|
+
|
70
|
+
def pause(queue)
|
71
|
+
return unless delay > 0
|
72
|
+
@paused_until[queue] = Time.now + delay
|
73
|
+
logger.debug "Paused #{queue}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Shoryuken
|
2
|
+
module Polling
|
3
|
+
class WeightedRoundRobin < BaseStrategy
|
4
|
+
def initialize(queues)
|
5
|
+
@initial_queues = queues
|
6
|
+
@queues = queues.dup.uniq
|
7
|
+
@paused_queues = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def next_queue
|
11
|
+
unpause_queues
|
12
|
+
queue = @queues.shift
|
13
|
+
return nil if queue.nil?
|
14
|
+
|
15
|
+
@queues << queue
|
16
|
+
QueueConfiguration.new(queue, {})
|
17
|
+
end
|
18
|
+
|
19
|
+
def messages_found(queue, messages_found)
|
20
|
+
if messages_found == 0
|
21
|
+
pause(queue)
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
maximum_weight = maximum_queue_weight(queue)
|
26
|
+
current_weight = current_queue_weight(queue)
|
27
|
+
if maximum_weight > current_weight
|
28
|
+
logger.info { "Increasing #{queue} weight to #{current_weight + 1}, max: #{maximum_weight}" }
|
29
|
+
@queues << queue
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def active_queues
|
34
|
+
unparse_queues(@queues)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def pause(queue)
|
40
|
+
return unless @queues.delete(queue)
|
41
|
+
@paused_queues << [Time.now + delay, queue]
|
42
|
+
logger.debug "Paused #{queue}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def unpause_queues
|
46
|
+
return if @paused_queues.empty?
|
47
|
+
return if Time.now < @paused_queues.first[0]
|
48
|
+
pause = @paused_queues.shift
|
49
|
+
@queues << pause[1]
|
50
|
+
logger.debug "Unpaused #{pause[1]}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def current_queue_weight(queue)
|
54
|
+
queue_weight(@queues, queue)
|
55
|
+
end
|
56
|
+
|
57
|
+
def maximum_queue_weight(queue)
|
58
|
+
queue_weight(@initial_queues, queue)
|
59
|
+
end
|
60
|
+
|
61
|
+
def queue_weight(queues, queue)
|
62
|
+
queues.count { |q| q == queue }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/shoryuken/processor.rb
CHANGED
@@ -2,35 +2,41 @@ module Shoryuken
|
|
2
2
|
class Processor
|
3
3
|
include Util
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
attr_reader :queue, :sqs_msg
|
6
|
+
|
7
|
+
def initialize(queue, sqs_msg)
|
8
|
+
@queue = queue
|
9
|
+
@sqs_msg = sqs_msg
|
7
10
|
end
|
8
11
|
|
9
|
-
def process
|
10
|
-
worker
|
11
|
-
body = get_body(worker.class, sqs_msg)
|
12
|
+
def process
|
13
|
+
return logger.error { "No worker found for #{queue}" } unless worker
|
12
14
|
|
13
15
|
worker.class.server_middleware.invoke(worker, queue, sqs_msg, body) do
|
14
16
|
worker.perform(sqs_msg, body)
|
15
17
|
end
|
16
18
|
rescue Exception => ex
|
17
|
-
|
19
|
+
logger.error { "Processor failed: #{ex.message}" }
|
20
|
+
logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?
|
21
|
+
|
18
22
|
raise
|
19
|
-
ensure
|
20
|
-
@manager.processor_done(queue)
|
21
23
|
end
|
22
24
|
|
23
25
|
private
|
24
26
|
|
25
|
-
def
|
26
|
-
|
27
|
-
sqs_msg.map { |m| parse_body(worker_class, m) }
|
28
|
-
else
|
29
|
-
parse_body(worker_class, sqs_msg)
|
30
|
-
end
|
27
|
+
def worker
|
28
|
+
@_worker ||= Shoryuken.worker_registry.fetch_worker(queue, sqs_msg)
|
31
29
|
end
|
32
30
|
|
33
|
-
def
|
31
|
+
def worker_class
|
32
|
+
worker.class
|
33
|
+
end
|
34
|
+
|
35
|
+
def body
|
36
|
+
@_body ||= sqs_msg.is_a?(Array) ? sqs_msg.map(&method(:parse_body)) : parse_body(sqs_msg)
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_body(sqs_msg)
|
34
40
|
body_parser = worker_class.get_shoryuken_options['body_parser']
|
35
41
|
|
36
42
|
case body_parser
|
@@ -50,9 +56,6 @@ module Shoryuken
|
|
50
56
|
body_parser.load(sqs_msg.body)
|
51
57
|
end
|
52
58
|
end
|
53
|
-
rescue => ex
|
54
|
-
logger.error { "Error parsing the message body: #{ex.message}\nbody_parser: #{body_parser}\nsqs_msg.body: #{sqs_msg.body}" }
|
55
|
-
raise
|
56
59
|
end
|
57
60
|
end
|
58
61
|
end
|
data/lib/shoryuken/runner.rb
CHANGED
@@ -19,7 +19,7 @@ module Shoryuken
|
|
19
19
|
def run(options)
|
20
20
|
self_read, self_write = IO.pipe
|
21
21
|
|
22
|
-
%w(INT TERM USR1
|
22
|
+
%w(INT TERM USR1 TTIN).each do |sig|
|
23
23
|
begin
|
24
24
|
trap sig do
|
25
25
|
self_write.puts(sig)
|
@@ -43,22 +43,15 @@ module Shoryuken
|
|
43
43
|
|
44
44
|
@launcher = Shoryuken::Launcher.new
|
45
45
|
|
46
|
-
if (callback = Shoryuken.start_callback)
|
47
|
-
logger.info { 'Calling on_start callback' }
|
48
|
-
callback.call
|
49
|
-
end
|
50
|
-
|
51
|
-
fire_event(:startup)
|
52
|
-
|
53
46
|
begin
|
54
|
-
@launcher.
|
47
|
+
@launcher.start
|
55
48
|
|
56
49
|
while (readable_io = IO.select([self_read]))
|
57
50
|
signal = readable_io.first[0].gets.strip
|
58
51
|
handle_signal(signal)
|
59
52
|
end
|
60
53
|
rescue Interrupt
|
61
|
-
@launcher.stop
|
54
|
+
@launcher.stop!
|
62
55
|
exit 0
|
63
56
|
end
|
64
57
|
end
|
@@ -110,7 +103,6 @@ module Shoryuken
|
|
110
103
|
logger.info { 'Received USR1, will soft shutdown down' }
|
111
104
|
|
112
105
|
@launcher.stop
|
113
|
-
fire_event(:quiet, true)
|
114
106
|
exit 0
|
115
107
|
end
|
116
108
|
|
@@ -126,13 +118,9 @@ module Shoryuken
|
|
126
118
|
end
|
127
119
|
|
128
120
|
def handle_signal(sig)
|
129
|
-
logger.info { "Got #{sig} signal" }
|
130
|
-
|
131
121
|
case sig
|
132
122
|
when 'USR1' then execute_soft_shutdown
|
133
123
|
when 'TTIN' then print_threads_backtrace
|
134
|
-
when 'USR2'
|
135
|
-
logger.warn { "Received #{sig}, will do nothing. To execute soft shutdown, please send USR1" }
|
136
124
|
else
|
137
125
|
logger.info { "Received #{sig}, will shutdown down" }
|
138
126
|
|
data/lib/shoryuken/version.rb
CHANGED
@@ -6,25 +6,31 @@ require 'securerandom'
|
|
6
6
|
RSpec.describe Shoryuken::Launcher do
|
7
7
|
describe 'Consuming messages', slow: :true do
|
8
8
|
before do
|
9
|
+
Aws.config[:stub_responses] = false
|
10
|
+
Aws.config[:region] = 'us-east-1'
|
11
|
+
|
9
12
|
StandardWorker.received_messages = 0
|
10
13
|
|
11
|
-
queue = "
|
14
|
+
queue = "shoryuken-travis-#{StandardWorker}-#{SecureRandom.uuid}"
|
12
15
|
|
13
|
-
Shoryuken::Client.sqs.create_queue
|
16
|
+
Shoryuken::Client.sqs.create_queue(queue_name: queue)
|
14
17
|
|
15
|
-
Shoryuken.
|
18
|
+
Shoryuken.add_group('default', 1)
|
19
|
+
Shoryuken.add_queue(queue, 1, 'default')
|
16
20
|
|
17
21
|
StandardWorker.get_shoryuken_options['queue'] = queue
|
18
22
|
|
19
|
-
Shoryuken.register_worker
|
23
|
+
Shoryuken.register_worker(queue, StandardWorker)
|
20
24
|
end
|
21
25
|
|
22
26
|
after do
|
27
|
+
Aws.config[:stub_responses] = true
|
28
|
+
|
23
29
|
queue_url = Shoryuken::Client.sqs.get_queue_url(
|
24
30
|
queue_name: StandardWorker.get_shoryuken_options['queue']
|
25
31
|
).queue_url
|
26
32
|
|
27
|
-
Shoryuken::Client.sqs.delete_queue
|
33
|
+
Shoryuken::Client.sqs.delete_queue(queue_url: queue_url)
|
28
34
|
end
|
29
35
|
|
30
36
|
it 'consumes as a command worker' do
|
@@ -61,7 +67,7 @@ RSpec.describe Shoryuken::Launcher do
|
|
61
67
|
end
|
62
68
|
|
63
69
|
def poll_queues_until
|
64
|
-
subject.
|
70
|
+
subject.start
|
65
71
|
|
66
72
|
Timeout::timeout(10) do
|
67
73
|
begin
|
@@ -15,20 +15,11 @@ RSpec.describe Shoryuken::EnvironmentLoader do
|
|
15
15
|
allow(subject).to receive(:patch_deprecated_workers)
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
Shoryuken.options[:queues] = ['
|
18
|
+
specify do
|
19
|
+
Shoryuken.options[:queues] = ['queue1', ['queue2', 2]]
|
20
20
|
subject.load
|
21
21
|
|
22
|
-
expect(Shoryuken.queues).to eq(%w(
|
23
|
-
end
|
24
|
-
|
25
|
-
context 'with priority' do
|
26
|
-
it 'parses' do
|
27
|
-
Shoryuken.options[:queues] = ['queue_1', ['queue_2', 2]]
|
28
|
-
subject.load
|
29
|
-
|
30
|
-
expect(Shoryuken.queues).to eq(%w(queue_1 queue_2 queue_2))
|
31
|
-
end
|
22
|
+
expect(Shoryuken.groups['default'][:queues]).to eq(%w(queue1 queue2 queue2))
|
32
23
|
end
|
33
24
|
end
|
34
25
|
end
|
@@ -2,36 +2,51 @@ require 'spec_helper'
|
|
2
2
|
require 'shoryuken/manager'
|
3
3
|
require 'shoryuken/fetcher'
|
4
4
|
|
5
|
-
describe Shoryuken::Fetcher do
|
6
|
-
let(:queue)
|
7
|
-
let(:queue_name)
|
5
|
+
RSpec.describe Shoryuken::Fetcher do
|
6
|
+
let(:queue) { instance_double('Shoryuken::Queue') }
|
7
|
+
let(:queue_name) { 'default' }
|
8
8
|
let(:queue_config) { Shoryuken::Polling::QueueConfiguration.new(queue_name, {}) }
|
9
|
+
let(:group) { 'default' }
|
9
10
|
|
10
11
|
let(:sqs_msg) do
|
11
|
-
double(
|
12
|
+
double(
|
13
|
+
Shoryuken::Message,
|
12
14
|
queue_url: queue_name,
|
13
15
|
body: 'test',
|
14
16
|
message_id: 'fc754df79cc24c4196ca5996a44b771e',
|
15
|
-
|
17
|
+
)
|
16
18
|
end
|
17
19
|
|
18
|
-
subject { described_class.new }
|
20
|
+
subject { described_class.new(group) }
|
19
21
|
|
20
22
|
describe '#fetch' do
|
21
|
-
|
23
|
+
let(:limit) { 1 }
|
24
|
+
|
25
|
+
specify do
|
22
26
|
expect(Shoryuken::Client).to receive(:queues).with(queue_name).and_return(queue)
|
27
|
+
|
28
|
+
Shoryuken.sqs_client_receive_message_opts[group] = { wait_time_seconds: 10 }
|
29
|
+
|
23
30
|
expect(queue).to receive(:receive_messages).
|
24
|
-
with(max_number_of_messages:
|
31
|
+
with(wait_time_seconds: 10, max_number_of_messages: limit, message_attribute_names: ['All'], attribute_names: ['All']).
|
25
32
|
and_return([])
|
26
|
-
|
33
|
+
|
34
|
+
subject.fetch(queue_config, limit)
|
27
35
|
end
|
28
36
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
37
|
+
context 'when limit is greater than FETCH_LIMIT' do
|
38
|
+
let(:limit) { 20 }
|
39
|
+
|
40
|
+
specify do
|
41
|
+
Shoryuken.sqs_client_receive_message_opts[group] = {}
|
42
|
+
|
43
|
+
allow(Shoryuken::Client).to receive(:queues).with(queue_name).and_return(queue)
|
44
|
+
expect(queue).to receive(:receive_messages).
|
45
|
+
with(max_number_of_messages: described_class::FETCH_LIMIT, attribute_names: ['All'], message_attribute_names: ['All']).
|
46
|
+
and_return([])
|
47
|
+
|
48
|
+
subject.fetch(queue_config, limit)
|
49
|
+
end
|
35
50
|
end
|
36
51
|
end
|
37
52
|
end
|