proletariat 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,19 +8,17 @@ module Proletariat
8
8
 
9
9
  # Public: Creates a new Subscriber instance.
10
10
  #
11
- # connection - An open Bunny::Session object.
12
- # exchange_name - A String of the RabbitMQ topic exchange.
13
- # queue_config - A QueueConfig value object.
14
- def initialize(connection, listener, queue_config)
15
- @connection = connection
11
+ # listener - Object to delegate new messages to.
12
+ # queue_config - A QueueConfig value object.
13
+ def initialize(listener, queue_config)
16
14
  @listener = listener
17
15
  @queue_config = queue_config
18
16
 
19
- @channel = @connection.create_channel
17
+ @channel = Proletariat.connection.create_channel
20
18
 
21
- @channel.prefetch queue_config.prefetch
19
+ @channel.prefetch Proletariat.worker_threads
22
20
 
23
- @exchange = @channel.topic queue_config.exchange_name, durable: true
21
+ @exchange = @channel.topic Proletariat.exchange_name, durable: true
24
22
  @bunny_queue = @channel.queue queue_config.queue_name,
25
23
  durable: true,
26
24
  auto_delete: queue_config.auto_delete
@@ -19,7 +19,6 @@ module Proletariat
19
19
  # block - The block of code within which the expectations should
20
20
  # be satisfied.
21
21
  def initialize(expectations, &block)
22
- @connection = Proletariat.runner.connection
23
22
  @counters = []
24
23
  @subscribers = []
25
24
 
@@ -27,7 +26,7 @@ module Proletariat
27
26
  queue_config = generate_queue_config_for_topic(expectation.topics)
28
27
  counter = MessageCounter.new(expectation.quantity)
29
28
  counters << counter
30
- subscribers << Subscriber.new(connection, counter, queue_config)
29
+ subscribers << Subscriber.new(counter, queue_config)
31
30
  end
32
31
 
33
32
  @block = block
@@ -40,7 +39,7 @@ module Proletariat
40
39
  def guarantee
41
40
  run_subscribers
42
41
 
43
- block.call
42
+ block.call if block
44
43
 
45
44
  timer = 0.0
46
45
 
@@ -61,9 +60,6 @@ module Proletariat
61
60
  # satisfied.
62
61
  attr_reader :block
63
62
 
64
- # Internal: Returns an open Bunny::Session object.
65
- attr_reader :connection
66
-
67
63
  # Internal: Returns an array of MessageCounter instances.
68
64
  attr_reader :counters
69
65
 
@@ -71,7 +67,7 @@ module Proletariat
71
67
  attr_reader :subscribers
72
68
 
73
69
  def generate_queue_config_for_topic(topics)
74
- QueueConfig.new('', Proletariat.runner.exchange_name, topics, 1, true)
70
+ QueueConfig.new('', topics, true)
75
71
  end
76
72
 
77
73
  # Internal: Checks each counter to ensure expected messages have arrived.
@@ -107,8 +103,8 @@ module Proletariat
107
103
  # Public: Creates a new MessageCounter instance.
108
104
  #
109
105
  # expected - The number of messages expected.
110
- def initialize(expected)
111
- @count = 0
106
+ def initialize(expected, count = 0)
107
+ @count = count
112
108
  @expected = expected
113
109
  end
114
110
 
@@ -4,7 +4,7 @@ require 'proletariat/testing/fixnum_extension'
4
4
 
5
5
  module Proletariat
6
6
  # Public: Mixin to aid solve test synchronization issues while still running
7
- # Proletariat the same way you would in production,
7
+ # Proletariat the same way you would in production.
8
8
  module Testing
9
9
  # Public: Builds an Expectation instance which listens for a single message
10
10
  # on any topic.
