proletariat 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f18e4d2458922ab3466f6913a079616c7ab3e5ab
4
- data.tar.gz: 0ed0637283e0056fca8e260bbb4fe97dc26eca1e
3
+ metadata.gz: 857a43f0570e09ed05e46f910b5a746474116604
4
+ data.tar.gz: 4c5a325c9d11bf52e1ac69b5c1c365a291ecfcc7
5
5
  SHA512:
6
- metadata.gz: f6e31240a83223f4d67f2f0e08ae194927f07b6671fa193b32e7ed6527d37e66de29ff7830047cbbeb23a300b19eb43b1ad5765546ac51f406bdabd0d83cecee
7
- data.tar.gz: 431900e4e0b1f59427666ceeca3197297c3723441c0f472242ee40dda0e3afe148bbc63cde95d10818a65358130a3ea83e46006042696385d6f4d7bd6a07dd3a
6
+ metadata.gz: 00801d41f5a02e885f82a15bcf4d189e3f5ebe80892afe982cec5efe2f3effe3dbbe3e1aca7e8477110907045968bb991ea83ddeac1f15ef844eaba287428049
7
+ data.tar.gz: b13a39c9cb321ade6df9a2566d00942edd1ddb35339cae3102e2b1d33279a11f585e8aeef0e7a494cb7334d08255b5308c03e4f748fbef9b0a5b632c56937486
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.0.5
2
+
3
+ Features:
4
+
5
+ - Added access to message headers in #work (breaking change).
6
+ - Added exponential back-off for failing workers.
7
+
1
8
  ## 0.0.4
2
9
 
3
10
  Features:
data/README.md CHANGED
@@ -41,7 +41,7 @@ Here's a complete example:
41
41
  class SendUserIntroductoryEmail < Proletariat::Worker
42
42
  listen_on 'user.created'
43
43
 
44
- def work(message, key)
44
+ def work(message, key, headers)
45
45
  params = JSON.parse(message)
46
46
 
47
47
  UserMailer.introductory_email(params).deliver!
@@ -4,6 +4,9 @@ module Proletariat
4
4
  # Public: The default name used for the RabbitMQ topic exchange.
5
5
  DEFAULT_EXCHANGE_NAME = 'proletariat'
6
6
 
7
+ # Public: The default maximum seconds to delay a failed job requeue.
8
+ DEFAULT_MAX_RETRY_DELAY = (ENV['MAX_RETRY_DELAY'] || 60).to_i
9
+
7
10
  # Public: The default number of threads to use for publishers.
8
11
  DEFAULT_PUBLISHER_THREADS = (ENV['PUBLISHER_THREADS'] || 2).to_i
9
12
 
@@ -19,6 +22,9 @@ module Proletariat
19
22
  # Internal: Sets the logger.
20
23
  attr_writer :logger
21
24
 
25
+ # Internal: Sets the maximum seconds to delay a failed job requeue.
26
+ attr_writer :max_retry_delay
27
+
22
28
  # Internal: Sets the number of threads to use for publishers.
23
29
  attr_writer :publisher_threads
24
30
 
@@ -67,6 +73,13 @@ module Proletariat
67
73
  @logger ||= Logger.new(STDOUT)
68
74
  end
69
75
 
76
+ # Public: Returns the set max delay seconds or a default.
77
+ #
78
+ # Returns a Fixnum.
79
+ def max_retry_delay
80
+ @max_retry_delay ||= DEFAULT_MAX_RETRY_DELAY
81
+ end
82
+
70
83
  # Public: Returns the set number of publisher threads or a default.
71
84
  #
72
85
  # Returns a Fixnum.
@@ -44,10 +44,12 @@ module Proletariat
44
44
  # with the RabbitMQ convention you can use the '*' character to
45
45
  # replace one word and the '#' to replace many words.
46
46
  # message - The message as a String.
47
+ # headers - Hash of message headers.
47
48
  #
48
49
  # Returns nil.
49
- def work(to, message)
50
- exchange.publish message, routing_key: to, persistent: true
50
+ def work(to, message, headers)
51
+ exchange.publish message, routing_key: to, persistent: true,
52
+ headers: headers
51
53
 
52
54
  nil
53
55
  end
@@ -25,10 +25,11 @@ module Proletariat
25
25
  # with the RabbitMQ convention you can use the '*' character to
26
26
  # replace one word and the '#' to replace many words.
27
27
  # message - The message as a String.
28
+ # headers - Hash of message headers.
28
29
  #
29
30
  # Returns nil.
