action_subscriber 5.0.3 → 5.1.0.pre

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: 605d2e8c1c46efca8695b90fdefca1b88342bc62
4
- data.tar.gz: e6a8b84e0fa197c5e085251964f75a362b7d324b
3
+ metadata.gz: aa70a7b7308a903e30cb492dc4c334cc76180984
4
+ data.tar.gz: 552c9aaae558ec3fae519c9fcde87dffbc8a6887
5
5
  SHA512:
6
- metadata.gz: d138a4b1709e7d0450b8a3c2cf85a9348ed4e4c8acbfbef61f69a9ea25beeba0fc88924ba46cc286a26ad527877d3b51a2b18dc0b5d3930033f3c1aedce5954a
7
- data.tar.gz: 566f397bcdbfc28242451be5c6551dc6a013f8e1b7e36aac271ea1afe07ebfbd3ae14d5425f15af2a84645b6d2828a0e33830192f9b1dfd250c30e4823f303fa
6
+ metadata.gz: e2b5a313624613ae7de8e72dee4cd48b880effecfd7ffcd4472e4d79c4bf4e960e7db09b7574f6a285da8995494fc6618e7b59c1d43865b0fac68df7920ef867
7
+ data.tar.gz: f3af3c8278b4d886394560bdf404d5487a311635c2232b6e8ae3159cc0f3255c96ff14f60e2ba868c8697500e38cd0eb8e860aa7f9a39a6d832f8d9faba592fd
data/README.md CHANGED
@@ -160,7 +160,7 @@ This also allows the broker to send messages as quickly as it wants down to your
160
160
 
161
161
  ### manual_acknowledgement!
162
162
 
163
- This mode leaves it up to the subscriber to handle acknowledging or rejecting messages. In your subscriber you can just call <code>acknowledge</code> or <code>reject</code>.
163
+ This mode leaves it up to the subscriber to handle acknowledging or rejecting messages. In your subscriber you can just call <code>acknowledge</code>, <code>reject</code>, or <code>nack</code>.
164
164
 
165
165
  ### at_most_once!
166
166
 
@@ -172,6 +172,11 @@ Rabbit is told to expect message acknowledgements, but sending the acknowledgeme
172
172
  We send the acknowledgement right after calling your subscriber.
173
173
  If an error is raised your message will be retried on a sent back to rabbitmq and retried on an exponential backoff schedule.
174
174
 
175
+ ### safe_nack
176
+ If you turn on acknowledgements and a message is not acknowledged by your code manually or using one of the filters above the `ErrorHandler` middleware
177
+ which wraps the entire block with call <code>nack</code> this is a last resort so the connection does not get backed up in cases of unexpected or
178
+ unhandled errors.
179
+
175
180
  ### redeliver
176
181
 
177
182
  A message can be sent to "redeliver" with `::ActionSubscriber::MessageRetry.redeliver_message_with_backoff` or the DSL method `redeliver` and optionally
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency "active_publisher", "~> 0.1.5"
34
34
  spec.add_development_dependency "activerecord", ">= 3.2"
35
35
  spec.add_development_dependency "bundler", ">= 1.6"
36
+ spec.add_development_dependency "pry-coolline"
36
37
  spec.add_development_dependency "pry-nav"
37
38
  spec.add_development_dependency "rabbitmq_http_api_client", "~> 1.2.0"
38
39
  spec.add_development_dependency "rspec", "~> 3.0"
@@ -44,6 +44,7 @@ module ActionSubscriber
44
44
  :message_id => properties.message_id,
45
45
  :routing_key => delivery_info.routing_key,
46
46
  :queue => queue.name,
47
+ :uses_acknowledgements => route.acknowledgements?,
47
48
  }
48
49
  env = ::ActionSubscriber::Middleware::Env.new(route.subscriber, encoded_payload, properties)
49
50
  run_env(env, threadpool)
@@ -42,6 +42,7 @@ module ActionSubscriber
42
42
  :message_id => metadata.message_id,
