action_subscriber 3.0.2-java → 4.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,16 +10,13 @@ module ActionSubscriber
10
10
  :heartbeat,
11
11
  :host,
12
12
  :hosts,
13
- :mode,
14
13
  :password,
15
- :pop_interval,
16
14
  :port,
17
15
  :prefetch,
18
16
  :seconds_to_wait_for_graceful_shutdown,
19
17
  :username,
20
18
  :threadpool_size,
21
19
  :timeout,
22
- :times_to_pop,
23
20
  :virtual_host
24
21
 
25
22
  CONFIGURATION_MUTEX = ::Mutex.new
@@ -30,14 +27,11 @@ module ActionSubscriber
30
27
  :heartbeat => 5,
31
28
  :host => 'localhost',
32
29
  :hosts => [],
33
- :mode => 'subscribe',
34
- :pop_interval => 100, # in milliseconds
35
30
  :port => 5672,
36
31
  :prefetch => 2,
37
32
  :seconds_to_wait_for_graceful_shutdown => 30,
38
33
  :threadpool_size => 8,
39
34
  :timeout => 1,
40
- :times_to_pop => 8,
41
35
  :username => "guest",
42
36
  :password => "guest",
43
37
  :virtual_host => "/"
@@ -115,7 +109,7 @@ module ActionSubscriber
115
109
 
116
110
  def inspect
117
111
  inspection_string = <<-INSPECT.strip_heredoc
118
- Rabbit Host: #{host}
112
+ Rabbit Hosts: #{hosts}
119
113
  Rabbit Port: #{port}
120
114
  Threadpool Size: #{threadpool_size}
121
115
  Low Priority Subscriber: #{allow_low_priority_methods}
@@ -2,12 +2,20 @@ module ActionSubscriber
2
2
  module DSL
3
3
  def at_least_once!
4
4
  @_acknowledge_messages = true
5
- around_filter :_at_least_once_filter
5
+ @_at_least_once = true
6
+ end
7
+
8
+ def at_least_once?
9
+ !!@_at_least_once
6
10
  end
7
11
 
8
12
  def at_most_once!
9
13
  @_acknowledge_messages = true
10
- around_filter :_at_most_once_filter
14
+ @_at_most_once = true
15
+ end
16
+
17
+ def at_most_once?
18
+ !!@_at_most_once
11
19
  end
12
20
 
13
21
  def acknowledge_messages?
@@ -38,6 +46,11 @@ module ActionSubscriber
38
46
 
39
47
  def manual_acknowledgement!
40
48
  @_acknowledge_messages = true
49
+ @_manual_acknowedgement = true
50
+ end
51
+
52
+ def manual_acknowledgement?
53
+ !!@_manual_acknowedgement
41
54
  end
42
55
 
43
56
  def no_acknowledgement!
@@ -76,7 +89,7 @@ module ActionSubscriber
76
89
  @_routing_key_names ||= {}
77
90
  end
78
91
 
79
- def run_action_with_filters(env, action)
92
+ def _run_action_with_filters(env, action)
80
93
  subscriber_instance = self.new(env)
81
94
  final_block = Proc.new { subscriber_instance.public_send(action) }
82
95
 
@@ -85,5 +98,84 @@ module ActionSubscriber
85
98
  end
86
99
  first_proc.call
87
100
  end