30
- def publish(to, message)
31
- supervisor['publishers'].post to, message
31
+ def publish(to, message = '', headers = {})
32
+ supervisor['publishers'].post to, message, headers
32
33
 
33
34
  nil
34
35
  end
@@ -58,6 +58,8 @@ module Proletariat
58
58
  acknowledger.acknowledge_on_channel channel
59
59
  acknowledgers.delete acknowledger
60
60
  end
61
+
62
+ completed_retries.each { |r| scheduled_retries.delete r }
61
63
  end
62
64
 
63
65
  # Public: Purge the RabbitMQ queue.
@@ -102,6 +104,39 @@ module Proletariat
102
104
  nil
103
105
  end
104
106
 
107
+ # Internal: Get scheduled retries whose messages have been requeued.
108
+ #
109
+ # Returns an Array of Retrys.
110
+ def completed_retries
111
+ scheduled_retries.select { |r| r.requeued? }
112
+ end
113
+
114
+ # Internal: Forwards all message bodies to listener#post. Auto-acks
115
+ # messages not meant for this subscriber's workers.
116
+ #
117
+ # Returns nil.
118
+ def handle_message(info, properties, body)
119
+ if handles_worker_type? properties.headers['worker']
120
+ future = listener.post?(body, info.routing_key, properties.headers)
121
+ acknowledgers << Acknowledger.new(future, info.delivery_tag, {
122
+ message: body, key: info.routing_key, headers: properties.headers,
123
+ worker: queue_config.queue_name }, scheduled_retries)
124
+ else
125
+ channel.acknowledge info.delivery_tag
126
+ end
127
+
128
+ nil
129
+ end
130
+
131
+ # Internal: Checks if subscriber should handle message for given worker
132
+ # header.
133
+ #
134
+ # Returns true if should be handled or header is nil.
135
+ # Returns false if should not be handled.
136
+ def handles_worker_type?(worker_header)
137
+ [nil, queue_config.queue_name].include? worker_header
138
+ end
139
+
105
140
  # Internal: Get acknowledgers for messages whose work has completed.
106
141
  #
107
142
  # Returns an Array of Acknowledgers.
@@ -111,14 +146,18 @@ module Proletariat
111
146
  end
112
147
  end
113
148
 
149
+ def scheduled_retries
150
+ @scheduled_retries ||= []
151
+ end
152
+
114
153
  # Internal: Starts a consumer on the queue. The consumer forwards all
115
- # message bodies to listener#post.
154
+ # message bodies to listener#post. Auto-acks messages not meant
155
+ # for this subscriber's workers.
116
156
  #
117
157
  # Returns nil.
118
158
  def start_consumer
119
159
  @consumer = bunny_queue.subscribe ack: true do |info, properties, body|
120
- future = listener.post?(body, info.routing_key)
121
- acknowledgers << Acknowledger.new(future, info.delivery_tag)
160
+ handle_message info, properties, body
122
161
 
123
162
  nil
124
163
  end
@@ -133,6 +172,7 @@ module Proletariat
133
172
  def stop_consumer
134
173
  @consumer.cancel if @consumer
135
174
  wait_for_acknowledgers if acknowledgers.any?
175
+ scheduled_retries.each { |r| r.expedite }
136
176
 
137
177
  nil
138
178
  end
@@ -160,11 +200,15 @@ module Proletariat
160
200
 
161
201
  # Public: Creates a new Acknowledger instance.
162
202
  #
163
- # future - A future-like object holding the Worker response.
164
- # delivery_tag - The RabbitMQ delivery tag to be used when ack/nacking.
165
- def initialize(future, delivery_tag)
166
- @future = future
167
- @delivery_tag = delivery_tag
203
+ # future - A future-like object holding the Worker response.
204
+ # delivery_tag - The RabbitMQ delivery tag for ack/nacking.
205
+ # properties - The original message properties; for requeuing.
206
+ # scheduled_retries - An Array to hold any created Retrys.
207
+ def initialize(future, delivery_tag, properties, scheduled_retries)
208
+ @future = future
209
+ @delivery_tag = delivery_tag
210
+ @properties = properties
211
+ @scheduled_retries = scheduled_retries
168
212
  end
169
213
 
170
214
  # Public: Retrieves the value from the future and sends the relevant
@@ -232,7 +276,9 @@ module Proletariat
232
276
  # Returns nil.
233
277
  def acknowledge_error(channel)
234
278
  Proletariat.logger.error future.reason
235
- channel.reject delivery_tag, true
279
+
280
+ scheduled_retries << Retry.new(properties)
281
+ channel.acknowledge delivery_tag
236
282
 
