action_subscriber 3.0.2 → 4.0.0.pre1

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: bf91f9b2566e00305d9bcc1bb873c550390f29ad
4
- data.tar.gz: e523407919e74f5af41f321e4a2bc64d00bcaf59
3
+ metadata.gz: 2ddc9cc72ddda0d121c1ffec7e99dca6d0ca323b
4
+ data.tar.gz: 401a3e22fd0bbbc48cfc414c55924b11b8146ac8
5
5
  SHA512:
6
- metadata.gz: f4d5e7566aabf121149d7d2eb453f8892872b268e018a07b6afbada975ce1dcd132307d54a35416077413f8b05174bdd0e0eae869e15a96f7cf4b2bcb8a1aab3
7
- data.tar.gz: 13c8d270b87969981b92f9777cde802ad1acc9b85c5d68577105730746561f6bc72de1d055efd0956a9cb9e5482dcbb1bf2930a147078af29ead94c6163c463a
6
+ metadata.gz: 1f8cca49c8ce821d1dca22951a41df636260a48d0a018c65ac3539dc27d7ce2ee200eb4d2e473d9c153a5d5ac01460efb0096bb5e94df850735c158c8ee3655f
7
+ data.tar.gz: 9eb940f6546ed1d35314aac1004eb87d00012a251fa7b61981b9dde4725be71317e8d7f0c4b83b6be495e584c679a9221dcb9d02d6b734c1e80b25e2b0df330d
data/README.md CHANGED
@@ -13,6 +13,38 @@ I test on Ruby 2.2.1 and Jruby 9.x. MRI 1.9 and jRuby 1.7 are still supported.
13
13
 
14
14
  If you want to use MRI 1.9 you will need to lock down the `amq-protocol` and `bunny` gems to `< 2.0` since they both require ruby 2.0+.
15
15
 
16
+ Migrating from ActionSubscriber 3.X or earlier
17
+ ----------------------------------------------
18
+
19
+ If you were using the `--mode=pop` from the 2.X or 3.X version of ActionSubscriber you can get the same sort of behavior by drawing your routes like this:
20
+
21
+ ```ruby
22
+ ::ActionSubscriber.draw_routes do
23
+ # instead of creating custom threadpools you set the threadpool size of your connection here in the routes
24
+ # you can set the threadpool size for the default connection via the `::ActionSubscriber.configuration.threadpool_size = 16`
25
+ route UserSubscriber, :created,
26
+ :prefetch => 1,
27
+ :concurrency => 16,
28
+ :acknowledgements => true
29
+
30
+ # in user_subscriber.rb make sure to set `at_most_once!` like this
31
+ #
32
+ # class UserSubscriber < ::ActionSubscriber::Base
33
+ # at_most_once!
34
+ # end
35
+
36
+ # If you were previously using custom threadpools for different routes you can mimic that behavior by opening multiple connections
37
+ connection(:slow_work, :threadpool_size => 32) do
38
+ route UserSubscriber, :created,
39
+ :prefetch => 1,
40
+ :concurrency => 32,
41
+ :acknowledgements => true
42
+ end
43
+ end
44
+ ```
45
+
46
+ That will give you a similar behavior to the old `--mode=pop` where messages polled from the server, but with reduced latency.
47
+
16
48
  Supported Message Types
17
49
  -----------------
18
50
  ActionSubscriber support JSON and plain text out of the box, but you can easily
@@ -51,12 +83,10 @@ Now you can start your subscriber process with:
51
83
 
52
84
 
53
85
  ```
54
- $ bundle exec action_subscriber start --mode=subscribe
86
+ $ bundle exec action_subscriber start
55
87
  ```
56
88
 
57
- This will start your subscribers in a mode where they connect to rabbitmq and let the broker push messages down to them.
58
-
59
- You can also start in `--mode=pop` where your process will poll the broker for messages.
89
+ This will connect your subscribers to the rabbitmq broker and allow it to push messages down to your subscribers.
60
90
 