43
43
  :routing_key => metadata.routing_key,
44
44
  :queue => queue.name,
45
+ :uses_acknowledgements => route.acknowledgements?,
45
46
  }
46
47
  env = ::ActionSubscriber::Middleware::Env.new(route.subscriber, encoded_payload, properties)
47
48
  run_env(env, threadpool)
@@ -38,7 +38,7 @@ module ActionSubscriber
38
38
  end
39
39
 
40
40
  def call(env)
41
- def call(env)
41
+ def call(env) # redefines so it only gets called once
42
42
  ::ActiveRecord::Base.connection_pool.with_connection do
43
43
  @app.call(env)
44
44
  end
@@ -18,7 +18,7 @@ module ActionSubscriber
18
18
  private
19
19
 
20
20
  def decoder
21
- ActionSubscriber.config.decoder[env.content_type]
21
+ ::ActionSubscriber.config.decoder[env.content_type]
22
22
  end
23
23
 
24
24
  def decoder?
@@ -33,27 +33,54 @@ module ActionSubscriber
33
33
  @delivery_tag = properties.fetch(:delivery_tag)
34
34
  @encoded_payload = encoded_payload
35
35
  @exchange = properties.fetch(:exchange)
36
- @headers = properties.fetch(:headers) || {}
37
- @message_id = properties.fetch(:message_id) || ::SecureRandom.hex(3)
36
+ @has_been_acked = false
37
+ @has_been_nacked = false
38
+ @has_been_rejected = false
39
+ @headers = properties.fetch(:headers, {})
40
+ @message_id = properties.fetch(:message_id, ::SecureRandom.hex(3))
38
41
  @queue = properties.fetch(:queue)
39
42
  @routing_key = properties.fetch(:routing_key)
40
43
  @subscriber = subscriber
44
+ @uses_acknowledgements = properties.fetch(:uses_acknowledgements, false)
41
45
  end
42
46
 
43
47
  def acknowledge
44
48
  fail ::RuntimeError, "you can't acknowledge messages under the polling API" unless @channel
45
49
  acknowledge_multiple_messages = false
50
+ @has_been_acked = true
46
51
  @channel.ack(@delivery_tag, acknowledge_multiple_messages)
47
52
  true
48
53
  end
49
54
 
55
+ def nack
56
+ fail ::RuntimeError, "you can't acknowledge messages under the polling API" unless @channel
57
+ nack_multiple_messages = false
58
+ requeue_message = true
59
+ @has_been_nacked = true
60
+ @channel.nack(@delivery_tag, nack_multiple_messages, requeue_message)
61
+ true
62
+ end
63
+
50
64
  def reject
51
65
  fail ::RuntimeError, "you can't acknowledge messages under the polling API" unless @channel
52
66
  requeue_message = true
67
+ @has_been_rejected = true
53
68
  @channel.reject(@delivery_tag, requeue_message)
54
69
  true
55
70
  end
56
71
 
72
+ def safe_acknowledge
73
+ acknowledge if uses_acknowledgements? && @channel && !has_used_delivery_tag?
74
+ end
75
+
76
+ def safe_nack
77
+ nack if uses_acknowledgements? && @channel && !has_used_delivery_tag?
78
+ end
79
+
80
+ def safe_reject
81
+ reject if uses_acknowledgements? && @channel && !has_used_delivery_tag?
82
+ end
83
+
57
84
  def to_hash
