shoryuken 0.0.4 → 0.0.5

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.
@@ -49,6 +49,10 @@ module Shoryuken
49
49
  yield self if block_given?
50
50
  end
51
51
 
52
+ def dup
53
+ self.class.new.tap { |new_chain| new_chain.entries.replace(entries) }
54
+ end
55
+
52
56
  def remove(klass)
53
57
  entries.delete_if { |entry| entry.klass == klass }
54
58
  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.class.to_s}/#{queue}/#{sqs_msg.id}") do
8
+ Shoryuken::Logging.with_context("#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.id}") do
9
9
  begin
10
10
  started_at = Time.now
11
11
 
@@ -9,22 +9,48 @@ module Shoryuken
9
9
  @manager = manager
10
10
  end
11
11
 
12
+ attr_accessor :proxy_id
13
+
12
14
  def process(queue, sqs_msg)
13
- worker = Shoryuken.worker_loader.call(queue, sqs_msg)
15
+ @manager.async.real_thread(proxy_id, Thread.current)
16
+
17
+ worker = Shoryuken.worker_registry.fetch_worker(queue, sqs_msg)
18
+
19
+ timer = auto_visibility_timeout(queue, sqs_msg, worker.class)
14
20
 
15
- defer do
16
- body = get_body(worker.class, sqs_msg)
21
+ begin
22
+ defer do
23
+ body = get_body(worker.class, sqs_msg)
17
24
 
18
- Shoryuken.server_middleware.invoke(worker, queue, sqs_msg, body) do
19
- worker.perform(sqs_msg, body)
25
+ worker.class.server_middleware.invoke(worker, queue, sqs_msg, body) do
26
+ worker.perform(sqs_msg, body)
27
+ end
20
28
  end
21
- end
22
29
 
23
- @manager.async.processor_done(queue, current_actor)
30
+ @manager.async.processor_done(queue, current_actor)
31
+ ensure
32
+ timer.cancel if timer
33
+ end
24
34
  end
25
35
 
26
36
  private
27
37
 
38
+ def auto_visibility_timeout(queue, sqs_msg, worker_class)
39
+ if worker_class.auto_visibility_timeout?
40
+ timer = every(worker_class.visibility_timeout_heartbeat) do
41
+ begin
42
+ logger.debug "Extending message #{worker_name(worker_class, sqs_msg)}/#{queue}/#{sqs_msg.id} visibility timeout to #{worker_class.extended_visibility_timeout}"
43
+
44
+ sqs_msg.visibility_timeout = worker_class.extended_visibility_timeout
45
+ rescue => e
46
+ logger.error "Could not auto extend the message #{worker_class}/#{queue}/#{sqs_msg.id} visibility timeout. Error: #{e.message}"
47
+ end
48
+ end
49
+ end
50
+
51
+ timer
52
+ end
53
+
28
54
  def get_body(worker_class, sqs_msg)
29
55
  if sqs_msg.is_a? Array
30
56
  sqs_msg.map { |m| parse_body(worker_class, m) }
@@ -23,5 +23,18 @@ module Shoryuken
23
23
  queue_and_weights
24
24
  end.to_a
25
25
  end
26
+
27
+ def worker_name(worker_class, sqs_msg, body = nil)
28
+ if defined?(::ActiveJob) \
29
+ && !sqs_msg.is_a?(Array) \
30
+ && sqs_msg.message_attributes \
31
+ && sqs_msg.message_attributes['shoryuken_class'] \
32
+ && sqs_msg.message_attributes['shoryuken_class'][:string_value] == ActiveJob::QueueAdapters::ShoryukenAdapter::JobWrapper.to_s
33
+
34
+ "ActiveJob/#{body['job_class'].constantize}"
35
+ else
36
+ worker_class.to_s
37
+ end
38
+ end
26
39
  end
27
40
  end
@@ -1,3 +1,3 @@
1
1
  module Shoryuken
2
- VERSION = '0.0.4'
2
+ VERSION = '0.0.5'
3
3
  end
@@ -30,6 +30,12 @@ module Shoryuken
30
30
 
31
31
  alias_method :perform_at, :perform_in
32
32
 
33
+ def server_middleware
34
+ @server_chain ||= Shoryuken.server_middleware.dup
35
+ yield @server_chain if block_given?
36
+ @server_chain
37
+ end
38
+
33
39
  def shoryuken_options(opts = {})