61
91
  Configuration
62
92
  -----------------
@@ -76,16 +106,18 @@ Other configuration options include :
76
106
  * config.error_handler - handle error like you want to handle them!
77
107
  * config.heartbeat - number of seconds between hearbeats (default 5) [see bunny documentation for more details](http://rubybunny.info/articles/connecting.html)
78
108
  * config.hosts - an array of hostnames in your cluster (ie `["rabbit1.myapp.com", "rabbit2.myapp.com"]`)
79
- * config.pop_interval - how long to wait between polling for messages in `--mode=pop`. It should be a number of milliseconds
80
109
  * config.threadpool_size - set the number of threads availiable to action_subscriber
81
110
  * config.timeout - how many seconds to allow rabbit to respond before timing out
82
- * config.times_to_pop - when using RabbitMQ's pull API, the number of messages we will grab each time we pool the broker
83
111
 
84
112
  Message Acknowledgment
85
113
  ----------------------
86
114
  ### no_acknolwedgement!
87
115
 
88
116
  This mode is the default. Rabbit is told to not expect any message acknowledgements so messages will be lost if an error occurs.
117
+ This also allows the broker to send messages as quickly as it wants down to your subscriber.
118
+
119
+ > Warning: If messages arrive very quickly this could cause your process to crash as your memory fills up with unprocessed message.
120
+ > We highly recommend you use `at_least_once!` mode to provide a throttle so the broker does not overwhelm your process with messages.
89
121
 
90
122
  ### manual_acknowledgement!
91
123
 
@@ -97,7 +129,9 @@ Rabbit is told to expect message acknowledgements, but sending the acknowledgeme
97
129
 
98
130
  ### at_least_once!
99
131
 
100
- Rabbit is told to expect message acknowledgements, but sending the acknowledgement is left up to ActionSubscriber. We send the acknowledgement right after calling your subscriber. If an error is raised by your subscriber we reject the message instead of acknowledging it. Rejected messages go back to rabbit and will be re-delivered.
132
+ Rabbit is told to expect message acknowledgements, but sending the acknowledgement is left up to ActionSubscriber.
133
+ We send the acknowledgement right after calling your subscriber.
134
+ If an error is raised your message will be retried on a sent back to rabbitmq and retried on an exponential backoff schedule.
101
135
 
102
136
  Testing
103
137
  -----------------
@@ -26,7 +26,6 @@ Gem::Specification.new do |spec|
26
26
  else
27
27
  spec.add_dependency 'bunny', '>= 1.5.0'
28
28
  end
29
- spec.add_dependency 'lifeguard', '>= 0.0.9'
30
29
  spec.add_dependency 'middleware'
31
30
  spec.add_dependency 'thor'
32
31
 
@@ -7,7 +7,6 @@ module ActionSubscriber
7
7
  class CLI < ::Thor
8
8
  class_option :allow_low_priority_methods, :type => :boolean, :desc => "subscribe to low priority queues in addition to the normal queues", :default => false
9
9
  class_option :app, :default => "./config/environment.rb"
10
- class_option :mode
11
10
  class_option :host
12
11
  class_option :hosts
13
12
  class_option :prefetch, :type => :numeric, :desc => "how many messages to hold in the local queue in subscribe mode"
@@ -36,14 +35,7 @@ module ActionSubscriber
36
35
  # Require action_subscriber if the application did not.
37
36
  require "action_subscriber"
38
37
 
39
- case ::ActionSubscriber.configuration.mode
40
- when /pop/i then
41
- ::ActionSubscriber::Babou.auto_pop!
42
- when /subscribe/i then
43
- ::ActionSubscriber::Babou.start_subscribers
44
- else
45
- fail "ActionSubscriber.configuration.mode must be 'pop' or 'subscribe'. Currently set to '#{::ActionSubscriber.configuration.mode}'"
46
- end
38
+ ::ActionSubscriber::Babou.start_subscribers
47
39
  end
48
40
  end
49
41
 
@@ -62,15 +54,15 @@ module ActionSubscriber
62
54
  end
63
55
 
64
56
  trap(:TTIN) {
65
- ::ActionSubscriber.print_subscriptions
57
+ ::Thread.new do
58
+ ::ActionSubscriber.print_subscriptions
59
+ end
66
60
  }
67
61
 
68
62
  trap(:USR2) {
69
- puts <<-CONFIG.strip_heredoc
70
- Action Subscriber Stats
71
- Pool Size: #{ ::ActionSubscriber.config.threadpool_size }
72
- Ready Size: #{ ::ActionSubscriber::Threadpool.ready_size }
73
- CONFIG
63
+ ::Thread.new do
64
+ ::ActionSubscriber.print_threadpool_stats
65
+ end.join
74
66
  }
75
67
 
76
68
  ::ActionSubscriber::CLI.start(ARGV)
@@ -5,7 +5,6 @@ if ::RUBY_PLATFORM == "java"
5
5
  else
6
6
  require "bunny"
7
7
  end
8
- require "lifeguard"
9
8
  require "middleware"
10
9
  require "thread"
11
10
 
@@ -28,7 +27,6 @@ require "action_subscriber/babou"
28
27
  require "action_subscriber/route"
29
28
  require "action_subscriber/route_set"
30
29
  require "action_subscriber/router"
31
- require "action_subscriber/threadpool"
32
30
  require "action_subscriber/base"
33
31
 
34
32
  module ActionSubscriber
@@ -36,21 +34,6 @@ module ActionSubscriber
36
34
  # Public Class Methods
37
35
  #
38
36
 
39
- # Loop over all subscribers and pull messages if there are
40
- # any waiting in the queue for us.
41
- #
42
- def self.auto_pop!
43
- return if ::ActionSubscriber::Threadpool.busy?
44
- route_set.auto_pop!
45
- end
46
-
47
- # Loop over all subscribers and register each as
48
- # a subscriber.
49
- #
50
- def self.auto_subscribe!
51
- route_set.auto_subscribe!
52
- end
53
-
54
37
  def self.configure
55
38
  yield(configuration) if block_given?
56
39
  end
@@ -66,16 +49,11 @@ module ActionSubscriber
66
49
 
67
50
  def self.print_subscriptions
68
51
  logger.info configuration.inspect
69
- route_set.routes.group_by(&:subscriber).each do |subscriber, routes|
70
- logger.info subscriber.name
71
- routes.each do |route|
72
- logger.info " -- method: #{route.action}"
73
- logger.info " -- exchange: #{route.exchange}"
74
- logger.info " -- queue: #{route.queue}"
75
- logger.info " -- routing_key: #{route.routing_key}"
76
- logger.info " -- threadpool: #{route.threadpool.name}, pool_size: #{route.threadpool.pool_size}"
77
- end
78
- end
52
+ route_set.print_subscriptions
53
+ end
54
+
55
+ def self.print_threadpool_stats
56
+ route_set.print_threadpool_stats
79
57
  end
80
58
 
81
59
  def self.setup_default_connection!
@@ -86,24 +64,14 @@ module ActionSubscriber
86
64
  route_set.setup_subscriptions!
87
65
  end
88
66
 
89
- def self.start_queues
90
- setup_subscriptions!
91
- print_subscriptions
92
- end
93
-
94
- def self.start_subscribers
95
- setup_subscriptions!
96
- auto_subscribe!
97
- print_subscriptions
67
+ def self.start_subscribers!
68
+ route_set.start_subscribers!
98
69
  end
99
70
 
100
- def self.stop_subscribers!
71
+ def self.stop_subscribers!(timeout = nil)
72
+ timeout ||= ::ActionSubscriber.configuration.seconds_to_wait_for_graceful_shutdown
101
73
  route_set.cancel_consumers!
102
- end
103
-
104
- def self.wait_for_threadpools_to_finish_with_timeout(timeout)
105
- puts "waiting for threadpools to empty (maximum wait of #{::ActionSubscriber.configuration.seconds_to_wait_for_graceful_shutdown}sec)"
106
- ::ActionSubscriber::Threadpool.wait_to_finish_with_timeout(timeout)
74
+ puts "waiting for threadpools to empty (maximum wait of #{timeout}sec)"
107
75
  route_set.wait_to_finish_with_timeout(timeout)
108
76
  end
109
77
 
@@ -122,11 +90,4 @@ module ActionSubscriber
122
90
  end
123
91
  end
124
92
  private_class_method :route_set
125
-
126
- def self.default_routes
127
- ::ActionSubscriber::Base.inherited_classes.flat_map do |klass|
128
- klass.routes
129
- end
130
- end
131
- private_class_method :default_routes
132
93
  end
@@ -3,36 +3,12 @@ module ActionSubscriber
3
3
  ##
4
4
  # Class Methods
5
5
  #
6
-
7
- def self.auto_pop!
8
- @pop_mode = true
9
- reload_active_record
10
- ::ActionSubscriber.setup_default_connection!
11
- sleep_time = ::ActionSubscriber.configuration.pop_interval.to_i / 1000.0
12
-
13
- ::ActionSubscriber.start_queues
14
- logger.info "Action Subscriber is popping messages every #{sleep_time} seconds."
15
-
16
- # How often do we want the timer checking for new pops
17
- # since we included an eager popper we decreased the
18
- # default check interval to 100m
19
- while true
20
- ::ActionSubscriber.auto_pop! unless shutting_down?
21
- sleep sleep_time
22
- break if shutting_down?
23
- end
24
- end
25
-
26
- def self.pop?
27
- !!@pop_mode
28
- end
29
-
30
6
  def self.start_subscribers
31
- @prowl_mode = true
32
7
  reload_active_record
33
8
  ::ActionSubscriber.setup_default_connection!
34
-
35
- ::ActionSubscriber.start_subscribers
9
+ ::ActionSubscriber.setup_subscriptions!
10
+ ::ActionSubscriber.print_subscriptions
11
+ ::ActionSubscriber.start_subscribers!
36
12
  logger.info "Action Subscriber connected"
37
13
 
38
14
  while true
@@ -41,10 +17,6 @@ module ActionSubscriber
41
17
  end
42
18
  end
43
19
 
44
- def self.prowl?
45
- !!@prowl_mode
46
- end
47
-
48
20
  def self.logger
49
21
  ::ActionSubscriber::Logging.logger
50
22
  end
@@ -59,21 +31,13 @@ module ActionSubscriber
59
31
  !!@shutting_down
60
32
  end
61
33
 
62
- def self.stop_receving_messages!
63
- @shutting_down = true
64
- ::Thread.new do
65
- ::ActionSubscriber.stop_subscribers!
66
- logger.info "stopped all subscribers"
67
- end.join
68
- end
69
-
70
34
  def self.stop_server!
71
35
  # this method is called from within a TRAP context so we can't use the logger
72
- puts "Stopping server..."
73
- ::ActionSubscriber::Babou.stop_receving_messages!
74
- ::ActionSubscriber.wait_for_threadpools_to_finish_with_timeout(::ActionSubscriber.configuration.seconds_to_wait_for_graceful_shutdown)
75
- puts "Shutting down"
36
+ @shutting_down = true
76
37
  ::Thread.new do
38
+ puts "Stopping subscribers..."
39
+ ::ActionSubscriber.stop_subscribers!
40
+ puts "Shutting down"
77
41
  ::ActionSubscriber::RabbitConnection.subscriber_disconnect!
78
42
  end.join
79
43
  end
@@ -56,76 +56,6 @@ module ActionSubscriber
56
56
  env.acknowledge
57
57
  end
58
58
 
59
- def _at_least_once_filter
60
- processed_acknowledgement = false
61
- yield
62
- processed_acknowledgement = acknowledge
63
- rescue => error
64
- ::ActionSubscriber::MessageRetry.redeliver_message_with_backoff(env)
65
- processed_acknowledgement = acknowledge
66
- raise error
67
- ensure
68
- rejected_message = false
69
- rejected_message = reject unless processed_acknowledgement
70
-
71
- if !processed_acknowledgement && !rejected_message
72
- Process.kill(:TTIN, Process.pid)
73
- Process.kill(:USR2, Process.pid)
74
-
75
- $stdout << <<-UNREJECTABLE
76
- CANNOT ACKNOWLEDGE OR REJECT THE MESSAGE
77
-
78
- This is a exceptional state for ActionSubscriber to enter and puts the current
79
- Process in the position of "I can't get new work from RabbitMQ, but also
80
- can't acknowledge or reject the work that I currently have" ... While rare
81
- this state can happen.
82
-
83
- Instead of continuing to try to process the message ActionSubscriber is
84
- sending a Kill signal to the current running process to gracefully shutdown
85
- so that the RabbitMQ server will purge any outstanding acknowledgements. If
86
- you are running a process monitoring tool (like Upstart) the Subscriber
87
- process will be restarted and be able to take on new work.
88
-
89
- ** Running a process monitoring tool like Upstart is recommended for this reason **
90
- UNREJECTABLE
91
-
92
- Process.kill(:TERM, Process.pid)
93
- end
94
- end
95
-
96
- def _at_most_once_filter
97
- processed_acknowledgement = false
98
- processed_acknowledgement = acknowledge
99
- yield
100
- ensure
101
- rejected_message = false
102
- rejected_message = reject unless processed_acknowledgement
103
-
104
- if !processed_acknowledgement && !rejected_message
105
- Process.kill(:TTIN, Process.pid)
106
- Process.kill(:USR2, Process.pid)
107
-
108
- $stdout << <<-UNREJECTABLE
109
- CANNOT ACKNOWLEDGE OR REJECT THE MESSAGE
110
-
111
- This is a exceptional state for ActionSubscriber to enter and puts the current
112
- Process in the position of "I can't get new work from RabbitMQ, but also
113
- can't acknowledge or reject the work that I currently have" ... While rare
114
- this state can happen.
115
-
116
- Instead of continuing to try to process the message ActionSubscriber is
117
- sending a Kill signal to the current running process to gracefully shutdown
118
- so that the RabbitMQ server will purge any outstanding acknowledgements. If
119
- you are running a process monitoring tool (like Upstart) the Subscriber
120
- process will be restarted and be able to take on new work.
121
-
122
- ** Running a process monitoring tool like Upstart is recommended for this reason **
123
- UNREJECTABLE
124
-
125
- Process.kill(:TERM, Process.pid)
126
- end
127
- end
128
-
129
59
  def reject
130
60
  env.reject
131
61
  end
@@ -11,41 +11,51 @@ module ActionSubscriber
11
11
  bunny_consumers.each(&:cancel)
12
12
  end
13
13
 
14
- def create_queue(channel, queue_name, queue_options)
15
- ::Bunny::Queue.new(channel, queue_name, queue_options)
14
+ def print_subscriptions
15
+ routes.group_by(&:subscriber).each do |subscriber, routes|
16
+ logger.info subscriber.name
17
+ routes.each do |route|
18
+ logger.info " -- method: #{route.action}"
19
+ logger.info " -- connection: #{route.connection_name}"
20
+ logger.info " -- concurrency: #{route.concurrency}"
21
+ logger.info " -- exchange: #{route.exchange}"
22
+ logger.info " -- queue: #{route.queue}"
23
+ logger.info " -- routing_key: #{route.routing_key}"
24
+ logger.info " -- prefetch: #{route.prefetch}"
25
+ logger.error "WARNING having a prefetch lower than your concurrency will prevent your subscriber from fully utilizing its threadpool" if route.prefetch < route.concurrency
26
+ end
27
+ end
16
28
  end
17
29
 
18
- def auto_pop!
19
- # Because threadpools can be large we want to cap the number
20
- # of times we will pop each time we poll the broker
21
- times_to_pop = [::ActionSubscriber::Threadpool.ready_size, ::ActionSubscriber.config.times_to_pop].min
22
- times_to_pop.times do
30
+ def print_threadpool_stats
31
+ logger.info "*DISCLAIMER* the number of running jobs is just a best guess. We don't have a good way to introspect the bunny threadpools so jobs that are sleeping or waiting on IO won't show up as running"
32
+ subscriptions.group_by{|subscription| subscription[:route].subscriber}.each do |subscriber, subscriptions|
33
+ logger.info subscriber.name
23
34
  subscriptions.each do |subscription|
24
35
  route = subscription[:route]
25
- queue = subscription[:queue]
26
- # Handle busy checks on a per threadpool basis
27
- next if route.threadpool.busy?
28
-
29
- delivery_info, properties, encoded_payload = queue.pop(route.queue_subscription_options)
30
- next unless encoded_payload # empty queue
31
- ::ActiveSupport::Notifications.instrument "popped_event.action_subscriber", :payload_size => encoded_payload.bytesize, :queue => queue.name
32
- properties = {
33
- :action => route.action,
34
- :content_type => properties[:content_type],
35
- :delivery_tag => delivery_info.delivery_tag,
36
- :exchange => delivery_info.exchange,
37
- :headers => properties.headers,
38
- :message_id => nil,
39
- :routing_key => delivery_info.routing_key,
40
- :queue => queue.name,
41
- }
42
- env = ::ActionSubscriber::Middleware::Env.new(route.subscriber, encoded_payload, properties)
43
- enqueue_env(route.threadpool, env)
36
+ work_pool = subscription[:queue].channel.work_pool
37
+ running_threads = work_pool.threads.select{|thread| thread.status == "run"}.count
38
+ routes.each do |route|
39
+ logger.info " -- method: #{route.action}"
40
+ logger.info " -- concurrency: #{route.concurrency}"
41
+ logger.info " -- running jobs: #{running_threads}"
42
+ logger.info " -- backlog: #{work_pool.backlog}"
43
+ end
44
44
  end
45
45
  end
46
46
  end
47
47
 
48
- def auto_subscribe!
48
+ def setup_subscriptions!
49
+ fail ::RuntimeError, "you cannot setup queues multiple times, this should only happen once at startup" unless subscriptions.empty?
50
+ routes.each do |route|
51
+ subscriptions << {
52
+ :route => route,
53
+ :queue => setup_queue(route),
54
+ }
55
+ end
56
+ end
57
+
58
+ def start_subscribers!
49
59
  subscriptions.each do |subscription|
50
60
  route = subscription[:route]
51
61
  queue = subscription[:queue]
@@ -84,20 +94,12 @@ module ActionSubscriber
84
94
 
85
95
  private
86
96
 
87
- def enqueue_env(threadpool, env)
88
- logger.info "RECEIVED #{env.message_id} from #{env.queue}"
89
- threadpool.async(env) do |env|
90
- ::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key, :queue => env.queue do
91
- ::ActionSubscriber.config.middleware.call(env)
92
- end
93
- end
94
- end
95
-
96
- def run_env(env)
97
- logger.info "RECEIVED #{env.message_id} from #{env.queue}"
98
- ::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key, :queue => env.queue do
99
- ::ActionSubscriber.config.middleware.call(env)
100
- end
97
+ def setup_queue(route)
98
+ channel = ::ActionSubscriber::RabbitConnection.with_connection(route.connection_name){ |connection| connection.create_channel(nil, route.concurrency) }
99
+ exchange = channel.topic(route.exchange)
100
+ queue = channel.queue(route.queue, :durable => route.durable)
101
+ queue.bind(exchange, :routing_key => route.routing_key)
102
+ queue
101
103
  end
102
104
  end
103
105
  end