58
85
  {
59
86
  :action => action,
@@ -65,6 +92,15 @@ module ActionSubscriber
65
92
  end
66
93
  alias_method :to_h, :to_hash
67
94
 
95
+ private
96
+
97
+ def has_used_delivery_tag?
98
+ @has_been_acked || @has_been_nacked || @has_been_rejected
99
+ end
100
+
101
+ def uses_acknowledgements?
102
+ @uses_acknowledgements
103
+ end
68
104
  end
69
105
  end
70
106
  end
@@ -9,8 +9,8 @@ module ActionSubscriber
9
9
 
10
10
  def call(env)
11
11
  @app.call(env)
12
- rescue => error
13
- logger.error "FAILED #{env.message_id}"
12
+ rescue Exception => error # make sure we capture any exception from the top of the hierarchy
13
+ logger.error { "FAILED #{env.message_id}" }
14
14
 
15
15
  # There is more to this rescue than meets the eye. MarchHare's java library will rescue errors
16
16
  # and attempt to close the channel with its default exception handler. To avoid this, we will
@@ -18,9 +18,11 @@ module ActionSubscriber
18
18
  # it should not re-raise. As a bonus, not killing these threads is better for your runtime :).
19
19
  begin
20
20
  ::ActionSubscriber.configuration.error_handler.call(error, env.to_h)
21
- rescue => error
22
- logger.error "ActionSubscriber error handler raised error, but should never raise. Error: #{error}"
21
+ rescue Exception => inner_error
22
+ logger.error { "ActionSubscriber error handler raised error, but should never raise. Error: #{inner_error}" }
23
23
  end
24
+ ensure
25
+ env.safe_nack # Make sure we attempt to `nack` a message that did not get processed if something fails
24
26
  end
25
27
  end
26
28
  end
@@ -1,6 +1,7 @@
1
1
  module ActionSubscriber
2
2
  module Middleware
3
3
  class Router
4
+ INSTRUMENT_KEY = "process_event.action_subscriber".freeze
4
5
  include ::ActionSubscriber::Logging
5
6
 
6
7
  def initialize(app)
@@ -8,11 +9,27 @@ module ActionSubscriber
8
9
  end
9
10
 
10
11
  def call(env)
11
- logger.info { "START #{env.message_id} #{env.subscriber}##{env.action}" }
12
- ::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key, :queue => env.queue do
13
- env.subscriber.run_action_with_filters(env, env.action)
12
+ action = env.action
13
+ message_id = env.message_id
14
+ queue = env.queue
15
+ routing_key = env.routing_key
16
+ subscriber = env.subscriber
17
+
18
+ logger.info { "START #{message_id} #{subscriber}##{action}" }
19
+
20
+ instrument_call(subscriber, routing_key, queue) do
21
+ subscriber.run_action_with_filters(env, action)
22
+ end
23
+
24
+ logger.info { "FINISHED #{message_id}" }
25
+ end
26
+
27
+ private
28
+
29
+ def instrument_call(subscriber, routing_key, queue)
30
+ ::ActiveSupport::Notifications.instrument INSTRUMENT_KEY, :subscriber => subscriber.to_s, :routing_key => routing_key, :queue => queue do
31
+ yield
14
32
  end
15
- logger.info { "FINISHED #{env.message_id}" }
16
33
  end
17
34
  end
18
35
  end
@@ -1,3 +1,4 @@
1
+ require "action_subscriber/logging"
1
2
  require "action_subscriber/middleware/decoder"
2
3
  require "action_subscriber/middleware/env"
3
4
  require "action_subscriber/middleware/error_handler"
@@ -6,11 +7,26 @@ require "action_subscriber/middleware/runner"
6
7
 
7
8
  module ActionSubscriber
8
9
  module Middleware
10
+
11
+ class Builder < ::Middleware::Builder
12
+ include ::ActionSubscriber::Logging
13
+
14
+ def print_middleware_stack
15
+ logger.info "Middlewares ["
16
+
17
+ stack.each do |middleware|
18
+ logger.info "#{middleware}"
19
+ end
20
+
21
+ logger.info "]"
22
+ end
23
+ end
24
+
9
25
  def self.initialize_stack
10
- builder = ::Middleware::Builder.new(:runner_class => ::ActionSubscriber::Middleware::Runner)
26
+ builder = ::ActionSubscriber::Middleware::Builder.new(:runner_class => ::ActionSubscriber::Middleware::Runner)
11
27
 
12
- builder.use ErrorHandler
13
- builder.use Decoder
28
+ builder.use ::ActionSubscriber::Middleware::ErrorHandler
29
+ builder.use ::ActionSubscriber::Middleware::Decoder
14
30
 
15
31
  builder
16
32
  end
@@ -6,8 +6,8 @@ module ActionSubscriber
6
6
  require "action_subscriber/middleware/active_record/connection_management"
7
7
  require "action_subscriber/middleware/active_record/query_cache"
8
8
 
9
- ::ActionSubscriber.config.middleware.use ::ActionSubscriber::Middleware::ActiveRecord::ConnectionManagement
10
- ::ActionSubscriber.config.middleware.use ::ActionSubscriber::Middleware::ActiveRecord::QueryCache
9
+ ::ActionSubscriber.config.middleware.insert_after ::ActionSubscriber::Middleware::Decoder, ::ActionSubscriber::Middleware::ActiveRecord::ConnectionManagement
10
+ ::ActionSubscriber.config.middleware.insert_after ::ActionSubscriber::Middleware::ActiveRecord::ConnectionManagement, ::ActionSubscriber::Middleware::ActiveRecord::QueryCache
11
11
  end
12
12
  end
13
13
  end
@@ -12,7 +12,12 @@ module ActionSubscriber
12
12
  @routes = routes
13
13
  end
14
14
 
15
+ def print_middleware_stack
16
+ ::ActionSubscriber.config.middleware.print_middleware_stack
17
+ end
18
+
15
19
  def print_subscriptions
20
+ print_middleware_stack
16
21
  routes.group_by(&:subscriber).each do |subscriber, routes|
17
22
  logger.info subscriber.name
18
23
  routes.each do |route|
@@ -39,15 +44,20 @@ module ActionSubscriber
39
44
  end
40
45
 
41
46
  def wait_to_finish_with_timeout(timeout)
47
+ finisher_threads = []
48
+
42
49
  ::ActionSubscriber::ThreadPools.threadpools.map do |name, threadpool|
43
50
  logger.info " -- Threadpool #{name} (queued: #{threadpool.queue_length})"
44
- ::Thread.new do
45
- completed = threadpool.wait_for_termination(timeout)
51
+ finisher_threads << ::Thread.new(threadpool, timeout, name) do |internal_pool, internal_timeout, internal_name|
52
+ completed = internal_pool.wait_for_termination(internal_timeout)
53
+
46
54
  unless completed
47
- logger.error " -- FAILED #{name} did not finish shutting down within #{timeout}sec"
55
+ logger.error " -- FAILED #{internal_name} did not finish shutting down within #{internal_timeout}sec"
48
56
  end
49
57
  end
50
- end.each(&:join)
58
+ end
59
+
60
+ finisher_threads.each(&:join)
51
61
  end
52
62
 
53
63
  private
@@ -7,6 +7,10 @@ module ActionSubscriber
7
7
  true
8
8
  end
9
9
 
10
+ def nack(delivery_tag, acknowledge_multiple, requeue_message)
11
+ true
12
+ end
13
+
10
14
  def reject(delivery_tag, requeue_message)
11
15
  true
12
16
  end
@@ -22,6 +26,7 @@ module ActionSubscriber
22
26
  :message_id => "MSG-123",
23
27
  :routing_key => "amigo.user.created",
24
28
  :queue => "test.amigo.user.created",
29
+ :uses_acknoledgements => false,
25
30
  }.freeze