34
40
  @shoryuken_options = get_shoryuken_options.merge(stringify_keys(Hash(opts)))
35
41
  queue = @shoryuken_options['queue']
@@ -41,8 +47,20 @@ module Shoryuken
41
47
  Shoryuken.register_worker(queue, self)
42
48
  end
43
49
 
50
+ def auto_visibility_timeout?
51
+ !!get_shoryuken_options['auto_visibility_timeout']
52
+ end
53
+
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
+
44
62
  def get_shoryuken_options # :nodoc:
45
- @shoryuken_options || { 'queue' => 'default', 'delete' => false, 'auto_delete' => false, 'batch' => false }
63
+ @shoryuken_options || Shoryuken.default_worker_options
46
64
  end
47
65
 
48
66
  def stringify_keys(hash) # :nodoc:
@@ -0,0 +1,34 @@
1
+ module Shoryuken
2
+ class WorkerRegistry
3
+ def batch_receive_messages?(queue)
4
+ # true if the workers for queue support batch processing of messages
5
+ fail NotImplementedError
6
+ end
7
+
8
+ def clear
9
+ # must remove all worker registrations
10
+ fail NotImplementedError
11
+ end
12
+
13
+ def fetch_worker(queue, message)
14
+ # must return an instance of the worker that handles
15
+ # message received on queue
16
+ fail NotImplementedError
17
+ end
18
+
19
+ def queues
20
+ # must return a list of all queues with registered workers
21
+ fail NotImplementedError
22
+ end
23
+
24
+ def register_worker(queue, clazz)
25
+ # must register the worker as a consumer of messages from queue
26
+ fail NotImplementedError
27
+ end
28
+
29
+ def workers(queue)
30
+ # must return the list of workers registered for queue, or []
31
+ fail NotImplementedError
32
+ end
33
+ end
34
+ end
data/shoryuken.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["pablo@pablocantero.com"]
11
11
  spec.description = spec.summary = %q{Shoryuken is a super efficient AWS SQS thread based message processor}
12
12
  spec.homepage = "https://github.com/phstc/shoryuken"
13
- spec.license = "MIT"
13
+ spec.license = "LGPL-3.0"
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0")
16
16
  spec.executables = %w[shoryuken]
@@ -23,5 +23,5 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency "pry-byebug"
24
24
 
25
25
  spec.add_dependency "aws-sdk-v1"
26
- spec.add_dependency "celluloid", "~> 0.15.2"
26
+ spec.add_dependency "celluloid", "~> 0.16.0"
27
27
  end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shoryuken::DefaultWorkerRegistry do
4
+ class RegistryTestWorker
5
+ include Shoryuken::Worker
6
+
7
+ shoryuken_options queue: 'registry-test'
8
+
9
+ def perform(sqs_msg, body); end
10
+ end
11
+
12
+ subject do
13
+ Shoryuken.worker_registry
14
+ end
15
+
16
+ before do
17
+ subject.register_worker 'registry-test', RegistryTestWorker
18
+ end
19
+
20
+ describe 'a registry containing workers is cleared' do
21
+ it 'removes all registrations' do
22
+ queue = 'some-other-queue'
23
+
24
+ registry = described_class.new
25
+
26
+ worker_class = Class.new do
27
+ include Shoryuken::Worker
28
+
29
+ shoryuken_options queue: queue
30
+
31
+ def perform(sqs_msg, body); end
32
+ end
33
+
34
+ registry.register_worker(queue, worker_class)
35
+
36
+ expect(registry.workers(queue)).to eq([worker_class])
37
+
38
+ registry.clear
39
+
40
+ expect(registry.workers(queue)).to eq([])
41
+ end
42
+ end
43
+
44
+ describe 'a registry with workers is handling messages' do
45
+ def build_message queue, explicit_worker = nil
46
+ attributes = {}
47
+ attributes['shoryuken_class'] = {
48
+ string_value: explicit_worker.to_s,
49
+ data_type: 'String' } if explicit_worker
50
+
51
+ double AWS::SQS::ReceivedMessage,
52
+ body: 'test',
53
+ message_attributes: attributes,
54
+ message_id: SecureRandom.uuid
55
+ end
56
+
57
+ context 'a batch of messages is being processed' do
58
+ it 'returns an instance of the worker registered for that queue' do
59
+ batch = [build_message('default', RegistryTestWorker)]
60
+ expect(subject.fetch_worker('default', batch)).to be_instance_of(TestWorker)
61
+ end
62
+ end
63
+
64
+ context 'a single message is being processed' do
65
+ context 'a worker class name is included in the message attributes' do
66
+ it 'returns an instance of that worker' do
67
+ message = build_message('default', RegistryTestWorker)
68
+ expect(subject.fetch_worker('default', message)).to be_instance_of(RegistryTestWorker)
69
+ end
70
+ end
71
+
72
+ context 'a worker class name is not included in the message attributes' do
73
+ it 'returns an instance of the worker registered for that queue' do
74
+ message = build_message('default')
75
+ expect(subject.fetch_worker('default', message)).to be_instance_of(TestWorker)
76
+
77
+ message = build_message('registry-test')
78
+ expect(subject.fetch_worker('registry-test', message)).to be_instance_of(RegistryTestWorker)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -2,6 +2,12 @@ require 'spec_helper'
2
2
  require 'shoryuken/manager'