@@ -0,0 +1,37 @@
1
+ module Proletariat
2
+ # Internal: Helper utility to parse and constantize strings into arrays of
3
+ # Worker classes.
4
+ module WorkerDescriptionParser
5
+ # Public: Parse given string into array of Worker classes.
6
+ #
7
+ # description - String to be parsed. Should contain comma-separated class
8
+ # names.
9
+ #
10
+ # Examples
11
+ #
12
+ # WorkerDescriptionParser.parse('FirstWorker,SecondWorker')
13
+ # # => [FirstWorker, SecondWorker]
14
+ #
15
+ # Returns an Array of Worker classes.
16
+ def self.parse(description)
17
+ description.split(',').map(&:strip).map do |string|
18
+ constantize string
19
+ end.compact
20
+ end
21
+
22
+ private
23
+
24
+ # Intenal: Performs constantizing of worker names into Classes.
25
+ #
26
+ # name - The name to be constantized.
27
+ #
28
+ # Returns the Worker class if valid.
29
+ # Returns the nil if Worker class cannot be found.
30
+ def self.constantize(name)
31
+ name.split('::').reduce(Object) { |a, e| a.const_get(e) }
32
+ rescue NameError => e
33
+ Proletariat.logger.warn "Missing worker class: #{e.name}"
34
+ nil
35
+ end
36
+ end
37
+ end
@@ -1,4 +1,4 @@
1
1
  # Public: Adds a constant for the current version number.
2
2
  module Proletariat
3
- VERSION = '0.0.2'
3
+ VERSION = '0.0.3'
4
4
  end
@@ -1,48 +1,39 @@
1
+ require 'proletariat/concerns/logging'
2
+
1
3
  module Proletariat
2
- # Public: Handles messages from a RabbitMQ queue. Subclasses should
4
+ # Public: Handles messages for Background processing. Subclasses should
3
5
  # overwrite the #work method.
4
- class Worker < Concurrent::Actor
6
+ class Worker
5
7
  include Concerns::Logging
6
8
 
7
- # Internal: Called by the Concurrent framework to handle new mailbox
8
- # messages. Overridden in this subclass to call the #work method
9
- # with the given message.
10
- #
11
- # message - The incoming message.
9
+ # Public: Logs the 'online' status of the worker.
12
10
  #
13
11
  # Returns nil.
14
- def act(message)
15
- work message
12
+ def started
13
+ log_info 'Now online'
14
+
15
+ nil
16
16
  end
17
17
 
18
- # Internal: Called by the Concurrent framework on actor start. Overridden
19
- # in this subclass to log the status of the worker.
18
+ # Public: Logs the 'offline' status of the worker.
20
19
  #
21
20
  # Returns nil.
22
- def on_run
23
- super
24
-
25
- log_info 'Now online'
21
+ def stopped
22
+ log_info 'Now offline'
26
23
 
27
24
  nil
28
25
  end
29
26
 
30
- # Internal: Called by the Concurrent framework on actor start. Overridden
31
- # in this subclass to log the status of the worker.
27
+ # Public: Logs the 'shutting down' status of the worker.
32
28
  #
33
29
  # Returns nil.
34
- def on_stop
30
+ def stopping
35
31
  log_info 'Attempting graceful shutdown.'
36
- wait_for_work_queue unless queue.empty?
37
-
38
- super
39
-
40
- log_info 'Now offline'
41
32
 
42
33
  nil
43
34
  end
44
35
 
45
- # Public: Handles RabbitMQ messages.
36
+ # Public: Handles an incoming message to perform background work.
46
37
  #
47
38
  # message - The incoming message.
48
39
  #
@@ -51,8 +42,6 @@ module Proletariat
51
42
  fail NotImplementedError
52
43
  end
53
44
 
54
- protected
55
-
56
45
  # Public: Helper method to ease accessing the logger from within #work.
57
46
  # Sends #info to logger if message provided.
58
47
  #
@@ -90,19 +79,6 @@ module Proletariat
90
79
  nil
91
80
  end
92
81
 
93
- private
94
-
95
- # Internal: Blocks until each message has been handled by #work.
96
- #
97
- # Returns nil.
98
- def wait_for_work_queue
99
- log_info 'Waiting for work queue to drain.'
100
-
101
- work(*queue.pop.message) until queue.empty?
102
-
103
- nil
104
- end
105
-
106
82
  # Internal: Class methods on Worker to provide configuration DSL.
107
83
  module ConfigurationMethods
108
84
  # Public: A configuration method for adding a routing key to be used when
@@ -111,8 +87,8 @@ module Proletariat
111
87
  # routing_key - A routing key for queue-binding as a String.
112
88
  #
