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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 49905f9a8ecb2a3b12f565f7b6c7e07c7cac6e39
4
+ data.tar.gz: 76771f1121f542f06a8ee2d9f6f7ffcf07982404
5
+ SHA512:
6
+ metadata.gz: 5a1e573f1fd1028068f5f91dcd5625d4ddf90052d9711b8c8a85d810db0ea4b0440cb51384040c865eea540cbbf48368dc7b2dacdc770e85fa0430b5f63f887d
7
+ data.tar.gz: 193ecfabdc6bb3c0d25bda8a8e57a66a77aa6ab830a552f395acc626a88803ef556019556a950201a302089a830d8328fa42690943dca4a9376a345c98200217
data/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ Documentation:
2
+ Exclude:
3
+ - spec/*
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.0.3
2
+
3
+ Features:
4
+
5
+ - Overhauled configuration (breaking change)
6
+ - Queues now purged before first cucumber scenario
7
+
8
+ Code gardening:
9
+
10
+ - Decoupling logic from concurrency for better testability
11
+ - Added tests
12
+
1
13
  ## 0.0.2
2
14
 
3
15
  Features:
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Lightweight background processing in Ruby powered by RabbitMQ and the excellent concurrent-ruby gem.
4
4
 
5
- [![Code Climate](https://codeclimate.com/repos/52d75c166956805abe000226/badges/bf98213c1d629072560c/gpa.png)](https://codeclimate.com/repos/52d75c166956805abe000226/feed)
5
+ [![Code Climate](https://codeclimate.com/github/SebastianEdwards/proletariat.png)](https://codeclimate.com/github/SebastianEdwards/proletariat)
6
6
 
7
7
  ### Warning!
8
8
 
@@ -100,6 +100,5 @@ Use the provided helpers in your step definitions to synchronize your test suite
100
100
  I wanted a library which shared one RabbitMQ connection across all of the workers on a given process. Many hosted RabbitMQ platforms tightly limit the max number of connections.
101
101
 
102
102
  ## TODO
103
- - Improve test suite :(
104
103
  - Add command line interface
105
104
  - Abstract retry strategies
@@ -0,0 +1,71 @@
1
+ module Proletariat
2
+ # Public: Interface abstraction for Concurrent::Actor. Creates a delegate
3
+ # instance from given class and arguments and delegates events to it.
4
+ class Actor < Concurrent::Actor
5
+ # Public: Creates a new Actor instance.
6
+ #
7
+ # delegate_class - The class to instantiate as a delegate.
8
+ # *arguments - The arguments to pass to delegate_class.new
9
+ def initialize(delegate_class, *arguments)
10
+ @delegate = delegate_class.new(*arguments)
11
+ end
12
+
13
+ # Internal: Called by the Concurrent framework to handle new mailbox
14
+ # messages. Overridden in this subclass to call the #work method
15
+ # with the given arguments on the delegate.
16
+ #
17
+ # *arguments - The arguments to pass to delegate#work
18
+ #
19
+ # Returns nil.
20
+ def act(*arguments)
21
+ delegate.work(*arguments)
22
+ end
23
+
24
+ # Internal: Called by the Concurrent framework on actor start. Overridden
25
+ # in this subclass to call the #starting and #started methods on
26
+ # the delegate.
27
+ #
28
+ # Returns nil.
29
+ def on_run
30
+ delegate.starting if delegate.respond_to?(:starting)
31
+
32
+ super
33
+
34
+ delegate.started if delegate.respond_to?(:started)
35
+
36
+ nil
37
+ end
38
+
39
+ # Internal: Called by the Concurrent framework on actor start. Overridden
40
+ # in this subclass to call the #stopping and #stopped methods on
41
+ # the delegate. Ensures queue is drained before calling #stopped.
42
+ #
43
+ # Returns nil.
44
+ def on_stop
45
+ delegate.stopping if delegate.respond_to?(:stopping)
46
+
47
+ wait_for_queue_to_drain unless queue.empty?
48
+
49
+ super
50
+
51
+ delegate.stopped if delegate.respond_to?(:stopped)
52
+
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ # Internal: Returns the delegate instance.
59
+ attr_reader :delegate
60
+
61
+ # Internal: Blocks until each queued message has been handled by the
62
+ # delegate #work method.
63
+ #
64
+ # Returns nil.
65
+ def wait_for_queue_to_drain
66
+ delegate.work(*queue.pop.message) until queue.empty?
67
+
68
+ nil
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ module Proletariat
2
+ class Supervisor
3
+ extend Forwardable
4
+
5
+ def_delegators :true_supervisor, :run, :run!, :stop, :running?, :add_worker
6
+
7
+ def [](mailbox_name)
8
+ mailboxes[mailbox_name]
9
+ end
10
+
11
+ def add_supervisor(supervisor, opts = {})
12
+ true_supervisor.add_worker supervisor, opts.merge(type: :supervisor)
13
+ end
14
+
15
+ def supervise_pool(mailbox_name, threads, actor_class, *arguments)
16
+ mailboxes[mailbox_name], workers = Actor.pool(threads, actor_class,
17
+ *arguments)
18
+ true_supervisor.add_workers workers
19
+ end
20
+
21
+ private
22
+
23
+ def mailboxes
24
+ @mailboxes ||= {}
25
+ end
26
+
27
+ def true_supervisor
28
+ @true_supervisor ||= Concurrent::Supervisor.new
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,131 @@
1
+ module Proletariat
2
+ # Public: Global configuration object. Provides sensible defaults.
3
+ class Configuration
4
+ # Public: The default name used for the RabbitMQ topic exchange.
5
+ DEFAULT_EXCHANGE_NAME = 'proletariat'
6
+
7
+ # Public: The default number of threads to use for publishers.
8
+ DEFAULT_PUBLISHER_THREADS = 2
9
+
10
+ # Public: The default number of threads to use for each worker class.
11
+ DEFAULT_WORKER_THREADS = 3
12
+
13
+ # Internal: Sets the RabbitMQ connection.
14
+ attr_writer :connection
15
+
16
+ # Internal: Sets the RabbitMQ topic exchange name.
17
+ attr_writer :exchange_name
18
+
19
+ # Internal: Sets the logger.
20
+ attr_writer :logger
21
+
22
+ # Internal: Sets the number of threads to use for publishers.
23
+ attr_writer :publisher_threads
24
+
25
+ # Internal: Sets the Array of Worker classes to use for background
26
+ # processing.
27
+ attr_writer :worker_classes
28
+
29
+ # Internal: Sets the number of threads to use for each Worker class.
30
+ attr_writer :worker_threads
31
+
32
+ # Public: Allows setting of the config attributes via a block.
33
+ #
34
+ # block - Block which modifies attributes by accessing them via #config.
35
+ #
36
+ # Returns nil.
37
+ def configure_with_block(&block)
38
+ ConfigurationDSL.new(self, &block).set_config
39
+
40
+ nil
41
+ end
42
+
43
+ # Public: Returns the set connection or defaults to a new, open
44
+ # Bunny::Session
45
+ #
46
+ # Returns a Bunny::Session.
47
+ def connection
48
+ @connection ||= begin
49
+ new_connection = Bunny.new
50
+ new_connection.start
51
+
52
+ new_connection
53
+ end
54
+ end
55
+
56
+ # Public: Returns the set name of the exchange or a default.
57
+ #
58
+ # Returns a String.
59
+ def exchange_name
60
+ @exchange_name ||= DEFAULT_EXCHANGE_NAME
61
+ end
62
+
63
+ # Public: Returns the set logger or a default standard output logger.
64
+ #
65
+ # Returns a logger.
66
+ def logger
67
+ @logger ||= Logger.new(STDOUT)
68
+ end
69
+
70
+ # Public: Returns the set number of publisher threads or a default.
71
+ #
72
+ # Returns a Fixnum.
73
+ def publisher_threads
74
+ @publisher_threads ||= DEFAULT_PUBLISHER_THREADS
75
+ end
76
+
77
+ # Public: Returns the set worker classes or a default pulled from the
78
+ # WORKERS env variable.
79
+ #
80
+ # Returns an array of Worker classes.
81
+ def worker_classes
82
+ @worker_classes ||= begin
83
+ if ENV['WORKERS']
84
+ WorkerDescriptionParser.parse ENV['WORKERS']
85
+ else
86
+ []
87
+ end
88
+ end
89
+ end
90
+
91
+ # Public: Returns the set number of worker threads or a default.
92
+ #
93
+ # Returns a Fixnum.
94
+ def worker_threads
95
+ @worker_threads ||= DEFAULT_WORKER_THREADS
96
+ end
97
+
98
+ private
99
+
100
+ # Internal: Handles running a configuration block in a context to allow
101
+ # access to the configuration object via a call to #config.
102
+ class ConfigurationDSL
103
+ # Public: Creates a new ConfigurationDSL instance.
104
+ #
105
+ # configuration - The Configuration instance you intend to update.
106
+ # block - The block containing the config settings.
107
+ def initialize(configuration, &block)
108
+ @config = configuration
109
+ @block = block
110
+ end
111
+
112
+ # Public: Runs the configuration block, exposing the configuration
113
+ # instance via the #config method.
114
+ #
115
+ # Returns nil.
116
+ def set_config
117
+ instance_eval(&block)
118
+
119
+ nil
120
+ end
121
+
122
+ private
123
+
124
+ # Internal: Returns the config block.
125
+ attr_reader :block
126
+
127
+ # Internal: Returns the Configuration instance.
128
+ attr_reader :config
129
+ end
130
+ end
131
+ end
@@ -4,8 +4,12 @@ require 'proletariat/testing'
4
4
  World(Proletariat::Testing)
5
5
 
6
6
  # Hide logs by default.
7
- AfterConfiguration do |config|
8
- Proletariat.logger = Logger.new('/dev/null')
7
+ AfterConfiguration do |_|
8
+ Proletariat.configure do
9
+ config.logger = Logger.new('/dev/null')
10
+ end
11
+
12
+ Proletariat.purge
9
13
  end
10
14
 
11
15
  # Ensure Proletariat running before each test.
@@ -2,27 +2,26 @@ 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 < Concurrent::Supervisor
5
+ class Manager
6
6
  # Public: Creates a new Manager instance.
7
7
  #
8
- # connection - An open Bunny::Session object.
9
- # exchange_name - A String of the RabbitMQ topic exchange.
10
8
  # worker_class - A subclass of Proletariat::Worker to handle messages.
11
- # options - A Hash of additional optional parameters (default: {}):
12
- # :worker_threads - The size of the worker thread pool.
13
- def initialize(connection, exchange_name, worker_class, options = {})
14
- super()
9
+ def initialize(worker_class)
10
+ @supervisor = Supervisor.new
15
11
 
16
- @connection = connection
17
- @exchange_name = exchange_name
18
- @worker_class = worker_class
19
- @worker_threads = options.fetch :worker_threads, 3
12
+ supervisor.supervise_pool('workers', Proletariat.worker_threads,
13
+ worker_class)
20
14
 
21
- create_worker_pool
22
- create_subscriber
15
+ @subscriber = Subscriber.new(supervisor['workers'],
16
+ generate_queue_config(worker_class))
23
17
 
24
- worker_pool.each { |worker| add_worker worker }
25
- add_worker subscriber
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 }
26
25
  end
27
26
 
28
27
  # Public: Purge the RabbitMQ queue.
@@ -36,64 +35,19 @@ module Proletariat
36
35
 
37
36
  private
38
37
 
39
- # Internal: Returns an open Bunny::Session object.
40
- attr_reader :connection
41
-
42
- # Internal: Returns the name of the RabbitMQ topic exchange.
43
- attr_reader :exchange_name
44
-
45
38
  # Internal: Returns the Subscriber actor for this Manager.
46
39
  attr_reader :subscriber
47
40
 
48
- # Internal: Returns the subclass of Worker used to process messages.
49
- attr_reader :worker_class
50
-
51
- # Internal: Returns the pool of initialized workers.
52
- attr_reader :worker_pool
53
-
54
- # Internal: Returns a shared mailbox for the pool of workers.
55
- attr_reader :workers_mailbox
56
-
57
- # Internal: Returns the number of worker threads in the worker pool.
58
- attr_reader :worker_threads
59
-
60
- # Internal: Assign a new Subscriber instance (configured for the current
61
- # worker type) to the manager's subscriber property.
62
- #
63
- # Returns nil.
64
- def create_subscriber
65
- @subscriber = Subscriber.new(
66
- connection,
67
- workers_mailbox,
68
- generate_queue_config
69
- )
70
-
71
- nil
72
- end
41
+ # Internal: The supervisor used to manage the Workers and Subscriber
42
+ attr_reader :supervisor
73
43
 
74
- # Internal: Assign new Concurrent::Poolbox and Array[Worker] to the
75
- # manager's workers_mailbox and worker_pool properties
76
- # respectively.
44
+ # Internal: Builds a new QueueConfig from a given Worker subclass.
77
45
  #
78
- # Returns nil.
79
- def create_worker_pool
80
- @workers_mailbox, @worker_pool = worker_class.pool(worker_threads)
81
-
82
- nil
83
- end
84
-
85
- # Internal: Builds a new QueueConfig object passing in some settings from
86
- # worker_class.
46
+ # worker_class - The Worker subclass to base settings on.
87
47
  #
88
48
  # Returns a new QueueConfig instance.
89
- def generate_queue_config
90
- QueueConfig.new(
91
- worker_class.name,
92
- exchange_name,
93
- worker_class.routing_keys,
94
- worker_threads,
95
- false
96
- )
49
+ def generate_queue_config(worker_class)
50
+ QueueConfig.new(worker_class.name, worker_class.routing_keys, false)
97
51
  end
98
52
  end
99
53
  end
@@ -1,87 +1,63 @@
1
+ require 'proletariat/concerns/logging'
2
+
1
3
  module Proletariat
2
- # Public: Listens for messages in it's mailbox and publishes
3
- # these to a RabbitMQ topic exchange.
4
- class Publisher < Concurrent::Actor
4
+ # Public: Receives messages and publishes them to a RabbitMQ topic exchange.
5
+ class Publisher
5
6
  include Concerns::Logging
6
7
 
7
8
  # Public: Creates a new Publisher instance.
8
- #
9
- # connection - An open Bunny::Session object.
10
- # exchange_name - A String of the RabbitMQ topic exchange.
11
- def initialize(connection, exchange_name)
12
- @channel = connection.create_channel
13
- @exchange = channel.topic(exchange_name, durable: true)
9
+ def initialize
10
+ @channel = Proletariat.connection.create_channel
11
+ @exchange = channel.topic(Proletariat.exchange_name, durable: true)
14
12
  end
15
13
 
16
- # Public: Called by the Concurrent framework to handle new mailbox
17
- # messages. Overridden in this subclass to push messages to a
18
- # RabbitMQ topic exchange.
19
- #
20
- # to - The routing key for the message to as a String. In accordance
21
- # with the RabbitMQ convention you can use the '*' character to
22
- # replace one word and the '#' to replace many words.
23
- # message - The message as a String.
14
+ # Public: Logs the 'online' status of the publisher.
24
15
  #
25
16
  # Returns nil.
26
- def act(to, message)
27
- publish(to, message)
17
+ def started
18
+ log_info 'Now online'
28
19
 
29
20
  nil
30
21
  end
31
22
 
32
- # Public: Called by the Concurrent framework on actor start. Overridden in
33
- # this subclass to log the status of the publisher.
23
+ # Public: Logs the 'offline' status of the publisher.
34
24
  #
35
25
  # Returns nil.
36
- def on_run
37
- super
38
- log_info 'Now online'
26
+ def stopped
27
+ log_info 'Now offline'
39
28
 
40
29
  nil
41
30
  end
42
31
 
43
- # Public: Called by the Concurrent framework on actor stop. Overridden in
44
- # this subclass to log the status of the publisher.
45
- def on_stop
32
+ # Public: Logs the 'shutting down' status of the publisher.
33
+ #
34
+ # Returns nil.
35
+ def stopping
46
36
  log_info 'Attempting graceful shutdown.'
47
- wait_for_publish_queue unless queue.empty?
48
-
49
- super
50
-
51
- log_info 'Now offline'
52
37
 
53
38
  nil
54
39
  end
55
40
 
56
- private
57
-
58
- # Internal: Returns the Bunny::Channel in use.
59
- attr_reader :channel
60
-
61
- # Internal: Returns the Bunny::Exchange in use.
62
- attr_reader :exchange
63
-
64
- # Internal: Handles the actual message send to the exchange.
41
+ # Public: Push a message to a RabbitMQ topic exchange.
65
42
  #
66
- # to - The routing key.
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.
67
46
  # message - The message as a String.
68
47
  #
69
48
  # Returns nil.
70
- def publish(to, message)
49
+ def work(to, message)
71
50
  exchange.publish message, routing_key: to, persistent: true
72
51
 
73
52
  nil
74
53
  end
75
54
 
76
- # Internal: Blocks until each message has been published.
77
- #
78
- # Returns nil.
79
- def wait_for_publish_queue
80
- log_info 'Waiting for work queue to drain.'
55
+ private
81
56
 
82
- publish(*queue.pop.message) until queue.empty?
57
+ # Internal: Returns the Bunny::Channel in use.
58
+ attr_reader :channel
83
59
 
84
- nil
85
- end
60
+ # Internal: Returns the Bunny::Exchange in use.
61
+ attr_reader :exchange
86
62
  end
87
63
  end
@@ -1,9 +1,5 @@
1
1
  # Internal: Value object to hold RabbitMQ settings.
2
- class QueueConfig < Struct.new(:worker_name,
3
- :exchange_name,
4
- :routing_keys,
5
- :prefetch,
6
- :auto_delete)
2
+ class QueueConfig < Struct.new(:worker_name, :routing_keys, :auto_delete)
7
3
  # Public: Create an underscored RabbitMQ queue name from the worker_name.
8
4
  #
9
5
  # Examples
@@ -7,37 +7,16 @@ module Proletariat
7
7
  # Public: Delegate lifecycle calls to the supervisor.
8
8
  def_delegators :supervisor, :run, :run!, :stop, :running?
9
9
 
10
- # Public: Returns an open Bunny::Session object.
11
- attr_reader :connection
12
-
13
- # Public: Returns the name of the RabbitMQ topic exchange.
14
- attr_reader :exchange_name
15
-
16
- # Public: Creates a new Runner instance. Default options should be fine for
17
- # most scenarios.
18
- #
19
- # options - A Hash of options (default: {}):
20
- # :connection - An open RabbitMQ::Session object.
21
- # :exchange_name - The RabbitMQ topic exchange name as a
22
- # String.
23
- # :publisher_threads - The size of the publisher thread pool.
24
- # :supervisor - A Supervisor instance.
25
- # :worker_classes - An Array of Worker subclasses.
26
- # :worker_threads - The size of the worker thread pool.
27
- def initialize(options = {})
28
- @connection = options.fetch :connection, create_connection
29
- @exchange_name = options.fetch :exchange_name, DEFAULT_EXCHANGE_NAME
30
- @publisher_threads = options.fetch :publisher_threads, 2
31
- @supervisor = options.fetch :supervisor, create_supervisor
32
- @worker_classes = options.fetch :worker_classes, []
33
- @worker_threads = options.fetch :worker_threads, 3
34
-
35
- @managers = []
36
-
37
- create_publisher_pool
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
38
16
 
39
- add_publishers_to_supervisor
40
- add_workers_to_supervisor
17
+ supervisor.supervise_pool('publishers', Proletariat.publisher_threads,
18
+ Publisher)
19
+ managers.each { |manager| supervisor.add_supervisor manager }
41
20
  end
42
21
 
43
22
  # Public: Publishes a message to RabbitMQ via the publisher pool.
@@ -49,7 +28,7 @@ module Proletariat
49
28
  #
50
29
  # Returns nil.
51
30
  def publish(to, message)
52
- publishers_mailbox.post to, message
31
+ supervisor['publishers'].post to, message
53
32
 
54
33
  nil
55
34
  end
@@ -68,82 +47,7 @@ module Proletariat
68
47
  # Internal: Returns an Array of the currently supervised Managers.
69
48
  attr_reader :managers
70
49
 
71
- # Internal: Returns the pool of initialized publishers.
72
- attr_reader :publisher_pool
73
-
74
- # Internal: Returns a shared mailbox for the pool of publishers.
75
- attr_reader :publishers_mailbox
76
-
77
- # Internal: Returns the number of publisher threads in the publisher pool.
78
- attr_reader :publisher_threads
79
-
80
50
  # Internal: Returns the supervisor instance.
81
51
  attr_reader :supervisor
82
-
83
- # Internal: Returns an Array of Worker subclasses.
84
- attr_reader :worker_classes
85
-
86
- # Internal: Returns the number of worker threads per manager.
87
- attr_reader :worker_threads
88
-
89
- # Internal: Adds each publisher in the publisher_pool to the supervisor.
90
- #
91
- # Returns nil.
92
- def add_publishers_to_supervisor
93
- publisher_pool.each { |publisher| supervisor.add_worker publisher }
94
-
95
- nil
96
- end
97
-
98
- # Internal: Creates a Manager per worker_class and adds these to the
99
- # supervisor.
100
- #
101
- # Returns nil.
102
- def add_workers_to_supervisor
103
- worker_classes.each do |worker_class|
104
- manager = create_manager(worker_class)
105
- @managers << manager
106
- supervisor.add_worker manager
107
- end
108
-
109
- nil
110
- end
111
-
112
- # Internal: Creates a new Bunny::Session and opens it.
113
- #
114
- # Returns an open Bunny::Session instance.
115
- def create_connection
116
- new_connection = Bunny.new
117
- new_connection.start
118
-
119
- new_connection
120
- end
121
-
122
- # Internal: Assign new Concurrent::Poolbox and Array[Publisher] to the
123
- # manager's publishers_mailbox and publisher_pool properties
124
- # respectively.
125
- #
126
- # Returns nil.
127
- def create_publisher_pool
128
- @publishers_mailbox, @publisher_pool = Publisher.pool(publisher_threads,
129
- connection,
130
- exchange_name)
131
- end
132
-
133
- # Internal: Creates a new Concurrent::Supervisor.
134
- #
135
- # Returns a Concurrent::Supervisor instance.
136
- def create_supervisor
137
- Concurrent::Supervisor.new
138
- end
139
-
140
- # Internal: Creates a Manager for a given Worker subclass adding relevant
141
- # arguments from Runner properties.
142
- #
143
- # Returns a Manager instance.
144
- def create_manager(worker_class)
145
- Manager.new(connection, exchange_name, worker_class,
146
- worker_threads: worker_threads)
147
- end
148
52
  end
149
53
  end