proletariat 0.0.6 → 0.1.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.
@@ -2,35 +2,22 @@ module Proletariat
2
2
  # Public: Maintains a pool of worker threads and a RabbitMQ subscriber
3
3
  # thread. Uses information from the worker class to generate queue
4
4
  # config.
5
- class Manager
5
+ class Manager < Concurrent::Actor::RestartingContext
6
6
  # Public: Creates a new Manager instance.
7
7
  #
8
8
  # worker_class - A subclass of Proletariat::Worker to handle messages.
9
9
  def initialize(worker_class)
10
- @supervisor = Supervisor.new
11
-
12
- supervisor.supervise_pool('workers', Proletariat.worker_threads,
13
- worker_class)
14
-
15
- @subscriber = Subscriber.new(supervisor['workers'],
16
- generate_queue_config(worker_class))
17
-
18
- supervisor.add_worker subscriber
19
- end
20
-
21
- # Delegate lifecycle calls to supervisor. Cannot use Forwardable due to
22
- # concurrent-ruby API checking implementation.
23
- %w(run stop running?).each do |method|
24
- define_method(method) { supervisor.send method }
25
- end
26
-
27
- # Public: Purge the RabbitMQ queue.
28
- #
29
- # Returns nil.
30
- def purge
31
- subscriber.purge
32
-
33
- nil
10
+ @workers = worker_class.pool(Proletariat.worker_threads, object_id)
11
+
12
+ @subscriber = Subscriber.spawn!(
13
+ name: "#{worker_class.to_s}_subscriber_#{object_id}",
14
+ supervise: true,
15
+ args: [
16
+ workers,
17
+ generate_queue_config(worker_class),
18
+ get_exception_handler_class(worker_class)
19
+ ]
20
+ )
34
21
  end
35
22
 
36
23
  private
@@ -38,8 +25,22 @@ module Proletariat
38
25
  # Internal: Returns the Subscriber actor for this Manager.
39
26
  attr_reader :subscriber
40
27
 
41
- # Internal: The supervisor used to manage the Workers and Subscriber
42
- attr_reader :supervisor
28
+ # Internal: Returns an Array of Worker actors.
29
+ attr_reader :workers
30
+
31
+ def get_exception_handler_class(worker_class)
32
+ if worker_class.exception_handler.is_a?(ExceptionHandler)
33
+ worker_class.exception_handler
34
+ else
35
+ name = worker_class.exception_handler
36
+ .to_s
37
+ .split('_')
38
+ .map(&:capitalize)
39
+ .join
40
+
41
+ Proletariat.const_get(name)
42
+ end
43
+ end
43
44
 
44
45
  # Internal: Builds a new QueueConfig from a given Worker subclass.
45
46
  #
@@ -47,7 +48,8 @@ module Proletariat
47
48
  #
48
49
  # Returns a new QueueConfig instance.
49
50
  def generate_queue_config(worker_class)
50
- QueueConfig.new(worker_class.name, worker_class.routing_keys, false)
51
+ QueueConfig.new(worker_class.name, worker_class.routing_keys,
52
+ Proletariat.test_mode?)
51
53
  end
52
54
  end
53
55
  end
@@ -0,0 +1,9 @@
1
+ # Internal: Struct to store message details for passing around.
2
+ #
3
+ # to - The routing key for the message to as a String. In accordance
4
+ # with the RabbitMQ convention you can use the '*' character to
5
+ # replace one word and the '#' to replace many words.
6
+ # body - The message as a String.
7
+ # headers - Hash of message headers.
8
+ class Message < Struct.new(:to, :body, :headers)
9
+ end
@@ -2,64 +2,42 @@ require 'proletariat/concerns/logging'
2
2
 
3
3
  module Proletariat
4
4
  # Public: Receives messages and publishes them to a RabbitMQ topic exchange.
5
- class Publisher
5
+ class Publisher < PoolableActor
6
6
  include Concerns::Logging
7
7
 
8
- # Public: Creates a new Publisher instance.
9
- def initialize
10
- @channel = Proletariat.connection.create_channel
11
- @exchange = channel.topic(Proletariat.exchange_name, durable: true)
12
- end
13
-
14
- # Public: Logs the 'online' status of the publisher.
15
- #
16
- # Returns nil.
17
- def started
18
- log_info 'Now online'
19
-
20
- nil
21
- end
22
-
23
- # Public: Logs the 'offline' status of the publisher.
24
- #
25
- # Returns nil.
26
- def stopped
27
- log_info 'Now offline'
28
-
29
- nil
30
- end
31
-
32
- # Public: Logs the 'shutting down' status of the publisher.
8
+ # Public: Closes the Bunny::Channel if open.
33
9
  #
