action_subscriber 3.0.2-java → 4.0.0-java

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