proletariat 0.0.6 → 0.1.0
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 +4 -4
- data/Gemfile +2 -0
- data/README.md +2 -1
- data/lib/proletariat.rb +15 -2
- data/lib/proletariat/concurrency/actor.rb +12 -67
- data/lib/proletariat/concurrency/actor_common.rb +30 -0
- data/lib/proletariat/concurrency/poolable_actor.rb +25 -0
- data/lib/proletariat/configuration.rb +15 -0
- data/lib/proletariat/cucumber.rb +2 -4
- data/lib/proletariat/exception_handler.rb +45 -0
- data/lib/proletariat/exception_handler/drop.rb +15 -0
- data/lib/proletariat/exception_handler/exponential_backoff.rb +130 -0
- data/lib/proletariat/manager.rb +30 -28
- data/lib/proletariat/message.rb +9 -0
- data/lib/proletariat/publisher.rb +19 -41
- data/lib/proletariat/runner.rb +18 -30
- data/lib/proletariat/subscriber.rb +91 -177
- data/lib/proletariat/tasks.rb +1 -1
- data/lib/proletariat/testing/expectation_guarantor.rb +36 -30
- data/lib/proletariat/version.rb +1 -1
- data/lib/proletariat/worker.rb +47 -34
- data/proletariat.gemspec +3 -3
- data/spec/lib/proletariat_spec.rb +18 -7
- data/spec/lib/testing/expectation_guarantor_spec.rb +0 -32
- data/spec/lib/worker_spec.rb +13 -15
- metadata +15 -12
- data/lib/proletariat/concurrency/supervisor.rb +0 -31
- data/spec/lib/publisher_spec.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6cb914420e4992314911f3e2fc3a69cfdaf17cc1
|
4
|
+
data.tar.gz: fbc67bb27579bb70284d67b38047dae9ffa5ba72
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a71faf28c2fceec5f5f9bdb5cef95732bb3fae7fb5f43738155300b44b83884790ffa6096bff75cf7196b284b0f9aafc0bdea14deffd3874229fc17fc74f8d5c
|
7
|
+
data.tar.gz: acce71270fc40d16ce387ae11512566345d1aedd5412219358f9308c1940ebe6af9841b865bbdf2ee4c5c6b821521fb73dd1d8c5e227117cbe2824463b2a5ec0
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -12,9 +12,10 @@ For production use I recommend the better supported and more fully-featured [Sne
|
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
15
|
-
Add
|
15
|
+
Add these lines to your application's `Gemfile`:
|
16
16
|
|
17
17
|
gem 'proletariat'
|
18
|
+
gem 'concurrent-ruby', github: 'ruby-concurrency/concurrent-ruby'
|
18
19
|
|
19
20
|
And run:
|
20
21
|
|
data/lib/proletariat.rb
CHANGED
@@ -6,18 +6,23 @@ require 'logger'
|
|
6
6
|
require 'forwardable'
|
7
7
|
|
8
8
|
require 'proletariat/concurrency/actor'
|
9
|
-
require 'proletariat/concurrency/
|
9
|
+
require 'proletariat/concurrency/poolable_actor'
|
10
10
|
|
11
11
|
require 'proletariat/util/worker_description_parser'
|
12
12
|
|
13
13
|
require 'proletariat/configuration'
|
14
|
+
require 'proletariat/exception_handler'
|
14
15
|
require 'proletariat/manager'
|
16
|
+
require 'proletariat/message'
|
15
17
|
require 'proletariat/publisher'
|
16
18
|
require 'proletariat/queue_config'
|
17
19
|
require 'proletariat/runner'
|
18
20
|
require 'proletariat/subscriber'
|
19
21
|
require 'proletariat/worker'
|
20
22
|
|
23
|
+
require 'proletariat/exception_handler/drop'
|
24
|
+
require 'proletariat/exception_handler/exponential_backoff'
|
25
|
+
|
21
26
|
# Public: Creates the Proletariat namespace and holds a process-wide Runner
|
22
27
|
# instance as well as access to the configuration attributes.
|
23
28
|
module Proletariat
|
@@ -25,7 +30,7 @@ module Proletariat
|
|
25
30
|
extend Forwardable
|
26
31
|
|
27
32
|
# Public: Delegate lifecycle calls to the process-wide Runner.
|
28
|
-
def_delegators :runner, :run, :
|
33
|
+
def_delegators :runner, :run, :running?, :stop
|
29
34
|
|
30
35
|
# Public: Allows configuration of Proletariat via given block.
|
31
36
|
#
|
@@ -38,6 +43,10 @@ module Proletariat
|
|
38
43
|
nil
|
39
44
|
end
|
40
45
|
|
46
|
+
def publish(to, body = '', headers = {})
|
47
|
+
publisher_pool << Message.new(to, body, headers)
|
48
|
+
end
|
49
|
+
|
41
50
|
def runner
|
42
51
|
@runner ||= Runner.new
|
43
52
|
end
|
@@ -56,5 +65,9 @@ module Proletariat
|
|
56
65
|
def config
|
57
66
|
@config ||= Configuration.new
|
58
67
|
end
|
68
|
+
|
69
|
+
def publisher_pool
|
70
|
+
@publisher_pool ||= Publisher.pool(Proletariat.publisher_threads)
|
71
|
+
end
|
59
72
|
end
|
60
73
|
end
|
@@ -1,71 +1,16 @@
|
|
1
|
-
|
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)
|
1
|
+
require 'proletariat/concurrency/actor_common'
|
46
2
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
3
|
+
module Proletariat
|
4
|
+
# Public: Interface abstraction for Concurrent::Actor.
|
5
|
+
class Actor < Concurrent::Actor::RestartingContext
|
6
|
+
include ActorCommon
|
7
|
+
|
8
|
+
def on_message(message)
|
9
|
+
if respond_to?(:work_method)
|
10
|
+
send work_method, message
|
11
|
+
else
|
12
|
+
work message
|
13
|
+
end
|
69
14
|
end
|
70
15
|
end
|
71
16
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Proletariat
|
2
|
+
# Internal: Common behavior for actor base classes.
|
3
|
+
module ActorCommon
|
4
|
+
def self.included(base)
|
5
|
+
base.class_exec do
|
6
|
+
def initialize(*args)
|
7
|
+
starting if respond_to?(:starting)
|
8
|
+
|
9
|
+
super
|
10
|
+
|
11
|
+
started if respond_to?(:started)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_event(event)
|
17
|
+
if event == :terminated
|
18
|
+
stopping if respond_to?(:stopping)
|
19
|
+
|
20
|
+
cleanup if respond_to?(:cleanup)
|
21
|
+
|
22
|
+
super
|
23
|
+
|
24
|
+
stopped if respond_to?(:stopped)
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'proletariat/concurrency/actor_common'
|
2
|
+
|
3
|
+
module Proletariat
|
4
|
+
# Public: Interface abstraction for a pool of Concurrent::Actor instances.
|
5
|
+
class PoolableActor < Concurrent::Actor::Utils::AbstractWorker
|
6
|
+
include ActorCommon
|
7
|
+
|
8
|
+
def on_message(message)
|
9
|
+
if respond_to?(:work_method)
|
10
|
+
send work_method, message
|
11
|
+
else
|
12
|
+
work message
|
13
|
+
end
|
14
|
+
ensure
|
15
|
+
@balancer << :subscribe
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.pool(pool_size, suffix = '')
|
19
|
+
Concurrent::Actor::Utils::
|
20
|
+
Pool.spawn!("#{to_s}_pool", pool_size) do |b, i|
|
21
|
+
spawn(name: "#{to_s}_#{i}_#{suffix}", supervise: true, args: [b])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -87,6 +87,21 @@ module Proletariat
|
|
87
87
|
@publisher_threads ||= DEFAULT_PUBLISHER_THREADS
|
88
88
|
end
|
89
89
|
|
90
|
+
# Public: Enables test mode. Queues will auto-delete after use.
|
91
|
+
#
|
92
|
+
# Returns nil.
|
93
|
+
def test_mode!
|
94
|
+
@test_mode = true
|
95
|
+
end
|
96
|
+
|
97
|
+
# Public: Returns whether test mode is enabled or not.
|
98
|
+
#
|
99
|
+
# Returns true if test mode enabled.
|
100
|
+
# Returns false if test mode disabled.
|
101
|
+
def test_mode?
|
102
|
+
!!@test_mode
|
103
|
+
end
|
104
|
+
|
90
105
|
# Public: Returns the set worker classes or a default pulled from the
|
91
106
|
# WORKERS env variable.
|
92
107
|
#
|
data/lib/proletariat/cucumber.rb
CHANGED
@@ -7,18 +7,16 @@ World(Proletariat::Testing)
|
|
7
7
|
AfterConfiguration do |_|
|
8
8
|
Proletariat.configure do
|
9
9
|
config.logger = Logger.new('/dev/null')
|
10
|
+
config.test_mode!
|
10
11
|
end
|
11
|
-
|
12
|
-
Proletariat.purge
|
13
12
|
end
|
14
13
|
|
15
14
|
# Ensure Proletariat running before each test.
|
16
15
|
Before do
|
17
|
-
Proletariat.run
|
16
|
+
Proletariat.run unless Proletariat.running?
|
18
17
|
end
|
19
18
|
|
20
19
|
# Stop workers and purge queues between scenarios.
|
21
20
|
After do
|
22
21
|
Proletariat.stop
|
23
|
-
Proletariat.purge
|
24
22
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'proletariat/concerns/logging'
|
2
|
+
|
3
|
+
module Proletariat
|
4
|
+
# Public: Handles messages whose work raised an exception. Should overwrite
|
5
|
+
# the #work method to implement retry/drop logic.
|
6
|
+
class ExceptionHandler < Actor
|
7
|
+
include Concerns::Logging
|
8
|
+
|
9
|
+
def initialize(queue_name)
|
10
|
+
@queue_name = queue_name
|
11
|
+
setup
|
12
|
+
end
|
13
|
+
|
14
|
+
# Internal: Handles the Actor mailbox. Delegates work to #work.
|
15
|
+
#
|
16
|
+
# message - A Message to send.
|
17
|
+
def actor_work(message)
|
18
|
+
work message.body, message.to, message.headers if message.is_a?(Message)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Callback hook for initialization.
|
22
|
+
def setup
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: Handles an incoming message to perform background work.
|
26
|
+
#
|
27
|
+
# body - The failed message body.
|
28
|
+
# to - The failed message's routing key.
|
29
|
+
# headers - The failed message's headers.
|
30
|
+
#
|
31
|
+
# Raises NotImplementedError unless implemented in subclass.
|
32
|
+
def work(body, to, headers)
|
33
|
+
fail NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
# Public: Use #actor_work to handle the actor mailbox.
|
37
|
+
def work_method
|
38
|
+
:actor_work
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
attr_reader :queue_name
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Proletariat
|
2
|
+
# Internal: Exception handler which just drops failed messages.
|
3
|
+
class Drop < ExceptionHandler
|
4
|
+
# Public: Does nothing with the failed messages.
|
5
|
+
#
|
6
|
+
# body - The failed message body.
|
7
|
+
# to - The failed message's routing key.
|
8
|
+
# headers - The failed message's headers.
|
9
|
+
#
|
10
|
+
# Returns nil.
|
11
|
+
def work(message, to, headers)
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Proletariat
|
2
|
+
# Internal: Exception handler with an exponential back-off strategy. Uses
|
3
|
+
# dead-letter queues.
|
4
|
+
class ExponentialBackoff < ExceptionHandler
|
5
|
+
# Public: Called on actor termination. Used to stop consumption off the
|
6
|
+
# requeue queue and close the channel.
|
7
|
+
#
|
8
|
+
# Returns nil.
|
9
|
+
def cleanup
|
10
|
+
@consumer.cancel if @consumer
|
11
|
+
@channel.close if @channel && channel.open?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: Callback hook for initialization. Kicks off the queue setup.
|
15
|
+
def setup
|
16
|
+
setup_requeue_queue
|
17
|
+
consume_requeue
|
18
|
+
setup_retry_queues
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Puts messages into a delay queue to be retried in the future.
|
22
|
+
#
|
23
|
+
# body - The failed message body.
|
24
|
+
# to - The failed message's routing key.
|
25
|
+
# headers - The failed message's headers.
|
26
|
+
#
|
27
|
+
# Returns nil.
|
28
|
+
def work(message, to, headers)
|
29
|
+
failures = queue_for_x_death(headers['x-death'])
|
30
|
+
|
31
|
+
exchange.publish(message,
|
32
|
+
routing_key: "#{queue_name}_delay_#{failures}",
|
33
|
+
persistent: !Proletariat.test_mode?,
|
34
|
+
headers: headers.merge('proletariat-to' => to))
|
35
|
+
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Internal: Returns the Bunny::Channel in use.
|
42
|
+
def channel
|
43
|
+
@channel ||= Proletariat.connection.create_channel
|
44
|
+
end
|
45
|
+
|
46
|
+
# Internal: Starts a consumer on the requeue queue which puts messages back
|
47
|
+
# onto the main queue.
|
48
|
+
#
|
49
|
+
# Returns nil.
|
50
|
+
def consume_requeue
|
51
|
+
@consumer = @requeue.subscribe do |info, props, body|
|
52
|
+
Proletariat.publish(props.headers['proletariat-to'], body,
|
53
|
+
props.headers)
|
54
|
+
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Internal: Returns the Bunny::Exchange in use.
|
60
|
+
def exchange
|
61
|
+
@exchange ||= channel.direct(exchange_name,
|
62
|
+
durable: !Proletariat.test_mode?,
|
63
|
+
auto_delete: Proletariat.test_mode?)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Internal: Returns a new exchange name for a direct exchange.
|
67
|
+
def exchange_name
|
68
|
+
"#{Proletariat.exchange_name}_retry"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Internal: Determines which delay queue a failed message should go in
|
72
|
+
# based on number of past fails shown in x-death header.
|
73
|
+
#
|
74
|
+
# header - the x-death header.
|
75
|
+
#
|
76
|
+
# Returns an Integer.
|
77
|
+
def queue_for_x_death(header)
|
78
|
+
if header
|
79
|
+
[header.length, retry_delay_times.length - 1].min
|
80
|
+
else
|
81
|
+
0
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Internal: Calculates an exponential retry delay based on the previous
|
86
|
+
# number of failures. Capped with configuration setting.
|
87
|
+
#
|
88
|
+
# Returns the delay in seconds as a Fixnum.
|
89
|
+
def retry_delay_times
|
90
|
+
@delay_times ||= begin
|
91
|
+
(1..Float::INFINITY)
|
92
|
+
.lazy
|
93
|
+
.map { |i| i**i }
|
94
|
+
.take_while { |i| i < Proletariat.max_retry_delay }
|
95
|
+
.to_a
|
96
|
+
.push(Proletariat.max_retry_delay)
|
97
|
+
.map { |seconds| seconds * 1000 }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Internal: Creates the requeue queue and binds it to the exchange.
|
102
|
+
def setup_requeue_queue
|
103
|
+
@requeue = channel.queue("#{queue_name}_requeue",
|
104
|
+
durable: !Proletariat.test_mode?,
|
105
|
+
auto_delete: Proletariat.test_mode?)
|
106
|
+
|
107
|
+
@requeue.bind(exchange, routing_key: "#{queue_name}_requeue")
|
108
|
+
end
|
109
|
+
|
110
|
+
# Internal: Create a delay queue and binds it to the exchange.
|
111
|
+
def setup_retry_queue(delay, index)
|
112
|
+
channel.queue("#{queue_name}_delay_#{index}",
|
113
|
+
durable: !Proletariat.test_mode?,
|
114
|
+
auto_delete: Proletariat.test_mode?,
|
115
|
+
arguments: {
|
116
|
+
'x-dead-letter-exchange' => exchange_name,
|
117
|
+
'x-dead-letter-routing-key' => "#{queue_name}_requeue",
|
118
|
+
'x-message-ttl' => delay
|
119
|
+
}
|
120
|
+
).bind(exchange, routing_key: "#{queue_name}_delay_#{index}")
|
121
|
+
end
|
122
|
+
|
123
|
+
# Internal: Creates the delay queues based on the max retry time.
|
124
|
+
def setup_retry_queues
|
125
|
+
retry_delay_times.each_with_index.map do |delay, index|
|
126
|
+
setup_retry_queue(delay, index)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|