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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b5b2573ecbcb0bba6ed669afbe997c428fe5892e
4
- data.tar.gz: f04b0420cf1d8dbf6f142a63a7441d14627120db
3
+ metadata.gz: 6cb914420e4992314911f3e2fc3a69cfdaf17cc1
4
+ data.tar.gz: fbc67bb27579bb70284d67b38047dae9ffa5ba72
5
5
  SHA512:
6
- metadata.gz: 4d3813e197e25da9ebd15b85cfcfd114d77c879dc42da712534c75e394e4c9f55781cdacbe8e7284fb0f2db0df7c24f5e0ffc96bd76e22835bf30df8756f2f5c
7
- data.tar.gz: 1b0fe700ce9145963d68c28adead0eda8c36b4ee6f50d60cb9f04faa7a2e91e6164281259d48f33f5fc7f4254ce016f6337b80d20d7d9c6d767d4e83d8dd46f8
6
+ metadata.gz: a71faf28c2fceec5f5f9bdb5cef95732bb3fae7fb5f43738155300b44b83884790ffa6096bff75cf7196b284b0f9aafc0bdea14deffd3874229fc17fc74f8d5c
7
+ data.tar.gz: acce71270fc40d16ce387ae11512566345d1aedd5412219358f9308c1940ebe6af9841b865bbdf2ee4c5c6b821521fb73dd1d8c5e227117cbe2824463b2a5ec0
data/Gemfile CHANGED
@@ -3,6 +3,8 @@ source "http://rubygems.org"
3
3
  # Specify your gem's dependencies in proletariat.gemspec
4
4
  gemspec
5
5
 
6
+ gem 'concurrent-ruby', github: 'ruby-concurrency/concurrent-ruby'
7
+
6
8
  platforms :rbx do
7
9
  gem 'rubysl'
8
10
  end
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 this line to your application's `Gemfile`:
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/supervisor'
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, :run!, :stop, :running?, :publish, :purge
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
- 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)
1
+ require 'proletariat/concurrency/actor_common'
46
2
 
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
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
  #
@@ -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! unless Proletariat.running?
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