101
+
102
+ def _run_action_at_most_once_with_filters(env, action)
103
+ processed_acknowledgement = false
104
+ rejected_message = false
105
+ processed_acknowledgement = env.acknowledge
106
+
107
+ _run_action_with_filters(env, action)
108
+ ensure
109
+ rejected_message = env.reject if !processed_acknowledgement
110
+
111
+ if !rejected_message && !processed_acknowledgement
112
+ $stdout << <<-UNREJECTABLE
113
+ CANNOT ACKNOWLEDGE OR REJECT THE MESSAGE
114
+
115
+ This is a exceptional state for ActionSubscriber to enter and puts the current
116
+ Process in the position of "I can't get new work from RabbitMQ, but also
117
+ can't acknowledge or reject the work that I currently have" ... While rare
118
+ this state can happen.
119
+
120
+ Instead of continuing to try to process the message ActionSubscriber is
121
+ sending a Kill signal to the current running process to gracefully shutdown
122
+ so that the RabbitMQ server will purge any outstanding acknowledgements. If
123
+ you are running a process monitoring tool (like Upstart) the Subscriber
124
+ process will be restarted and be able to take on new work.
125
+
126
+ ** Running a process monitoring tool like Upstart is recommended for this reason **
127
+ UNREJECTABLE
128
+
129
+ Process.kill(:TERM, Process.pid)
130
+ end
131
+ end
132
+
133
+ def _run_action_at_least_once_with_filters(env, action)
134
+ processed_acknowledgement = false
135
+ rejected_message = false
136
+
137
+ _run_action_with_filters(env, action)
138
+
139
+ processed_acknowledgement = env.acknowledge
140
+ rescue
141
+ ::ActionSubscriber::MessageRetry.redeliver_message_with_backoff(env)
142
+ processed_acknowledgement = env.acknowledge
143
+
144
+ raise
145
+ ensure
146
+ rejected_message = env.reject if !processed_acknowledgement
147
+
148
+ if !rejected_message && !processed_acknowledgement
149
+ $stdout << <<-UNREJECTABLE
150
+ CANNOT ACKNOWLEDGE OR REJECT THE MESSAGE
151
+
152
+ This is a exceptional state for ActionSubscriber to enter and puts the current
153
+ Process in the position of "I can't get new work from RabbitMQ, but also
154
+ can't acknowledge or reject the work that I currently have" ... While rare
155
+ this state can happen.
156
+
157
+ Instead of continuing to try to process the message ActionSubscriber is
158
+ sending a Kill signal to the current running process to gracefully shutdown
159
+ so that the RabbitMQ server will purge any outstanding acknowledgements. If
160
+ you are running a process monitoring tool (like Upstart) the Subscriber
161
+ process will be restarted and be able to take on new work.
162
+
163
+ ** Running a process monitoring tool like Upstart is recommended for this reason **
164
+ UNREJECTABLE
165
+
166
+ Process.kill(:TERM, Process.pid)
167
+ end
168
+ end
169
+
170
+ def run_action_with_filters(env, action)
171
+ case
172
+ when at_least_once?
173
+ _run_action_at_least_once_with_filters(env, action)
174
+ when at_most_once?
175
+ _run_action_at_most_once_with_filters(env, action)
176
+ else
177
+ _run_action_with_filters(env, action)
178
+ end
179
+ end
88
180
  end
89
181
  end
@@ -7,46 +7,48 @@ module ActionSubscriber
7
7
  march_hare_consumers.each(&:cancel)
8
8
  end
9
9
 
10
- def create_queue(channel, queue_name, queue_options)
11
- queue = ::MarchHare::Queue.new(channel, queue_name, queue_options)
12
- queue.declare!
13
- queue
10
+ def march_hare_consumers
11
+ @march_hare_consumers ||= []
14
12
  end
15
13
 
16
- def auto_pop!
17
- # Because threadpools can be large we want to cap the number
18
- # of times we will pop each time we poll the broker
19
- times_to_pop = [::ActionSubscriber::Threadpool.ready_size, ::ActionSubscriber.config.times_to_pop].min
20
- times_to_pop.times do
21
- subscriptions.each do |subscription|
22
- route = subscription[:route]
23
- queue = subscription[:queue]
24
- # Handle busy checks on a per threadpool basis
25
- next if route.threadpool.busy?
14
+ def print_subscriptions
15
+ routes.group_by(&:subscriber).each do |subscriber, routes|
16
+ logger.info subscriber.name
17
+ routes.each do |route|
18
+ executor = ::ActionSubscriber::RabbitConnection.connection_threadpools[route.connection_name]
19
+ logger.info " -- method: #{route.action}"
20
+ logger.info " -- connection: #{route.connection_name} (#{executor.get_maximum_pool_size} threads)"
21
+ logger.info " -- concurrency: #{route.concurrency}"
22
+ logger.info " -- exchange: #{route.exchange}"
23
+ logger.info " -- queue: #{route.queue}"
24
+ logger.info " -- routing_key: #{route.routing_key}"
25
+ logger.info " -- prefetch: #{route.prefetch} per consumer (#{route.prefetch * route.concurrency} total)"
26
+ end
27
+ end
28
+ end
26
29
 
