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