26
31
 
27
32
  # Create a new subscriber instance. Available options are:
@@ -1,3 +1,3 @@
1
1
  module ActionSubscriber
2
- VERSION = "5.0.3"
2
+ VERSION = "5.1.0.pre"
3
3
  end
@@ -3,8 +3,10 @@ class BaconSubscriber < ActionSubscriber::Base
3
3
 
4
4
  def served
5
5
  $messages << "#{payload}::#{$messages.size}"
6
- if $messages.size > 2
6
+ if $messages.size > 3
7
7
  acknowledge
8
+ elsif $messages.size > 2
9
+ nack
8
10
  else
9
11
  reject
10
12
  end
@@ -20,12 +22,12 @@ describe "Manual Message Acknowledgment", :integration => true do
20
22
  end
21
23
  let(:subscriber) { BaconSubscriber }
22
24
 
23
- it "retries rejected messages and stops retrying acknowledged messages" do
25
+ it "retries rejected/nacked messages and stops retrying acknowledged messages" do
24
26
  ::ActionSubscriber.start_subscribers!
25
27
  ::ActivePublisher.publish("bacon.served", "BACON!", "events")
26
28
 
27
- verify_expectation_within(2.0) do
28
- expect($messages).to eq(Set.new(["BACON!::0", "BACON!::1", "BACON!::2"]))
29
+ verify_expectation_within(2.5) do
30
+ expect($messages).to eq(Set.new(["BACON!::0", "BACON!::1", "BACON!::2", "BACON!::3"]))
29
31
  end