27
- metadata, encoded_payload = queue.pop(route.queue_subscription_options)
28
- next unless encoded_payload
29
- ::ActiveSupport::Notifications.instrument "popped_event.action_subscriber", :payload_size => encoded_payload.bytesize, :queue => queue.name
30
- properties = {
31
- :action => route.action,
32
- :content_type => metadata.content_type,
33
- :delivery_tag => metadata.delivery_tag,
34
- :exchange => metadata.exchange,
35
- :headers => _normalized_headers(metadata),
36
- :message_id => metadata.message_id,
37
- :routing_key => metadata.routing_key,
38
- :queue => queue.name,
30
+ def print_threadpool_stats
31
+ ::ActionSubscriber::RabbitConnection.connection_threadpools.each do |name, executor|
32
+ logger.info "Connection #{name}"
33
+ logger.info " -- available threads: #{executor.get_maximum_pool_size}"
34
+ logger.info " -- running thread: #{executor.get_active_count}"
35
+ logger.info " -- backlog: #{executor.get_queue.size}"
36
+ end
37
+ end
38
+
39
+ def setup_subscriptions!
40
+ fail ::RuntimeError, "you cannot setup queues multiple times, this should only happen once at startup" unless subscriptions.empty?
41
+ routes.each do |route|
42
+ route.concurrency.times do
43
+ subscriptions << {
44
+ :route => route,
45
+ :queue => setup_queue(route),
39
46
  }
40
- env = ::ActionSubscriber::Middleware::Env.new(route.subscriber, encoded_payload, properties)
41
- enqueue_env(route.threadpool, env)
42
47
  end
43
48
  end
44
-
45
- rescue ::MarchHare::ChannelAlreadyClosed => e
46
- # The connection has gone down, we can just try again on the next pop
47
49
  end
48
50
 
49
- def auto_subscribe!
51
+ def start_subscribers!
50
52
  subscriptions.each do |subscription|
51
53
  route = subscription[:route]
52
54
  queue = subscription[:queue]
@@ -72,10 +74,6 @@ module ActionSubscriber
72
74
  end
73
75
  end
74
76
 
75
- def march_hare_consumers
76
- @march_hare_consumers ||= []
77
- end
78
-
79
77
  def wait_to_finish_with_timeout(timeout)
80
78
  wait_loops = 0
81
79
  loop do
@@ -83,7 +81,7 @@ module ActionSubscriber
83
81
  any_threadpools_busy = false
84
82
  ::ActionSubscriber::RabbitConnection.connection_threadpools.each do |name, executor|
85
83
  next if executor.get_active_count <= 0
86
- puts " -- Connection #{name} (remaining: #{executor.get_active_count})"
84
+ puts " -- Connection #{name} (active: #{executor.get_active_count}, queued: #{executor.get_queue.size})"
87
85
  any_threadpools_busy = true
88
86
  end
89
87
  if !any_threadpools_busy
@@ -98,20 +96,12 @@ module ActionSubscriber
98
96
 
99
97
  private
100
98
 
101
- def enqueue_env(threadpool, env)
102
- logger.info "RECEIVED #{env.message_id} from #{env.queue}"
103
- threadpool.async(env) do |env|
104
- ::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key, :queue => env.queue do
105
- ::ActionSubscriber.config.middleware.call(env)
106
- end
107
- end
108
- end
109
-
110
- def run_env(env)
111
- logger.info "RECEIVED #{env.message_id} from #{env.queue}"
112
- ::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key, :queue => env.queue do
113
- ::ActionSubscriber.config.middleware.call(env)
114
- end
99
+ def setup_queue(route)
100
+ channel = ::ActionSubscriber::RabbitConnection.with_connection(route.connection_name){ |connection| connection.create_channel }
101
+ exchange = channel.topic(route.exchange)
102
+ queue = channel.queue(route.queue, :durable => route.durable)
103
+ queue.bind(exchange, :routing_key => route.routing_key)
104
+ queue
115
105
  end
116
106
 
117
107
  def _normalized_headers(metadata)
@@ -23,7 +23,6 @@ module ActionSubscriber
23
23
  @queue = attributes.fetch(:queue)
24
24
  @routing_key = attributes.fetch(:routing_key)
25
25
  @subscriber = attributes.fetch(:subscriber)
26
- @threadpool = attributes.fetch(:threadpool) { ::ActionSubscriber::Threadpool.pool(:default) }
27
26
  end
28
27
 
29
28
  def acknowledgements?
@@ -12,31 +12,17 @@ module ActionSubscriber
12
12
  @routes = routes
13
13
  end
14
14
 
15
- def setup_subscriptions!
16
- fail ::RuntimeError, "you cannot setup queues multiple times, this should only happen once at startup" unless subscriptions.empty?
17
- routes.each do |route|
18
- route.concurrency.times do
19
- subscriptions << {
20
- :route => route,
21
- :queue => setup_queue(route),
22
- }
23
- end
24
- end
25
- end
26
-
27
15
  private
28
16
 
29
17
  def subscriptions
30
18
  @subscriptions ||= []
31
19
  end
32
20
 
33
- def setup_queue(route)
34
- channel = ::ActionSubscriber::RabbitConnection.with_connection(route.connection_name){ |connection| connection.create_channel }
35
- exchange = channel.topic(route.exchange)
36
- # TODO go to back to the old way of creating a queue?
37
- queue = create_queue(channel, route.queue, :durable => route.durable)
38
- queue.bind(exchange, :routing_key => route.routing_key)
39
- queue
21
+ def run_env(env)
22
+ logger.info "RECEIVED #{env.message_id} from #{env.queue}"
23
+ ::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key, :queue => env.queue do
24
+ ::ActionSubscriber.config.middleware.call(env)
25
+ end
40
26
  end
41
27
  end
42
28
  end
@@ -1,3 +1,3 @@
1
1
  module ActionSubscriber
2
- VERSION = "3.0.2"
2
+ VERSION = "4.0.0"
3
3
  end
@@ -31,7 +31,7 @@ describe "subscriber filters", :integration => true do
31
31
 
32
32
  it "runs multiple around filters" do
33
33
  $messages = [] #testing the order of things
34
- ::ActionSubscriber.auto_subscribe!
34
+ ::ActionSubscriber.start_subscribers!
35
35
  ::ActivePublisher.publish("insta.first", "hEY Guyz!", "events")
36
36
 
37
37
  verify_expectation_within(1.0) do
@@ -16,7 +16,7 @@ describe "at_least_once! mode", :integration => true do
16
16
  let(:subscriber) { GorbyPuffSubscriber }
17
17
 
18
18
  it "retries a failed job until it succeeds" do
19
- ::ActionSubscriber.auto_subscribe!
19
+ ::ActionSubscriber.start_subscribers!
20
20
  ::ActivePublisher.publish("gorby_puff.grumpy", "GrumpFace", "events")
21
21
 
22
22
  verify_expectation_within(2.0) do
@@ -7,20 +7,56 @@ class PokemonSubscriber < ActionSubscriber::Base
7
7
  end
8
8
  end
9
9
 
10
+ class PokemonWithAroundSubscriber < ActionSubscriber::Base
11
+ around_filter :catch_you_first
12
+ at_most_once!
13
+
14
+ def caught_em_all
15
+ raise RuntimeError.new("what do I do now?")
16
+ end
17
+
18
+ private
19
+
20
+ def catch_you_first
21
+ $messages << "DONE::#{$messages.size}"
22
+ raise RuntimeError.new("caught you first")
23
+ end
24
+ end
25
+
10
26
  describe "at_most_once! mode", :integration => true do
11
- let(:draw_routes) do
12
- ::ActionSubscriber.draw_routes do
13
- default_routes_for PokemonSubscriber
27
+ context "without overriding around_filter" do
28
+ let(:draw_routes) do
29
+ ::ActionSubscriber.draw_routes do
30
+ default_routes_for PokemonSubscriber
31
+ end
32
+ end
33
+ let(:subscriber) { PokemonSubscriber }
34
+
35
+ it "does not retry a failed message" do
36
+ ::ActionSubscriber.start_subscribers!
37
+ ::ActivePublisher.publish("pokemon.caught_em_all", "All Pokemon have been caught", "events")
38
+
39
+ verify_expectation_within(1.0) do
40
+ expect($messages.size).to eq 1
41
+ end
14
42
  end
15
43
  end
16
- let(:subscriber) { PokemonSubscriber }
17
44
 
18
- it "does not retry a failed message" do
19
- ::ActionSubscriber.auto_subscribe!
20
- ::ActivePublisher.publish("pokemon.caught_em_all", "All Pokemon have been caught", "events")
45
+ context "with overriding around_filter" do
46
+ let(:draw_routes) do
47
+ ::ActionSubscriber.draw_routes do
48
+ default_routes_for PokemonWithAroundSubscriber
49
+ end
50
+ end
51
+ let(:subscriber) { PokemonWithAroundSubscriber }
52
+
53
+ it "does not retry a failed message" do
54
+ ::ActionSubscriber.start_subscribers!
55
+ ::ActivePublisher.publish("pokemon_with_around.caught_em_all", "All Pokemon have been caught", "events")
21
56
 
22
- verify_expectation_within(1.0) do
23
- expect($messages.size).to eq 1
57
+ verify_expectation_within(1.0) do
58
+ expect($messages.size).to eq 1
59
+ end
24
60
  end
25
61
  end
26
62
  end
@@ -7,36 +7,36 @@ class GusSubscriber < ActionSubscriber::Base
7
7
  end
8
8
 
9
9
  describe "Automatically reconnect on connection failure", :integration => true, :slow => true do
10
- let(:draw_routes) do
11
- ::ActionSubscriber.draw_routes do
12
- default_routes_for GusSubscriber
13
- end
14
- end
15
- let(:http_client) { RabbitMQ::HTTP::Client.new("http://127.0.0.1:15672") }
16
- let(:subscriber) { GusSubscriber }
10
+ let(:draw_routes) do
11
+ ::ActionSubscriber.draw_routes do
12
+ default_routes_for GusSubscriber
13
+ end
14
+ end
15
+ let(:http_client) { RabbitMQ::HTTP::Client.new("http://127.0.0.1:15672") }
16
+ let(:subscriber) { GusSubscriber }
17
17
 
18
- it "reconnects when a connection drops" do
19
- ::ActionSubscriber::auto_subscribe!
20
- ::ActivePublisher.publish("gus.spoke", "First", "events")
21
- verify_expectation_within(5.0) do
22
- expect($messages).to eq(Set.new(["First"]))
23
- end
18
+ it "reconnects when a connection drops" do
19
+ ::ActionSubscriber::start_subscribers!
20
+ ::ActivePublisher.publish("gus.spoke", "First", "events")
21
+ verify_expectation_within(5.0) do
22
+ expect($messages).to eq(Set.new(["First"]))
23
+ end
24
24
 
25
- close_all_connections!
26
- sleep 5.0
27
- verify_expectation_within(5.0) do
28
- expect(::ActionSubscriber::RabbitConnection.with_connection(:default){|connection| connection.open?}).to eq(true)
29
- end
25
+ close_all_connections!
26
+ sleep 5.0
27
+ verify_expectation_within(5.0) do
28
+ expect(::ActionSubscriber::RabbitConnection.with_connection(:default){|connection| connection.open?}).to eq(true)
29
+ end
30
30
 
31
- ::ActivePublisher.publish("gus.spoke", "Second", "events")
32
- verify_expectation_within(5.0) do
33
- expect($messages).to eq(Set.new(["First", "Second"]))
34
- end
35
- end
31
+ ::ActivePublisher.publish("gus.spoke", "Second", "events")
32
+ verify_expectation_within(5.0) do
33
+ expect($messages).to eq(Set.new(["First", "Second"]))
34
+ end
35
+ end
36
36
 
37
- def close_all_connections!
38
- http_client.list_connections.each do |conn_info|
39
- http_client.close_connection(conn_info.name)
40
- end
41
- end
37
+ def close_all_connections!
38
+ http_client.list_connections.each do |conn_info|
39
+ http_client.close_connection(conn_info.name)
40
+ end
41
+ end
42
42
  end