34
10
  # Returns nil.
35
- def stopping
36
- log_info 'Attempting graceful shutdown.'
11
+ def cleanup
12
+ @channel.close if @channel
37
13
 
38
14
  nil
39
15
  end
40
16
 
41
- # Public: Push a message to a RabbitMQ topic exchange.
17
+ # Public: Push a Message to a RabbitMQ topic exchange.
42
18
  #
43
- # to - The routing key for the message to as a String. In accordance
44
- # with the RabbitMQ convention you can use the '*' character to
45
- # replace one word and the '#' to replace many words.
46
- # message - The message as a String.
47
- # headers - Hash of message headers.
19
+ # message - A Message to send.
48
20
  #
49
21
  # Returns nil.
50
- def work(to, message, headers)
51
- exchange.publish message, routing_key: to, persistent: true,
52
- headers: headers
53
-
54
- nil
22
+ def work(message)
23
+ if message.is_a?(Message)
24
+ exchange.publish(message.body, routing_key: message.to,
25
+ persistent: !Proletariat.test_mode?,
26
+ headers: message.headers)
27
+ end
55
28
  end
56
29
 
57
30
  private
58
31
 
59
32
  # Internal: Returns the Bunny::Channel in use.
60
- attr_reader :channel
33
+ def channel
34
+ @channel ||= Proletariat.connection.create_channel
35
+ end
61
36
 
62
37
  # Internal: Returns the Bunny::Exchange in use.
63
- attr_reader :exchange
38
+ def exchange
39
+ @exchange ||= channel.topic(Proletariat.exchange_name,
40
+ durable: !Proletariat.test_mode?)
41
+ end
64
42
  end
65
43
  end
@@ -4,41 +4,32 @@ module Proletariat
4
4
  class Runner
5
5
  extend Forwardable
6
6
 
7
- # Public: Delegate lifecycle calls to the supervisor.
8
- def_delegators :supervisor, :run, :run!, :stop, :running?
9
-
10
- # Public: Creates a new Runner instance.
11
- def initialize
12
- @supervisor = Supervisor.new
13
- @managers = Proletariat.worker_classes.map do |worker_class|
14
- Manager.new(worker_class)
15
- end
16
-
17
- supervisor.supervise_pool('publishers', Proletariat.publisher_threads,
18
- Publisher)
19
- managers.each { |manager| supervisor.add_supervisor manager }
20
- end
21
-
22
- # Public: Publishes a message to RabbitMQ via the publisher pool.
23
- #
24
- # to - The routing key for the message to as a String. In accordance
25
- # with the RabbitMQ convention you can use the '*' character to
26
- # replace one word and the '#' to replace many words.
27
- # message - The message as a String.
28
- # headers - Hash of message headers.
7
+ # Public: Start the workers.
29
8
  #
30
9
  # Returns nil.
31
- def publish(to, message = '', headers = {})
32
- supervisor['publishers'].post to, message, headers
10
+ def run
11
+ @managers = Proletariat.worker_classes.map do |worker_class|
12
+ Manager.spawn!(name: "manager_#{worker_class.to_s}_#{object_id}",
13
+ supervise: true,
14
+ args: [worker_class])
15
+ end
16
+
17
+ managers.each { |manager| manager << :run }
33
18
 
34
19
  nil
35
20
  end
36
21
 
37
- # Public: Purge the RabbitMQ queues.
22
+ # Public: Check whether the workers are currently running.
23
+ def running?
24
+ !!managers
25
+ end
26
+
27
+ # Public: Stop the workers.
38
28
  #
39
29
  # Returns nil.
40
- def purge
41
- managers.each { |manager| manager.purge }
30
+ def stop
31
+ managers.each { |manager| manager << :terminate! } if managers
32
+ @managers = nil
42
33
 
43
34
  nil
44
35
  end
@@ -47,8 +38,5 @@ module Proletariat
47
38
 
48
39
  # Internal: Returns an Array of the currently supervised Managers.
49
40
  attr_reader :managers
50
-
51
- # Internal: Returns the supervisor instance.
52
- attr_reader :supervisor
53
41
  end
54
42
  end
@@ -1,86 +1,42 @@
1
1
  module Proletariat