113
89
  # Returns nil.
114
- def listen_on(routing_key)
115
- routing_keys << routing_key
90
+ def listen_on(*new_routing_keys)
91
+ routing_keys.concat new_routing_keys
116
92
 
117
93
  nil
118
94
  end
data/lib/proletariat.rb CHANGED
@@ -5,8 +5,12 @@ require 'bunny'
5
5
  require 'logger'
6
6
  require 'forwardable'
7
7
 
8
- require 'proletariat/concerns/logging'
8
+ require 'proletariat/concurrency/actor'
9
+ require 'proletariat/concurrency/supervisor'
9
10
 
11
+ require 'proletariat/util/worker_description_parser'
12
+
13
+ require 'proletariat/configuration'
10
14
  require 'proletariat/manager'
11
15
  require 'proletariat/publisher'
12
16
  require 'proletariat/queue_config'
@@ -15,73 +19,42 @@ require 'proletariat/subscriber'
15
19
  require 'proletariat/worker'
16
20
 
17
21
  # Public: Creates the Proletariat namespace and holds a process-wide Runner
18
- # instance as well as a logger.
22
+ # instance as well as access to the configuration attributes.
19
23
  module Proletariat
20
- # Public: The default name used for the RabbitMQ topic exchange.
21
- DEFAULT_EXCHANGE_NAME = 'proletariat'
22
-
23
24
  class << self
24
25
  extend Forwardable
25
26
 
26
27
  # Public: Delegate lifecycle calls to the process-wide Runner.
27
28
  def_delegators :runner, :run, :run!, :stop, :running?, :publish, :purge
28
29
 
29
- # Public: Allows the setting of an alternate logger.
30
+ # Public: Allows configuration of Proletariat via given block.
30
31
  #
31
- # logger - An object which fulfills the role of a Logger.
32
- attr_writer :logger
33
-
34
- # Public: Sets the process-wide Runner to an instance initialized with a
35
- # given hash of options.
32
+ # block - Block containing configuration calls.
36
33
  #
37
- # options - A Hash of options (default: {}):
38
- # :connection - An open RabbitMQ::Session object.
39
- # :exchange_name - The RabbitMQ topic exchange name as a
40
- # String.
41
- # :logger - An object which fulfills the role of a
42
- # Logger.
43
- # :publisher_threads - The size of the publisher thread pool.
44
- # :supervisor - A Supervisor instance.
45
- # :worker_classes - An Array of Worker subclasses.
46
- # :worker_threads - The size of the worker thread pool.
47
- def configure(options = {})
48
- self.logger = options.fetch(:logger, default_logger)
34
+ # Returns nil.
35
+ def configure(&block)
36
+ config.configure_with_block(&block)
49
37
 
50
- @runner = Runner.new(defaults.merge(options))
51
- end
52
-
53
- # Internal: The logger used if no other is specified via .configure.
54
- #
55
- # Returns a Logger which logs to STDOUT.
56
- def default_logger
57
- Logger.new(STDOUT)
38
+ nil
58
39
  end
59
40
 
60
- # Internal: Default process-wide Runner options.
61
- #
62
- # Returns a Hash of options.
63
- def defaults
64
- {
65
- worker_classes: workers_from_env || []
66
- }
41
+ def runner
42
+ @runner ||= Runner.new
67
43
  end
68
44
 
69
- def logger
70
- @logger ||= default_logger
45
+ def method_missing(method_sym, *arguments, &block)
46
+ if config.respond_to? method_sym
47
+ config.send method_sym
48
+ else
49
+ super
50
+ end
71
51
  end
72
52
 
73
- def runner
74
- @runner ||= Runner.new(defaults)
75
- end
53
+ private
76
54
 
77
- def workers_from_env
78
- if ENV['WORKERS']
79
- ENV['WORKERS'].split(',').map(&:strip).map do |string|
80
- string
81
- .split('::')
82
- .reduce(Object) { |a, e| a.const_get(e) }
83
- end
84
- end
55
+ # Internal: Global configuration object.
56
+ def config
57
+ @config ||= Configuration.new
85
58
  end
86
59
  end
87
60
  end
