proletariat 0.0.2 → 0.0.3

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