2
2
  # Internal: Creates, binds and listens on a RabbitMQ queue. Forwards
3
3
  # messages to a given listener.
4
- class Subscriber
5
- include Concurrent::Runnable
6
-
4
+ class Subscriber < Actor
7
5
  include Concerns::Logging
8
6
 
9
7
  # Public: Creates a new Subscriber instance.
10
8
  #
11
9
  # listener - Object to delegate new messages to.
12
10
  # queue_config - A QueueConfig value object.
13
- def initialize(listener, queue_config)
14
- @listener = listener
15
- @queue_config = queue_config
16
-
17
- @channel = Proletariat.connection.create_channel
18
-
19
- @channel.prefetch Proletariat.worker_threads
20
-
21
- @exchange = @channel.topic Proletariat.exchange_name, durable: true
22
- @bunny_queue = @channel.queue queue_config.queue_name,
23
- durable: true,
24
- auto_delete: queue_config.auto_delete
11
+ def initialize(listener, queue_config, exception_handler_class)
12
+ @listener = listener
13
+ @queue_config = queue_config
14
+ @exception_handler_class = exception_handler_class
25
15
 
26
16
  bind_queue
27
- end
28
-
29
- # Internal: Called by the Concurrent framework on run. Used here to start
30
- # consumption of the queue and to log the status of the
31
- # subscriber.
32
- #
33
- # Returns nil.
34
- def on_run
35
17
  start_consumer
36
- log_info 'Now online'
37
-
38
- nil
39
- end
40
18
 
41
- # Internal: Called by the Concurrent framework on run. Used here to stop
42
- # consumption of the queue and to log the status of the
43
- # subscriber.
44
- #
45
- # Returns nil.
46
- def on_stop
47
- log_info 'Attempting graceful shutdown.'
48
- stop_consumer
49
- log_info 'Now offline'
50
- end
51
-
52
- # Internal: Called by the Concurrent framework to perform work. Used here
53
- # acknowledge RabbitMQ messages.
54
- #
55
- # Returns nil.
56
- def on_task
57
- ready_acknowledgers.each do |acknowledger|
58
- acknowledger.acknowledge_on_channel channel
59
- acknowledgers.delete acknowledger
19
+ @ticker = Concurrent::TimerTask.execute(execution: 5, timeout: 2) do
20
+ acknowledge_messages
60
21
  end
61
-
62
- completed_retries.each { |r| scheduled_retries.delete r }
63
22
  end
64
23
 
65
- # Public: Purge the RabbitMQ queue.
24
+ # Internal: Called on actor termination. Used to stop consumption off the
25
+ # queue and end the ticker.
66
26
  #
67
27
  # Returns nil.
68
- def purge
69
- bunny_queue.purge
28
+ def cleanup
29
+ @ticker.kill if @ticker
30
+ stop_consumer if @consumer
31
+ @channel.close if @channel && channel.open?
70
32
 
71
33
  nil
72
34
  end
73
35
 
74
36
  private
75
37
 
76
- # Internal: Returns the Bunny::Queue in use.
77
- attr_reader :bunny_queue
78
-
79
- # Internal: Returns the Bunny::Channel in use.
80
- attr_reader :channel
81
-
82
- # Internal: Returns the Bunny::Exchange in use.
83
- attr_reader :exchange
38
+ # Internal: Returns the ExceptionHandler class.
39
+ attr_reader :exception_handler_class
84
40
 
85
41
  # Internal: Returns the listener object.
86
42
  attr_reader :listener
@@ -88,6 +44,20 @@ module Proletariat
88
44
  # Internal: Returns the queue_config in use.
89
45
  attr_reader :queue_config
90
46
 
47
+ # Internal: Acknowledge processed messages.
48
+ #
49
+ # Returns nil.
50
+ def acknowledge_messages
51
+ ready_acknowledgers.each do |acknowledger|
52
+ acknowledger.acknowledge_on_channel channel
53
+ acknowledgers.delete acknowledger
54
+ end
55
+
56
+ nil
57
+ end
58
+
59
+ # Internal: Returns array of Acknowledgers which haven't acknowledged their
60
+ # messages.
91
61
  def acknowledgers
92
62
  @acknowledgers ||= []
93
63
  end
@@ -104,11 +74,31 @@ module Proletariat
104
74
  nil
105
75
  end