@@ -0,0 +1,85 @@
1
+ require 'proletariat'
2
+
3
+ FirstWorker = Class.new
4
+ SecondWorker = Class.new
5
+
6
+ module Proletariat
7
+ describe Configuration do
8
+ describe '#configure_with_block' do
9
+ it 'should allow configuration via `config.[attribute]=`' do
10
+ configuration = Configuration.new
11
+ expect(configuration).to receive(:connection=).with('new connection')
12
+ configuration.configure_with_block do
13
+ config.connection = 'new connection'
14
+ end
15
+ end
16
+ end
17
+
18
+ describe '#connection' do
19
+ let(:new_session) { double.as_null_object }
20
+
21
+ before do
22
+ stub_const 'Bunny', double(new: new_session)
23
+ end
24
+
25
+ it 'should default to a creating a new bunny session' do
26
+ expect(Bunny).to receive(:new)
27
+ Configuration.new.connection
28
+ end
29
+
30
+ it 'should open any new bunny sessions' do
31
+ expect(new_session).to receive(:start)
32
+ Configuration.new.connection
33
+ end
34
+ end
35
+
36
+ describe '#exchange_name' do
37
+ it 'should default to proletariat' do
38
+ expect(Configuration.new.exchange_name).to eq 'proletariat'
39
+ end
40
+ end
41
+
42
+ describe '#logger' do
43
+ it 'should default to STDOUT' do
44
+ expect(Logger).to receive(:new).with(STDOUT)
45
+ Configuration.new.logger
46
+ end
47
+ end
48
+
49
+ describe '#publisher_threads' do
50
+ it 'should default to 2' do
51
+ expect(Configuration.new.publisher_threads).to eq 2
52
+ end
53
+ end
54
+
55
+ describe '#worker_classes' do
56
+ context 'WORKERS env variable is set' do
57
+ before do
58
+ ENV['WORKERS'] = 'FirstWorker,SecondWorker'
59
+ end
60
+
61
+ after do
62
+ ENV['WORKERS'] = nil
63
+ end
64
+
65
+ it 'should default to workers in env variable' do
66
+ expect(Configuration.new.worker_classes).to \
67
+ eq [FirstWorker, SecondWorker]
68
+ end
69
+ end
70
+
71
+ context 'WORKERS env variable is not set' do
72
+ it 'should default to an empty array' do
73
+ expect(Configuration.new.worker_classes).to \
74
+ eq []
75
+ end
76
+ end
77
+ end
78
+
79
+ describe '#worker_threads' do
80
+ it 'should default to 3' do
81
+ expect(Configuration.new.worker_threads).to eq 3
82
+ end
83
+ end
84
+ end
85
+ end
@@ -40,10 +40,11 @@ end
40
40
 
41
41
  describe Proletariat do
42
42
  it 'should roughly work' do
43
- Proletariat.configure(
44
- logger: Logger.new('/dev/null'),
45
- worker_classes: [PingWorker, PongWorker]
46
- )
43
+ Proletariat.configure do
44
+ config.logger = Logger.new('/dev/null')
45
+ config.worker_classes = [PingWorker, PongWorker]
46
+ end
47
+
47
48
  Proletariat.run!
48
49
  sleep 2
49
50
  Proletariat.publish 'ping', ''