3
3
 
4
4
  describe Shoryuken::Manager do
5
+ subject do
6
+ condvar = double(:condvar)
7
+ allow(condvar).to receive(:signal).and_return(nil)
8
+ Shoryuken::Manager.new(condvar)
9
+ end
10
+
5
11
  describe 'Auto Scaling' do
6
12
  it 'decreases weight' do
7
13
  queue1 = 'shoryuken'
@@ -12,11 +12,12 @@ describe Shoryuken::Processor do
12
12
 
13
13
  before do
14
14
  allow(manager).to receive(:async).and_return(manager)
15
+ allow(manager).to receive(:real_thread)
15
16
  allow(Shoryuken::Client).to receive(:queues).with(queue).and_return(sqs_queue)
16
17
  end
17
18
 
18
19
  describe '#process' do
19
- it 'parsers the body into JSON' do
20
+ it 'parses the body into JSON' do
20
21
  TestWorker.get_shoryuken_options['body_parser'] = :json
21
22
 
22
23
  body = { 'test' => 'hi' }
@@ -28,7 +29,7 @@ describe Shoryuken::Processor do
28
29
  subject.process(queue, sqs_msg)
29
30
  end
30
31
 
31
- it 'parsers the body calling the proc' do
32
+ it 'parses the body calling the proc' do
32
33
  TestWorker.get_shoryuken_options['body_parser'] = Proc.new { |sqs_msg| "*#{sqs_msg.body}*" }
33
34
 
34
35
  expect_any_instance_of(TestWorker).to receive(:perform).with(sqs_msg, '*test*')
@@ -38,7 +39,7 @@ describe Shoryuken::Processor do
38
39
  subject.process(queue, sqs_msg)
39
40
  end
40
41
 
41
- it 'parsers the body as text' do
42
+ it 'parses the body as text' do
42
43
  TestWorker.get_shoryuken_options['body_parser'] = :text
43
44
 
44
45
  body = 'test'
@@ -50,7 +51,7 @@ describe Shoryuken::Processor do
50
51
  subject.process(queue, sqs_msg)
51
52
  end
52
53
 
53
- it 'parsers calling `.parse`' do
54
+ it 'parses calling `.parse`' do
54
55
  TestWorker.get_shoryuken_options['body_parser'] = JSON
55
56
 
56
57
  body = { 'test' => 'hi' }
@@ -77,7 +78,7 @@ describe Shoryuken::Processor do
77
78
  end
78
79
 
79
80
  context 'when `object_type: nil`' do
80
- it 'parsers the body as text' do
81
+ it 'parses the body as text' do
81
82
  TestWorker.get_shoryuken_options['body_parser'] = nil
82
83
 
83
84
  body = 'test'
@@ -91,6 +92,8 @@ describe Shoryuken::Processor do
91
92
  end
92
93
 
93
94
  context 'when custom middleware' do
95
+ let(:queue) { 'worker_called_middleware' }
96
+
94
97
  class WorkerCalledMiddleware
95
98
  def call(worker, queue, sqs_msg, body)
96
99
  # called is defined with `allow(...).to receive(...)`
@@ -100,6 +103,14 @@ describe Shoryuken::Processor do
100
103
  end
101
104
 
102
105
  before do
