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 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