237
283
  nil
238
284
  end
@@ -242,6 +288,85 @@ module Proletariat
242
288
 
243
289
  # Internal: Returns the future-like object holding the Worker response.
244
290
  attr_reader :future
291
+
292
+ # Internal: Returns the original message properties.
293
+ attr_reader :properties
294
+
295
+ # Internal: Returns the Array of Retrys.
296
+ attr_reader :scheduled_retries
297
+
298
+ # Internal: Used publish an exponential delayed requeue for failures.
299
+ class Retry
300
+ # Public: Creates a new Retry instance. Sets appropriate headers for
301
+ # requeue message.
302
+ #
303
+ # properties - The original message properties.
304
+ def initialize(properties)
305
+ @properties = properties
306
+
307
+ properties[:headers]['failures'] = failures
308
+ properties[:headers]['worker'] = properties[:worker]
309
+
310
+ @scheduled_task = Concurrent::ScheduledTask.new(retry_delay) do
311
+ requeue_message
312
+ end
313
+ end
314
+
315
+ # Public: Attempt to requeue the message immediately if pending or
316
+ # wait for natural completion.
317
+ #
318
+ # Returns nil.
319
+ def expedite
320
+ if scheduled_task.cancel
321
+ requeue_message
322
+ else
323
+ scheduled_task.value
324
+ end
325
+
326
+ nil
327
+ end
328
+
329
+ # Public: Tests whether the message has been requeued.
330
+ #
331
+ # Returns a Boolean.
332
+ def requeued?
333
+ scheduled_task.fulfilled?
334
+ end
335
+
336
+ private
337
+
338
+ # Internal: Returns the original message properties.
339
+ attr_reader :properties
340
+
341
+ # Internal: Returns the ScheduledTask which will requeue the message.
342
+ attr_reader :scheduled_task
343
+
344
+ # Internal: Fetches the current number of message failures from the
345
+ # headers. Defaults to 1.
346
+ #
347
+ # Returns a Fixnum.
348
+ def failures
349
+ @failures ||= (properties[:headers]['failures'] || 0) + 1
350
+ end
351
+
352
+ # Internal: Performs the actual message requeue.
353
+ #
354
+ # Returns nil.
355
+ def requeue_message
356
+ Proletariat.publish(properties[:key], properties[:message],
357
+ properties[:headers])
358
+
359
+ nil
360
+ end
361
+
362
+ # Internal: Calculates an exponential retry delay based on the previous
363
+ # number of failures. Capped with configuration setting.
364
+ #
365
+ # Returns the delay in seconds as a Fixnum.
366
+ def retry_delay
367
+ [2**failures, Proletariat.max_retry_delay].min
368
+ end
369
+ end
245
370
  end
246
371
  end
247
372
  end
@@ -125,10 +125,12 @@ module Proletariat
125
125
  # Public: Handles message calls from a subscriber and increments the
126
126
  # count. Return value matches interface expected by Subscriber.
127
127
  #
128
- # message - The contents of the message.
128
+ # message - The contents of the message.
129
+ # routing_key - Routing key for messages.
130
+ # headers - Hash of message headers.
129
131
  #
130
132
  # Returns a future-like object holding an :ok Symbol.
131
- def post?(message, routing_key)
133
+ def post?(message, routing_key, headers)
132
134
  self.count = count + 1
133
135
 
134
136
  Concurrent::Future.new { :ok }
@@ -1,4 +1,4 @@
1
1
  # Public: Adds a constant for the current version number.
2
2
  module Proletariat
3
- VERSION = '0.0.4'
3
+ VERSION = '0.0.5'
4
4
  end
@@ -38,7 +38,7 @@ module Proletariat
38
38
  # message - The incoming message.
39
39
  #
40
40
  # Raises NotImplementedError unless implemented in subclass.
41
- def work(message, routing_key)
41
+ def work(message, routing_key, headers)
42
42
  fail NotImplementedError
43
43
  end
44
44
 
@@ -71,11 +71,12 @@ module Proletariat
71
71
  # with the RabbitMQ convention you can use the '*' character to
72
72
  # replace one word and the '#' to replace many words.
73
73
  # message - The message as a String.
74
+ # headers - Hash of message headers.
74
75
  #
75
76
  # Returns nil.
76
- def publish(to, message = '')
77
+ def publish(to, message = '', headers = {})
77
78
  log "Publishing to: #{to}"
78
- Proletariat.publish to, message
79
+ Proletariat.publish to, message, headers
79
80
 
80
81
  nil