106
76
 
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? }
77
+ # Internal: Returns the Bunny::Queue in use.
78
+ def bunny_queue
79
+ @bunny_queue ||= channel.queue(queue_config.queue_name,
80
+ durable: !Proletariat.test_mode?,
81
+ auto_delete: Proletariat.test_mode?)
82
+ end
83
+
84
+ # Internal: Returns the Bunny::Channel in use.
85
+ def channel
86
+ @channel ||= Proletariat.connection.create_channel.tap do |channel|
87
+ channel.prefetch Proletariat.worker_threads + 1
88
+ end
89
+ end
90
+
91
+ def exception_handler
92
+ @exception_handler ||= exception_handler_class.spawn!(
93
+ name: "#{queue_config.worker_name}_exception_handler",
94
+ supervise: true, args: [queue_config.queue_name]
95
+ )
96
+ end
97
+
98
+ # Internal: Returns the Bunny::Exchange in use.
99
+ def exchange
100
+ @exchange ||= channel.topic(Proletariat.exchange_name,
101
+ durable: !Proletariat.test_mode?)
112
102
  end
113
103
 
114
104
  # Internal: Forwards all message bodies to listener#post. Auto-acks
@@ -117,12 +107,12 @@ module Proletariat
117
107
  # Returns nil.
118
108
  def handle_message(info, properties, body)
119
109
  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)
110
+ message = Message.new(info.routing_key, body, properties.headers)
111
+ ivar = listener.ask(message)
112
+ acknowledgers << Acknowledger.new(ivar, info.delivery_tag, message,
113
+ exception_handler)
124
114
  else
125
- channel.acknowledge info.delivery_tag
115
+ channel.ack info.delivery_tag
126
116
  end
127
117
 
128
118
  nil
@@ -146,18 +136,16 @@ module Proletariat
146
136
  end
147
137
  end
148
138
 
149
- def scheduled_retries
150
- @scheduled_retries ||= []
151
- end
152
-
153
139
  # Internal: Starts a consumer on the queue. The consumer forwards all
154
140
  # message bodies to listener#post. Auto-acks messages not meant
155
141
  # for this subscriber's workers.
156
142
  #
157
143
  # Returns nil.
158
144
  def start_consumer
159
- @consumer = bunny_queue.subscribe ack: true do |info, properties, body|
160
- handle_message info, properties, body
145
+ @consumer = bunny_queue.subscribe manual_ack: true do |info, props, body|
146
+ acknowledge_messages
147
+
148
+ handle_message info, props, body
161
149
 
162
150
  nil
163
151
  end
@@ -172,7 +160,6 @@ module Proletariat
172
160
  def stop_consumer
173
161
  @consumer.cancel if @consumer
174
162
  wait_for_acknowledgers if acknowledgers.any?
175
- scheduled_retries.each { |r| r.expedite }
176
163
 
177
164
  nil
178
165
  end
@@ -194,34 +181,36 @@ module Proletariat
194
181
  # Internal: Used to watch the state of dispatched Work and send ack/nack
195
182
  # to a RabbitMQ channel.
196
183
  class Acknowledger
184
+ include Concerns::Logging
185
+
197
186
  # Public: Maximum time in seconds to wait synchronously for an
198
187
  # acknowledgement.
199
188
  MAX_BLOCK_TIME = 5
200
189
 
201
190
  # Public: Creates a new Acknowledger instance.
202
191
  #
203
- # future - A future-like object holding the Worker response.
192
+ # ivar - A ivar-like object holding the Worker response.
204
193
  # 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
194
+ # message - The original message; for exception handling.
195
+ # exception_handler - A reference to an ExceptionHandler.
196
+ def initialize(ivar, delivery_tag, message, exception_handler)
197
+ @ivar = ivar
209
198
  @delivery_tag = delivery_tag
210
- @properties = properties
211
- @scheduled_retries = scheduled_retries
199
+ @message = message
200
+ @exception_handler = exception_handler
212
201
  end
213
202
 
214
- # Public: Retrieves the value from the future and sends the relevant
203
+ # Public: Retrieves the value from the ivar and sends the relevant
215
204
  # acknowledgement on a given channel. Logs a warning if the
216
- # future value is unexpected.
205
+ # ivar value is unexpected.
217
206
  #
218
207
  # channel - The Bunny::Channel to receive the acknowledgement.
219
208
  #
220
209
  # Returns nil.
221
210
  def acknowledge_on_channel(channel)
222
- if future.fulfilled?
211
+ if ivar.fulfilled?
223
212
  acknowledge_success(channel)
