proletariat 0.0.2 → 0.0.3

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.
@@ -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