@@ -0,0 +1,36 @@
1
+ require 'proletariat/publisher'
2
+
3
+ module Proletariat
4
+ describe Publisher do
5
+ let(:connection) { double.as_null_object }
6
+ let(:exchange_name) { 'great-exchange' }
7
+ let(:logger) { double }
8
+
9
+ before do
10
+ allow(Proletariat).to receive(:connection).and_return(connection)
11
+ allow(Proletariat).to receive(:exchange_name).and_return(exchange_name)
12
+ allow(Proletariat).to receive(:logger).and_return(logger)
13
+ end
14
+
15
+ describe '#started' do
16
+ it 'should log status' do
17
+ expect(logger).to receive(:info).with /online/
18
+ Publisher.new.started
19
+ end
20
+ end
21
+
22
+ describe '#stopped' do
23
+ it 'should log status' do
24
+ expect(logger).to receive(:info).with /offline/
25
+ Publisher.new.stopped
26
+ end
27
+ end
28
+
29
+ describe '#stopping' do
30
+ it 'should log status' do
31
+ expect(logger).to receive(:info).with /graceful shutdown/
32
+ Publisher.new.stopping
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ require 'proletariat/queue_config'
2
+
3
+ module Proletariat
4
+ describe QueueConfig do
5
+ describe '#queue_name' do
6
+ it 'should return an underscored version of the worker name' do
7
+ queue_config = QueueConfig.new('ExampleWorker', ['lolcats'], false)
8
+ expect(queue_config.queue_name).to eq 'example_worker'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,55 @@
1
+ require 'proletariat/testing/expectation_guarantor'
2
+
3
+ module Proletariat
4
+ module Testing
5
+ describe ExpectationGuarantor::MessageCounter do
6
+ describe '#expected_messages_received?' do
7
+ context 'count is equal or greater than expected' do
8
+ it 'should return true' do
9
+ counter = ExpectationGuarantor::MessageCounter.new(1, 3)
10
+ expect(counter.expected_messages_received?).to be_truthy
11
+ end
12
+ end
13
+
14
+ context 'count is less than expected' do
15
+ it 'should return false' do
16
+ counter = ExpectationGuarantor::MessageCounter.new(5, 3)
17
+ expect(counter.expected_messages_received?).to be_falsey
18
+ end
19
+ end
20
+ end
21
+
22
+ describe '#post?' do
23
+ class FakeBlockTaker
24
+ attr_reader :block
25
+
26
+ def initialize(&block)
27
+ @block = block
28
+ end
29
+ end
30
+
31
+ before do
32
+ stub_const 'Concurrent::Future', FakeBlockTaker
33
+ end
34
+
35
+ it 'should increment the count' do
36
+ counter = ExpectationGuarantor::MessageCounter.new(1)
37
+ counter.post?('message')
38
+ expect(counter.expected_messages_received?).to be_truthy
39
+ end
40
+
41
+ it 'should return a future containing :ok' do
42
+ counter = ExpectationGuarantor::MessageCounter.new(1)
43
+ expect(Concurrent::Future).to receive(:new)
44
+ counter.post?('message')
45
+ end
46
+
47
+ it 'should ensure the returned future contains :ok' do
48
+ counter = ExpectationGuarantor::MessageCounter.new(1)
49
+ future = counter.post?('message')
50
+ expect(future.block.call).to eq :ok
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ require 'proletariat/testing/expectation'
2
+
3
+ module Proletariat
4
+ module Testing
5
+ describe Expectation do
6
+ describe '#on_topic' do
7
+ it 'should return a new expectation with given topic' do
8
+ expectation = Expectation.new([], 2)
9
+ expect(expectation.on_topic('lolcats', 'dogs').topics).to \
10
+ eq %w(lolcats dogs)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ require 'proletariat/util/worker_description_parser'
2
+
3
+ FakeWorker = Class.new
4
+ AnotherFakeWorker = Class.new
5
+
6
+ module Proletariat
7
+ describe WorkerDescriptionParser do
8
+ describe '.parse' do
9
+ let(:logger) { double.as_null_object }
10
+
11
+ before do
12
+ stub_const 'Proletariat', double(logger: logger)
13
+ end
14
+
15
+ context 'worker classes exist' do
16
+ it 'should return the worker classes' do
17
+ parsed = WorkerDescriptionParser.parse 'FakeWorker,AnotherFakeWorker'
18
+ expect(parsed).to eq [FakeWorker, AnotherFakeWorker]
19
+ end
20
+ end
21
+
22
+ context 'worker classes do not exist' do
23
+ it 'should return an empty array' do
24
+ parsed = WorkerDescriptionParser.parse('NonexistantWorker')
25
+ expect(parsed).to eq []
26
+ end
27
+
28
+ it 'should log a warning for the missing class' do
29
+ expect(logger).to receive(:warn).with(/Missing worker class/)
30
+ WorkerDescriptionParser.parse('NonexistantWorker')
31
+ end
32
+ end
33
+
34
+ context 'some worker classes exist' do
35
+ it 'should return only the existing classes' do
36
+ parsed = WorkerDescriptionParser.parse 'FakeWorker,NonexistantWorker'
37
+ expect(parsed).to eq [FakeWorker]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end