30
32
  end
31
33
  end
@@ -7,14 +7,32 @@ describe ActionSubscriber::Middleware::ErrorHandler do
7
7
 
8
8
  it_behaves_like 'an action subscriber middleware'
9
9
 
10
- let(:error) { ::RuntimeError.new("Boom!") }
10
+ let(:load_error) { ::LoadError.new("Boom!") }
11
+ let(:runtime_error) { ::RuntimeError.new("Boom!") }
11
12
 
12
13
  context "when an exception occurs" do
13
- before { allow(app).to receive(:call).and_raise(error) }
14
+ context "LoadError" do
15
+ before { allow(app).to receive(:call).and_raise(load_error) }
16
+ it "calls the exception handler with a LoadError" do
17
+ handler = ::ActionSubscriber.configuration.error_handler
18
+ expect(handler).to receive(:call).with(load_error, env.to_h)
14
19
 
15
- it "calls the exception handler" do
16
- handler = ::ActionSubscriber.configuration.error_handler
17
- expect(handler).to receive(:call).with(error, env.to_h)
20
+ subject.call(env)
21
+ end
22
+ end
23
+
24
+ context "RuntimError" do
25
+ before { allow(app).to receive(:call).and_raise(runtime_error) }
26
+ it "calls the exception handler with a RuntimeError" do
27
+ handler = ::ActionSubscriber.configuration.error_handler
28
+ expect(handler).to receive(:call).with(runtime_error, env.to_h)
29
+
30
+ subject.call(env)
31
+ end
32
+ end
33
+
34
+ it "calls safe_nack after execution" do
35
+ expect(env).to receive(:safe_nack)
18
36
 
19
37
  subject.call(env)
20
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_subscriber
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.3
4
+ version: 5.1.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Stien
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2017-12-18 00:00:00.000000000 Z
15
+ date: 2018-01-16 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activesupport
@@ -126,6 +126,20 @@ dependencies:
126
126
  - - ">="
127
127
  - !ruby/object:Gem::Version
128
128
  version: '1.6'
129
+ - !ruby/object:Gem::Dependency
130
+ name: pry-coolline
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
129
143
  - !ruby/object:Gem::Dependency
130
144
  name: pry-nav
131
145
  requirement: !ruby/object:Gem::Requirement
@@ -278,12 +292,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
278
292
  version: '0'
279
293
  required_rubygems_version: !ruby/object:Gem::Requirement
280
294
  requirements:
281
- - - ">="
295
+ - - ">"
282
296
  - !ruby/object:Gem::Version
283
- version: '0'
297
+ version: 1.3.1
284
298
  requirements: []
285
299
  rubyforge_project:
286
- rubygems_version: 2.6.13
300
+ rubygems_version: 2.5.1
287
301
  signing_key:
288
302
  specification_version: 4
289
303
  summary: ActionSubscriber is a DSL that allows a rails app to consume messages from