proletariat 0.0.1

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: ccf124f3fb011f0dd8f06666daa0f8748c70fe32
4
+ data.tar.gz: a601e7e559d77d40b30f91be6824d4d51b13af3b
5
+ SHA512:
6
+ metadata.gz: 22cd707dcdcbfbefbff38a8c4c4bf74cceefa87c42c30706f9028a89d2039fc0095ac1deff157f5efa9c4e60c881e4fe6cd2b8f10596142ef257cfbb54460cdb
7
+ data.tar.gz: 0160aced5ba612299ddb023b8cf19c361a2d02e9212bfb8c2e1d79dfbe2db07d017378ba7b9b029d894e7d18903ee371ec34623cef2941e611e4f80148a34fe6
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .ruby-version
6
+ .ruby-gemset
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in proletariat.gemspec
4
+ gemspec
5
+
6
+ platforms :rbx do
7
+ gem 'rubysl'
8
+ end
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # Proletariat: Background Workers Unite!
2
+
3
+ Lightweight background processing in Ruby powered by RabbitMQ and the excellent concurrent-ruby gem.
4
+
5
+ ### Warning!
6
+
7
+ This software is early-alpha, may contain bugs and change considerably in the near future.
8
+
9
+ For production use I recommend the better supported and more fully-featured [Sneakers gem](https://github.com/jondot/sneakers).
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's `Gemfile`:
14
+
15
+ gem 'proletariat'
16
+
17
+ And run:
18
+
19
+ $ bundle
20
+
21
+ ## How to use
22
+
23
+ ### RabbitMQ connection config
24
+
25
+ If you aren't using default RabbitMQ connection settings, ensure the `RABBITMQ_URL` env variable is present. Here's how that might look in your `.env` if you use Foreman:
26
+
27
+ RABBITMQ_URL=amqp://someuser:somepass@127.0.0.1/another_vhost
28
+
29
+ ### Setting up a Worker
30
+
31
+ Your worker classes should inherit from `Proletariat::Worker` and implement the `#work` method.
32
+
33
+ Proletariat works exclusively on RabbitMQ Topic exchanges and routing keys can be bound via a call to `.listen_on`. This can be called multiple times to bind to multiple keys.
34
+
35
+ The `#work` method should return `:ok` on success or `:drop` / `:requeue` on failure.
36
+
37
+ Here's a complete example:
38
+
39
+ class SendUserIntroductoryEmail < Proletariat::Worker
40
+ listen_on 'user.created'
41
+
42
+ def work(message)
43
+ params = JSON.parse(message)
44
+
45
+ UserMailer.introductory_email(params).deliver!
46
+
47
+ publish 'email_sent.user.introductory', {id: params['id']}.to_json
48
+
49
+ :ok
50
+ end
51
+ end
52
+
53
+ ### Select your Workers
54
+
55
+ If you are using Rails just create a `proletariat.rb` file in your initializers directory.
56
+
57
+ Proletariat.configure worker_classes: [SendUserIntroductoryEmail, SomeOtherWorker]
58
+ Proletariat.run!
59
+
60
+ Or define the `WORKERS` env variable.
61
+
62
+ WORKERS=SendUserIntroductoryEmail,SomeOtherWorker
63
+
64
+ ### Testing with Cucumber
65
+
66
+ Add the following to your `env.rb`:
67
+
68
+ require 'proletariat/cucumber'
69
+
70
+ Use the provided helpers in your step definitions to synchronize your test suite with your workers without sacrificing the ability to test in production-like environment:
71
+
72
+ When(/^I submit a valid 'register user' form$/) do
73
+ wait_for message.on_topic('email_sent.user.introductory') do
74
+ visit ...
75
+ fill_in ...
76
+ submit ...
77
+ end
78
+ end
79
+
80
+ Then(/^the user should receive an introductory email$/) do
81
+ expect(unread_emails_for(new_user_email).size).to eq 1
82
+ end
83
+
84
+ ## FAQ
85
+
86
+ #### Why build another RabbitMQ background worker library?
87
+
88
+ 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.
89
+
90
+ ## TODO
91
+ - Improve test suite :(
92
+ - Add command line interface
93
+ - Abstract retry strategies
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ module Proletariat
2
+ module Concerns
3
+ # Public: Mixin to handle logging concerns.
4
+ module Logging
5
+ # Public: Logs info to the process-wide logger.
6
+ #
7
+ # message - The message to be logged.
8
+ #
9
+ # Returns nil.
10
+ def log_info(message)
11
+ Proletariat.logger.info "#{self.class.name} #{object_id}: #{message}"
12
+
13
+ nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ require 'proletariat/testing'
2
+
3
+ # Mix testing helpers into World.
4
+ World(Proletariat::Testing)
5
+
6
+ # Hide logs by default.
7
+ AfterConfiguration do |config|
8
+ Proletariat.logger = Logger.new('/dev/null')
9
+ end
10
+
11
+ # Ensure Proletariat running before each test.
12
+ Before do
13
+ Proletariat.run! unless Proletariat.running?
14
+ end
15
+
16
+ # Stop workers and purge queues between scenarios.
17
+ After do
18
+ Proletariat.stop
19
+ Proletariat.purge
20
+ end
@@ -0,0 +1,99 @@
1
+ module Proletariat
2
+ # Public: Maintains a pool of worker threads and a RabbitMQ subscriber
3
+ # thread. Uses information from the worker class to generate queue
4
+ # config.
5
+ class Manager < Concurrent::Supervisor
6
+ # Public: Creates a new Manager instance.
7
+ #
8
+ # connection - An open Bunny::Session object.
9
+ # exchange_name - A String of the RabbitMQ topic exchange.
10
+ # 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()
15
+
16
+ @connection = connection
17
+ @exchange_name = exchange_name
18
+ @worker_class = worker_class
19
+ @worker_threads = options.fetch :worker_threads, 3
20
+
21
+ create_worker_pool
22
+ create_subscriber
23
+
24
+ worker_pool.each { |worker| add_worker worker }
25
+ add_worker subscriber
26
+ end
27
+
28
+ # Public: Purge the RabbitMQ queue.
29
+ #
30
+ # Returns nil.
31
+ def purge
32
+ subscriber.purge
33
+
34
+ nil
35
+ end
36
+
37
+ private
38
+
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
+ # Internal: Returns the Subscriber actor for this Manager.
46
+ attr_reader :subscriber
47
+
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
73
+
74
+ # Internal: Assign new Concurrent::Poolbox and Array[Worker] to the
75
+ # manager's workers_mailbox and worker_pool properties
76
+ # respectively.
77
+ #
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.
87
+ #
88
+ # 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
+ )
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,87 @@
1
+ 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
5
+ include Concerns::Logging
6
+
7
+ # 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)
14
+ end
15
+
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.
24
+ #
25
+ # Returns nil.
26
+ def act(to, message)
27
+ publish(to, message)
28
+
29
+ nil
30
+ end
31
+
32
+ # Public: Called by the Concurrent framework on actor start. Overridden in
33
+ # this subclass to log the status of the publisher.
34
+ #
35
+ # Returns nil.
36
+ def on_run
37
+ super
38
+ log_info 'Now online'
39
+
40
+ nil
41
+ end
42
+
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
46
+ log_info 'Attempting graceful shutdown.'
47
+ wait_for_publish_queue unless queue.empty?
48
+
49
+ super
50
+
51
+ log_info 'Now offline'
52
+
53
+ nil
54
+ end
55
+
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.
65
+ #
66
+ # to - The routing key.
67
+ # message - The message as a String.
68
+ #
69
+ # Returns nil.
70
+ def publish(to, message)
71
+ exchange.publish message, routing_key: to, persistent: true
72
+
73
+ nil
74
+ end
75
+
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.'
81
+
82
+ publish(*queue.pop.message) until queue.empty?
83
+
84
+ nil
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,26 @@
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)
7
+ # Public: Create an underscored RabbitMQ queue name from the worker_name.
8
+ #
9
+ # Examples
10
+ #
11
+ # config = QueueConfig.new('ExampleWorker', ...)
12
+ # config.queue_name
13
+ # # => 'example_worker'
14
+ #
15
+ # Returns the queue name as a String.
16
+ def queue_name
17
+ @queue_name ||= begin
18
+ worker_name
19
+ .gsub(/::/, '/')
20
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
21
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
22
+ .tr('-', '_')
23
+ .downcase
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,149 @@
1
+ module Proletariat
2
+ # Public: Sets up a supervisor which maintains a single Publisher and a
3
+ # per-worker Manager instance.
4
+ class Runner
5
+ extend Forwardable
6
+
7
+ # Public: Delegate lifecycle calls to the supervisor.
8
+ def_delegators :supervisor, :run, :run!, :stop, :running?
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
38
+
39
+ add_publishers_to_supervisor
40
+ add_workers_to_supervisor
41
+ end
42
+
43
+ # Public: Publishes a message to RabbitMQ via the publisher pool.
44
+ #
45
+ # to - The routing key for the message to as a String. In accordance
46
+ # with the RabbitMQ convention you can use the '*' character to
47
+ # replace one word and the '#' to replace many words.
48
+ # message - The message as a String.
49
+ #
50
+ # Returns nil.
51
+ def publish(to, message)
52
+ publishers_mailbox.post to, message
53
+
54
+ nil
55
+ end
56
+
57
+ # Public: Purge the RabbitMQ queues.
58
+ #
59
+ # Returns nil.
60
+ def purge
61
+ managers.each { |manager| manager.purge }
62
+
63
+ nil
64
+ end
65
+
66
+ private
67
+
68
+ # Internal: Returns an Array of the currently supervised Managers.
69
+ attr_reader :managers
70
+
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
+ # Internal: Returns the supervisor instance.
81
+ 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
+ end
149
+ end