106
+ class WorkerCalledMiddlewareWorker
107
+ include Shoryuken::Worker
108
+
109
+ shoryuken_options queue: 'worker_called_middleware'
110
+
111
+ def perform(sqs_msg, body); end
112
+ end
113
+
103
114
  Shoryuken.configure_server do |config|
104
115
  config.server_middleware do |chain|
105
116
  chain.add WorkerCalledMiddleware
@@ -118,8 +129,8 @@ describe Shoryuken::Processor do
118
129
  it 'invokes middleware' do
119
130
  expect(manager).to receive(:processor_done).with(queue, subject)
120
131
 
121
- expect_any_instance_of(TestWorker).to receive(:perform).with(sqs_msg, sqs_msg.body)
122
- expect_any_instance_of(TestWorker).to receive(:called).with(sqs_msg, queue)
132
+ expect_any_instance_of(WorkerCalledMiddlewareWorker).to receive(:perform).with(sqs_msg, sqs_msg.body)
133
+ expect_any_instance_of(WorkerCalledMiddlewareWorker).to receive(:called).with(sqs_msg, queue)
123
134
 
124
135
  subject.process(queue, sqs_msg)
125
136
  end
@@ -158,7 +169,7 @@ describe Shoryuken::Processor do
158
169
  } }
159
170
 
160
171
  it 'performs without delete' do
161
- Shoryuken.workers.clear # unregister TestWorker
172
+ Shoryuken.worker_registry.clear # unregister TestWorker
162
173
 
163
174
  expect(manager).to receive(:processor_done).with(queue, subject)
164
175
 
@@ -170,4 +181,50 @@ describe Shoryuken::Processor do
170
181
  end
171
182
  end
172
183
  end
184
+
185
+ describe '#auto_visibility_timeout' do
186
+ let(:heartbeat) { sqs_queue.visibility_timeout - 5 }
187
+ let(:visibility_timeout) { sqs_queue.visibility_timeout }
188
+
189
+ before do
190
+ TestWorker.get_shoryuken_options['auto_visibility_timeout'] = true
191
+
192
+ allow(sqs_queue).to receive(:visibility_timeout).and_return(15)
193
+ end
194
+
195
+ context 'when the worker takes a long time', slow: true do
196
+ it 'extends the message invisibility to prevent it from being dequeued concurrently' do
197
+ TestWorker.get_shoryuken_options['body_parser'] = Proc.new do |sqs_msg|
198
+ sleep visibility_timeout
199
+ 'test'
200
+ end
201
+
202
+ expect(sqs_msg).to receive(:visibility_timeout=).with(visibility_timeout).once
203
+ expect(manager).to receive(:processor_done).with(queue, subject)
204
+
205
+ allow(sqs_msg).to receive(:body).and_return('test')
206
+
207
+ subject.process(queue, sqs_msg)
208
+ end
209
+ end
210
+
211
+ context 'when the worker takes a short time' do
212
+ it 'does not extend the message invisibility' do
213
+ expect(sqs_msg).to receive(:visibility_timeout=).never
214
+ expect(manager).to receive(:processor_done).with(queue, subject)
215
+
216
+ allow(sqs_msg).to receive(:body).and_return('test')
217
+
218
+ subject.process(queue, sqs_msg)
219
+ end
220
+ end
221
+
222
+ context 'when the worker fails' do
223
+ it 'does not extend the message invisibility' do
224
+ expect(sqs_msg).to receive(:visibility_timeout=).never
225
+ expect_any_instance_of(TestWorker).to receive(:perform).and_raise 'worker failed'
226
+ expect { subject.process(queue, sqs_msg) }.to raise_error
227
+ end
228
+ end
229
+ end
173
230
  end
@@ -14,4 +14,14 @@ describe 'Shoryuken::Util' do
14
14
  expect(subject.unparse_queues(queues)).to eq([['queue1', 2], ['queue2', 1], ['queue3', 1], ['queue4', 3]])
15
15
  end
16
16
  end
17
+
18
+ describe '#worker_name' do
19
+ let(:sqs_msg) { double AWS::SQS::ReceivedMessage, id: 'fc754df7-9cc2-4c41-96ca-5996a44b771e', message_attributes: {} }
20
+
21
+ it 'returns Shoryuken worker name' do
22
+ expect(subject.worker_name(TestWorker, sqs_msg)).to eq 'TestWorker'
23
+ end
24
+
25
+ it 'returns ActiveJob worker name'
26
+ end
17
27
  end