proletariat 0.0.6 → 0.1.0

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