224
- elsif future.rejected?
213
+ elsif ivar.rejected?
225
214
  acknowledge_error(channel)
226
215
  end
227
216
 
@@ -234,17 +223,17 @@ module Proletariat
234
223
  #
235
224
  # Returns nil.
236
225
  def block_until_acknowledged(channel)
237
- future.value(MAX_BLOCK_TIME)
226
+ ivar.wait(MAX_BLOCK_TIME)
238
227
  acknowledge_on_channel(channel)
239
228
 
240
229
  nil
241
230
  end
242
231
 
243
- # Public: Gets the readiness of the future for acknowledgement use.
232
+ # Public: Gets the readiness of the ivar for acknowledgement use.
244
233
  #
245
- # Returns true if future is fulfilled or rejected.
234
+ # Returns true if ivar is fulfilled or rejected.
246
235
  def ready_to_acknowledge?
247
- future.state != :pending
236
+ ivar.completed?
248
237
  end
249
238
 
250
239
  private
@@ -256,8 +245,8 @@ module Proletariat
256
245
  #
257
246
  # Returns nil.
258
247
  def acknowledge_success(channel)
259
- case future.value
260
- when :ok then channel.acknowledge delivery_tag
248
+ case ivar.value
249
+ when :ok then channel.ack delivery_tag
261
250
  when :drop then channel.reject delivery_tag, false
262
251
  when :requeue then channel.reject delivery_tag, true
263
252
  else
@@ -275,10 +264,10 @@ module Proletariat
275
264
  #
276
265
  # Returns nil.
277
266
  def acknowledge_error(channel)
278
- Proletariat.logger.error future.reason
267
+ Proletariat.logger.error ivar.reason
279
268
 
280
- scheduled_retries << Retry.new(properties)
281
- channel.acknowledge delivery_tag
269
+ exception_handler << message
270
+ channel.ack delivery_tag
282
271
 
283
272
  nil
284
273
  end
@@ -286,89 +275,14 @@ module Proletariat
286
275
  # Internal: Returns the RabbitMQ delivery tag.
287
276
  attr_reader :delivery_tag
288
277
 
289
- # Internal: Returns the future-like object holding the Worker response.
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
278
+ # Internal: Returns the ExceptionHandler reference.
279
+ attr_reader :exception_handler
306
280
 
307
- properties[:headers]['failures'] = failures
308
- properties[:headers]['worker'] = properties[:worker]
281
+ # Internal: Returns the ivar-like object holding the Worker response.
282
+ attr_reader :ivar
309
283
 
310
- @scheduled_task = Concurrent::ScheduledTask.new(retry_delay) do
311
- requeue_message
312
- end
313
-
314
- @scheduled_task.execute
315
- end
316
-
317
- # Public: Attempt to requeue the message immediately if pending or
318
- # wait for natural completion.
319
- #
320
- # Returns nil.
321
- def expedite
322
- if scheduled_task.cancel
323
- requeue_message
324
- else
325
- scheduled_task.value
326
- end
327
-
328
- nil
329
- end
330
-
331
- # Public: Tests whether the message has been requeued.
332
- #
333
- # Returns a Boolean.
334
- def requeued?
335
- scheduled_task.fulfilled?
336
- end
337
-
338
- private
339
-
340
- # Internal: Returns the original message properties.
341
- attr_reader :properties
342
-
343
- # Internal: Returns the ScheduledTask which will requeue the message.
344
- attr_reader :scheduled_task
345
-
346
- # Internal: Fetches the current number of message failures from the
347
- # headers. Defaults to 1.
348
- #
349
- # Returns a Fixnum.
350
- def failures
351
- @failures ||= (properties[:headers]['failures'] || 0) + 1
352
- end
353
-
354
- # Internal: Performs the actual message requeue.
355
- #
356
- # Returns nil.
357
- def requeue_message
358
- Proletariat.publish(properties[:key], properties[:message],
359
- properties[:headers])
360
-
361
- nil
362
- end
363
-
364
- # Internal: Calculates an exponential retry delay based on the previous
365
- # number of failures. Capped with configuration setting.
366
- #
367
- # Returns the delay in seconds as a Fixnum.
368
- def retry_delay
369
- [2**failures, Proletariat.max_retry_delay].min
370
- end
371
- end
284
+ # Internal: Returns the original message.
285
+ attr_reader :message
372
286
  end
373
287
  end
374
288
  end