81
82
  end
@@ -8,7 +8,7 @@ class PingWorker < Proletariat::Worker
8
8
  attr_accessor :pinged
9
9
  end
10
10
 
11
- def work(message, routing_key)
11
+ def work(message, routing_key, headers)
12
12
  self.class.pinged = true
13
13
 
14
14
  log 'PING'
@@ -24,10 +24,15 @@ class PongWorker < Proletariat::Worker
24
24
  listen_on :pong
25
25
 
26
26
  class << self
27
+ attr_accessor :fail_mode
27
28
  attr_accessor :ponged
28
29
  end
29
30
 
30
- def work(message, routing_key)
31
+ def work(message, routing_key, headers)
32
+ if self.class.fail_mode == true
33
+ fail 'Error' unless headers['failures'] == 2
34
+ end
35
+
31
36
  self.class.ponged = true
32
37
 
33
38
  log 'PONG'
@@ -39,18 +44,38 @@ class PongWorker < Proletariat::Worker
39
44
  end
40
45
 
41
46
  describe Proletariat do
42
- it 'should roughly work' do
47
+ before do
43
48
  Proletariat.configure do
44
49
  config.logger = Logger.new('/dev/null')
45
50
  config.worker_classes = [PingWorker, PongWorker]
46
51
  end
47
52
 
53
+ PongWorker.fail_mode = false
54
+
55
+ Proletariat.purge
48
56
  Proletariat.run!
49
57
  sleep 2
50
- Proletariat.publish 'ping', ''
51
- sleep 3
58
+ end
59
+
60
+ after do
52
61
  Proletariat.stop
53
62
  Proletariat.purge
63
+ PingWorker.pinged = false
64
+ PongWorker.ponged = false
65
+ end
66
+
67
+ it 'should roughly work' do
68
+ Proletariat.publish 'ping', ''
69
+ sleep 5
70
+
71
+ expect(PingWorker.pinged).to be_truthy
72
+ expect(PongWorker.ponged).to be_truthy
73
+ end
74
+
75
+ it 'should work in error conditions' do
76
+ PongWorker.fail_mode = true
77
+ Proletariat.publish 'ping', ''
78
+ sleep 10
54
79
 
55
80
  expect(PingWorker.pinged).to be_truthy
56
81
  expect(PongWorker.ponged).to be_truthy
@@ -34,19 +34,19 @@ module Proletariat
34
34
 
35
35
  it 'should increment the count' do
36
36
  counter = ExpectationGuarantor::MessageCounter.new(1)
37
- counter.post?('message', 'key')
37
+ counter.post?('message', 'key', {})
38
38
  expect(counter.expected_messages_received?).to be_truthy
39
39
  end
40
40
 
41
41
  it 'should return a future containing :ok' do
42
42
  counter = ExpectationGuarantor::MessageCounter.new(1)
43
43
  expect(Concurrent::Future).to receive(:new)
44
- counter.post?('message', 'key')
44
+ counter.post?('message', 'key', {})
45
45
  end
46
46
 
47
47
  it 'should ensure the returned future contains :ok' do
48
48
  counter = ExpectationGuarantor::MessageCounter.new(1)
49
- future = counter.post?('message', 'key')
49
+ future = counter.post?('message', 'key', {})
50
50
  expect(future.block.call).to eq :ok
51
51
  end
52
52
  end
@@ -31,7 +31,7 @@ module Proletariat
31
31
 
32
32
  describe '#work' do
33
33
  it 'should raise NotImplementedError' do
34
- expect { Worker.new.work('message', 'key') }.to \
34
+ expect { Worker.new.work('message', 'key', {}) }.to \
35
35
  raise_exception NotImplementedError
36
36
  end
37
37
  end
@@ -53,12 +53,12 @@ module Proletariat
53
53
 
54
54
  describe '#publish' do
55
55
  it 'should forward the message to the publisher' do
56
- expect(Proletariat).to receive(:publish).with('topic', 'message')
57
- Worker.new.publish 'topic', 'message'
56
+ expect(Proletariat).to receive(:publish).with('topic', 'message', {})
57
+ Worker.new.publish 'topic', 'message', {}
58
58
  end
59
59
 
60
60
  it 'should have a blank default message' do
61
- expect(Proletariat).to receive(:publish).with('topic', '')
61
+ expect(Proletariat).to receive(:publish).with('topic', '', {})
62
62
  Worker.new.publish 'topic'
63
63
  end
64
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proletariat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Edwards
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-17 00:00:00.000000000 Z
11
+ date: 2014-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec