action_subscriber 2.0.1 → 2.1.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: fa7ffc8e113cf3414e95bd9ff2f4271716316da3
4
- data.tar.gz: bb9f4b158454c734e75484f8d3fb722ea9817b3d
3
+ metadata.gz: cdfd01dd3dd2055815f165b70bce923d36fe1db7
4
+ data.tar.gz: 261530f967fe4368afa58295344e1fe40ee70746
5
5
  SHA512:
6
- metadata.gz: e3495e8deb06c7ec7540a3cfb0f8080badcbfaa6565e9f1839bdf515c6c557997727e60802f95bca40683db7b5b98d2338eff316575f609c3d2b8616e3503bee
7
- data.tar.gz: 2a34bf987ad18fa9b5d8fd852c8148173dbcc5b0bbf94329cd89ccbead8d66c152df1d3a766b9badfbf5baf5ce988a3c99e5c857cb82dc5f152fe8b2e075e661
6
+ metadata.gz: 109f19c08d6975bca0af2df1ff70483ef8e3211d235e4c4767cba1d386c78085be51d021357ccc76f1d2f86564fa3a43c3500869ba690e970f5cc06a3032ed9e
7
+ data.tar.gz: 3646c5fe3efb28a73d98a3a8aa991dfeac31e71d7946b0549dc7e32610c30cd6914241e6775f867828729e3bb15c6fa15ba48be715a1b1a7bc30352acc6e160e
@@ -1,6 +1,10 @@
1
1
  module ActionSubscriber
2
2
  class Configuration
3
3
  attr_accessor :allow_low_priority_methods,
4
+ :async_publisher,
5
+ :async_publisher_drop_messages_when_queue_full,
6
+ :async_publisher_max_queue_size,
7
+ :async_publisher_supervisor_interval,
4
8
  :decoder,
5
9
  :default_exchange,
6
10
  :error_handler,
@@ -19,6 +23,10 @@ module ActionSubscriber
19
23
 
20
24
  DEFAULTS = {
21
25
  :allow_low_priority_methods => false,
26
+ :async_publisher => 'memory',
27
+ :async_publisher_drop_messages_when_queue_full => false,
28
+ :async_publisher_max_queue_size => 1_000_000,
29
+ :async_publisher_supervisor_interval => 200, # in milliseconds
22
30
  :default_exchange => 'events',
23
31
  :heartbeat => 5,
24
32
  :host => 'localhost',
@@ -0,0 +1,153 @@
1
+ require "thread"
2
+
3
+ module ActionSubscriber
4
+ module Publisher
5
+ module Async
6
+ class InMemoryAdapter
7
+ include ::ActionSubscriber::Logging
8
+
9
+ attr_reader :async_queue
10
+
11
+ def initialize
12
+ logger.info "Starting in-memory publisher adapter."
13
+
14
+ @async_queue = AsyncQueue.new
15
+ end
16
+
17
+ def publish(route, payload, exchange_name, options = {})
18
+ message = Message.new(route, payload, exchange_name, options)
19
+ async_queue.push(message)
20
+ nil
21
+ end
22
+
23
+ def shutdown!
24
+ max_wait_time = ::ActionSubscriber.configuration.seconds_to_wait_for_graceful_shutdown
25
+ started_shutting_down_at = ::Time.now
26
+
27
+ logger.info "Draining async publisher in-memory adapter queue before shutdown. Current queue size: #{async_queue.size}."
28
+ while async_queue.size > 0
29
+ if (::Time.now - started_shutting_down_at) > max_wait_time
30
+ logger.info "Forcing async publisher adapter shutdown because graceful shutdown period of #{max_wait_time} seconds was exceeded. Current queue size: #{async_queue.size}."
31
+ break
32
+ end
33
+
34
+ sleep 0.1
35
+ end
36
+ end
37
+
38
+ class Message
39
+ attr_reader :route, :payload, :exchange_name, :options
40
+
41
+ def initialize(route, payload, exchange_name, options)
42
+ @route = route
43
+ @payload = payload
44
+ @exchange_name = exchange_name
45
+ @options = options
46
+ end
47
+ end
48
+
49
+ class UnableToPersistMessageError < ::StandardError
50
+ end
51
+
52
+ class AsyncQueue
53
+ include ::ActionSubscriber::Logging
54
+
55
+ attr_reader :consumer, :queue, :supervisor
56
+
57
+ if ::RUBY_PLATFORM == "java"
58
+ NETWORK_ERRORS = [::MarchHare::Exception, ::Java::ComRabbitmqClient::AlreadyClosedException, ::Java::JavaIo::IOException].freeze
59
+ else
60
+ NETWORK_ERRORS = [::Bunny::Exception, ::Timeout::Error, ::IOError].freeze
61
+ end
62
+
63
+ def initialize
64
+ @queue = ::Queue.new
65
+ create_and_supervise_consumer!
66
+ end
67
+
68
+ def push(message)
69
+ # Default of 1_000_000 messages.
70
+ if queue.size > ::ActionSubscriber.configuration.async_publisher_max_queue_size
71
+ # Drop Messages if the queue is full and we were configured to do so.
72
+ return if ::ActionSubscriber.configuration.async_publisher_drop_messages_when_queue_full
73
+
74
+ # By default we will raise an error to push the responsibility onto the caller.
75
+ fail UnableToPersistMessageError, "Queue is full, messages will be dropped."
76
+ end
77
+
78
+ queue.push(message)
79
+ end
80
+
81
+ def size
82
+ queue.size
83
+ end
84
+
85
+ private
86
+
87
+ def await_network_reconnect
88
+ sleep ::ActionSubscriber::RabbitConnection::NETWORK_RECOVERY_INTERVAL
89
+ end
90
+
91
+ def create_and_supervise_consumer!
92
+ @consumer = create_consumer
93
+ @supervisor = ::Thread.new do
94
+ loop do
95
+ unless consumer.alive?
96
+ # Why might need to requeue the last message.
97
+ queue.push(@current_message) if @current_message.present?
98
+ consumer.kill
99
+ @consumer = create_consumer
100
+ end
101
+
102
+ # Pause before checking the consumer again.
103
+ sleep supervisor_interval
104
+ end
105
+ end
106
+ end
107
+
108
+ def create_consumer
109
+ ::Thread.new do
110
+ loop do
111
+ # Write "current_message" so we can requeue should something happen to the consumer. I don't love this, but it's
112
+ # better than writing my own `#peek' method.
113
+ @current_message = message = queue.pop
114
+
115
+ begin
116
+ ::ActionSubscriber::Publisher.publish(message.route, message.payload, message.exchange_name, message.options)
117
+
118
+ # Reset
119
+ @current_message = nil
120
+ rescue *NETWORK_ERRORS
121
+ # Sleep because the connection is down.
122
+ await_network_reconnect
123
+
124
+ # Requeue and try again.
125
+ queue.push(message)
126
+ rescue => unknown_error
127
+ # Do not requeue the message because something else horrible happened.
128
+ @current_message = nil
129
+
130
+ # Log the error.
131
+ logger.info unknown_error.class
132
+ logger.info unknown_error.message
133
+ logger.info unknown_error.backtrace.join("\n")
134
+
135
+ # TODO: Find a way to bubble this out of the thread for logging purposes.
136
+ # Reraise the error out of the publisher loop. The Supervisor will restart the consumer.
137
+ raise unknown_error
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ def supervisor_interval
144
+ @supervisor_interval ||= begin
145
+ interval_in_milliseconds = ::ActionSubscriber.configuration.async_publisher_supervisor_interval
146
+ interval_in_milliseconds / 1000.0
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,31 @@
1
+ module ActionSubscriber
2
+ module Publisher
3
+ # Publish a message asynchronously to RabbitMQ.
4
+ #
5
+ # Asynchronous is designed to do two things:
6
+ # 1. Introduce the idea of a durable retry should the RabbitMQ connection disconnect.
7
+ # 2. Provide a higher-level pattern for fire-and-forget publishing.
8
+ #
9
+ # @param [String] route The routing key to use for this message.
10
+ # @param [String] payload The message you are sending. Should already be encoded as a string.
11
+ # @param [String] exchange The exchange you want to publish to.
12
+ # @param [Hash] options hash to set message parameters (e.g. headers).
13
+ def self.publish_async(route, payload, exchange_name, options = {})
14
+ Async.publisher_adapter.publish(route, payload, exchange_name, options)
15
+ end
16
+
17
+ module Async
18
+ def self.publisher_adapter
19
+ @publisher_adapter ||= case ::ActionSubscriber.configuration.async_publisher
20
+ when /memory/i then
21
+ require "action_subscriber/publisher/async/in_memory_adapter"
22
+ InMemoryAdapter.new
23
+ when /redis/i then
24
+ fail "Not yet implemented"
25
+ else
26
+ fail "Unknown adapter '#{::ActionSubscriber.configuration.async_publisher}' provided"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,6 +4,7 @@ module ActionSubscriber
4
4
  module RabbitConnection
5
5
  SUBSCRIBER_CONNECTION_MUTEX = ::Mutex.new
6
6
  PUBLISHER_CONNECTION_MUTEX = ::Mutex.new
7
+ NETWORK_RECOVERY_INTERVAL = 1.freeze
7
8
 
8
9
  def self.publisher_connected?
9
10
  publisher_connection.try(:connected?)
@@ -66,7 +67,7 @@ module ActionSubscriber
66
67
  :port => ::ActionSubscriber.configuration.port,
67
68
  :continuation_timeout => ::ActionSubscriber.configuration.timeout * 1_000.0, #convert sec to ms
68
69
  :automatically_recover => true,
69
- :network_recovery_interval => 1,
70
+ :network_recovery_interval => NETWORK_RECOVERY_INTERVAL,
70
71
  :recover_from_connection_close => true,
71
72
  }
72
73
  end
@@ -1,3 +1,3 @@
1
1
  module ActionSubscriber
2
- VERSION = "2.0.1"
2
+ VERSION = "2.1.0.pre1"
3
3
  end
@@ -23,6 +23,7 @@ require "action_subscriber/bunny/subscriber"
23
23
  require "action_subscriber/march_hare/subscriber"
24
24
  require "action_subscriber/babou"
25
25
  require "action_subscriber/publisher"
26
+ require "action_subscriber/publisher/async"
26
27
  require "action_subscriber/route"
27
28
  require "action_subscriber/route_set"
28
29
  require "action_subscriber/router"
@@ -110,6 +111,9 @@ module ActionSubscriber
110
111
  # Initialize config object
111
112
  config
112
113
 
114
+ # Intialize async publisher adapter
115
+ ::ActionSubscriber::Publisher::Async.publisher_adapter
116
+
113
117
  ::ActiveSupport.run_load_hooks(:action_subscriber, Base)
114
118
 
115
119
  ##
@@ -135,5 +139,6 @@ end
135
139
  require "action_subscriber/railtie" if defined?(Rails)
136
140
 
137
141
  at_exit do
142
+ ::ActionSubscriber::Publisher::Async.publisher_adapter.shutdown!
138
143
  ::ActionSubscriber::RabbitConnection.publisher_disconnect!
139
144
  end
@@ -1,7 +1,10 @@
1
1
  describe ::ActionSubscriber::Configuration do
2
2
  describe "default values" do
3
3
  specify { expect(subject.allow_low_priority_methods).to eq(false) }
4
- specify { expect(subject.default_exchange).to eq("events") }
4
+ specify { expect(subject.async_publisher).to eq("memory") }
5
+ specify { expect(subject.async_publisher_drop_messages_when_queue_full).to eq(false) }
6
+ specify { expect(subject.async_publisher_max_queue_size).to eq(1_000_000) }
7
+ specify { expect(subject.async_publisher_supervisor_interval).to eq(200) }
5
8
  specify { expect(subject.heartbeat).to eq(5) }
6
9
  specify { expect(subject.host).to eq("localhost") }
7
10
  specify { expect(subject.mode).to eq('subscribe') }
@@ -0,0 +1,135 @@
1
+ describe ::ActionSubscriber::Publisher::Async::InMemoryAdapter do
2
+ let(:route) { "test" }
3
+ let(:payload) { "message" }
4
+ let(:exchange_name) { "place" }
5
+ let(:options) { { :test => :ok } }
6
+ let(:message) { described_class::Message.new(route, payload, exchange_name, options) }
7
+ let(:mock_queue) { double(:push => nil, :size => 0) }
8
+
9
+ describe "#publish" do
10
+ before do
11
+ allow(described_class::Message).to receive(:new).with(route, payload, exchange_name, options).and_return(message)
12
+ allow(described_class::AsyncQueue).to receive(:new).and_return(mock_queue)
13
+ end
14
+
15
+ it "can publish a message to the queue" do
16
+ expect(mock_queue).to receive(:push).with(message)
17
+ subject.publish(route, payload, exchange_name, options)
18
+ end
19
+ end
20
+
21
+ describe "#shutdown!" do
22
+ # This is called when the rspec finishes. I'm sure we can make this a better test.
23
+ end
24
+
25
+ describe "::ActionSubscriber::Publisher::Async::InMemoryAdapter::Message" do
26
+ specify { expect(message.route).to eq(route) }
27
+ specify { expect(message.payload).to eq(payload) }
28
+ specify { expect(message.exchange_name).to eq(exchange_name) }
29
+ specify { expect(message.options).to eq(options) }
30
+ end
31
+
32
+ describe "::ActionSubscriber::Publisher::Async::InMemoryAdapter::AsyncQueue" do
33
+ subject { described_class::AsyncQueue.new }
34
+
35
+ describe ".initialize" do
36
+ it "creates a supervisor" do
37
+ expect_any_instance_of(described_class::AsyncQueue).to receive(:create_and_supervise_consumer!)
38
+ subject
39
+ end
40
+ end
41
+
42
+ describe "#create_and_supervise_consumer!" do
43
+ it "creates a supervisor" do
44
+ expect_any_instance_of(described_class::AsyncQueue).to receive(:create_consumer)
45
+ subject
46
+ end
47
+
48
+ it "restarts the consumer when it dies" do
49
+ consumer = subject.consumer
50
+ consumer.kill
51
+
52
+ verify_expectation_within(0.1) do
53
+ expect(consumer).to_not be_alive
54
+ end
55
+
56
+ verify_expectation_within(0.3) do
57
+ expect(subject.consumer).to be_alive
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#create_consumer" do
63
+ it "can successfully publish a message" do
64
+ expect(::ActionSubscriber::Publisher).to receive(:publish).with(route, payload, exchange_name, options)
65
+ subject.push(message)
66
+ sleep 0.1 # Await results
67
+ end
68
+
69
+ context "when network error occurs" do
70
+ let(:error) { described_class::AsyncQueue::NETWORK_ERRORS.first }
71
+ before { allow(::ActionSubscriber::Publisher).to receive(:publish).and_raise(error) }
72
+
73
+ it "requeues the message" do
74
+ consumer = subject.consumer
75
+ expect(consumer).to be_alive
76
+ expect(subject).to receive(:await_network_reconnect).at_least(:once)
77
+ subject.push(message)
78
+ sleep 0.1 # Await results
79
+ end
80
+ end
81
+
82
+ context "when an unknown error occurs" do
83
+ before { allow(::ActionSubscriber::Publisher).to receive(:publish).and_raise(ArgumentError) }
84
+
85
+ it "kills the consumer" do
86
+ consumer = subject.consumer
87
+ expect(consumer).to be_alive
88
+ subject.push(message)
89
+ sleep 0.1 # Await results
90
+ expect(consumer).to_not be_alive
91
+ end
92
+ end
93
+ end
94
+
95
+ describe "#push" do
96
+ after { ::ActionSubscriber.configuration.async_publisher_max_queue_size = 1000 }
97
+ after { ::ActionSubscriber.configuration.async_publisher_drop_messages_when_queue_full = false }
98
+
99
+ context "when the queue has room" do
100
+ before { allow(::Queue).to receive(:new).and_return(mock_queue) }
101
+
102
+ it "successfully adds to the queue" do
103
+ expect(mock_queue).to receive(:push).with(message)
104
+ subject.push(message)
105
+ end
106
+ end
107
+
108
+ context "when the queue is full" do
109
+ before { ::ActionSubscriber.configuration.async_publisher_max_queue_size = -1 }
110
+
111
+ context "and we're dropping messages" do
112
+ before { ::ActionSubscriber.configuration.async_publisher_drop_messages_when_queue_full = true }
113
+
114
+ it "adding to the queue should not raise an error" do
115
+ expect { subject.push(message) }.to_not raise_error
116
+ end
117
+ end
118
+
119
+ context "and we're not dropping messages" do
120
+ before { ::ActionSubscriber.configuration.async_publisher_drop_messages_when_queue_full = false }
121
+
122
+ it "adding to the queue should raise error back to caller" do
123
+ expect { subject.push(message) }.to raise_error(described_class::UnableToPersistMessageError)
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "#size" do
130
+ it "can return the size of the queue" do
131
+ expect(subject.size).to eq(0)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,40 @@
1
+ describe ::ActionSubscriber::Publisher::Async do
2
+
3
+ before { described_class.instance_variable_set(:@publisher_adapter, nil) }
4
+ after { ::ActionSubscriber.configuration.async_publisher = "memory" }
5
+
6
+ let(:mock_adapter) { double(:publish => nil) }
7
+
8
+ describe ".publish_async" do
9
+ before { allow(described_class).to receive(:publisher_adapter).and_return(mock_adapter) }
10
+
11
+ it "calls through the adapter" do
12
+ expect(mock_adapter).to receive(:publish).with("1", "2", "3", { "four" => "five" })
13
+ ::ActionSubscriber::Publisher.publish_async("1", "2", "3", { "four" => "five" })
14
+ end
15
+ end
16
+
17
+ context "when an in-memory adapter is selected" do
18
+ before { ::ActionSubscriber.configuration.async_publisher = "memory" }
19
+
20
+ it "Creates an in-memory publisher" do
21
+ expect(described_class.publisher_adapter).to be_an(::ActionSubscriber::Publisher::Async::InMemoryAdapter)
22
+ end
23
+ end
24
+
25
+ context "when an redis adapter is selected" do
26
+ before { ::ActionSubscriber.configuration.async_publisher = "redis" }
27
+
28
+ it "raises an error" do
29
+ expect { described_class.publisher_adapter }.to raise_error("Not yet implemented")
30
+ end
31
+ end
32
+
33
+ context "when some random adapter is selected" do
34
+ before { ::ActionSubscriber.configuration.async_publisher = "yolo" }
35
+
36
+ it "raises an error" do
37
+ expect { described_class.publisher_adapter }.to raise_error("Unknown adapter 'yolo' provided")
38
+ end
39
+ end
40
+ end
data/spec/spec_helper.rb CHANGED
@@ -13,6 +13,7 @@ require 'support/user_subscriber'
13
13
  require 'action_subscriber/rspec'
14
14
 
15
15
  # Silence the Logger
16
+ $TESTING = true
16
17
  ::ActionSubscriber::Logging.initialize_logger(nil)
17
18
 
18
19
  RSpec.configure do |config|
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: 2.0.1
4
+ version: 2.1.0.pre1
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: 2016-01-26 00:00:00.000000000 Z
15
+ date: 2016-02-04 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activesupport
@@ -214,6 +214,8 @@ files:
214
214
  - lib/action_subscriber/middleware/router.rb
215
215
  - lib/action_subscriber/middleware/runner.rb
216
216
  - lib/action_subscriber/publisher.rb
217
+ - lib/action_subscriber/publisher/async.rb
218
+ - lib/action_subscriber/publisher/async/in_memory_adapter.rb
217
219
  - lib/action_subscriber/rabbit_connection.rb
218
220
  - lib/action_subscriber/railtie.rb
219
221
  - lib/action_subscriber/route.rb
@@ -245,6 +247,8 @@ files:
245
247
  - spec/lib/action_subscriber/middleware/error_handler_spec.rb
246
248
  - spec/lib/action_subscriber/middleware/router_spec.rb
247
249
  - spec/lib/action_subscriber/middleware/runner_spec.rb
250
+ - spec/lib/action_subscriber/publisher/async/in_memory_adapter_spec.rb
251
+ - spec/lib/action_subscriber/publisher/async_spec.rb
248
252
  - spec/lib/action_subscriber/publisher_spec.rb
249
253
  - spec/lib/action_subscriber/router_spec.rb
250
254
  - spec/lib/action_subscriber/subscribable_spec.rb
@@ -266,12 +270,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
266
270
  version: '0'
267
271
  required_rubygems_version: !ruby/object:Gem::Requirement
268
272
  requirements:
269
- - - ">="
273
+ - - ">"
270
274
  - !ruby/object:Gem::Version
271
- version: '0'
275
+ version: 1.3.1
272
276
  requirements: []
273
277
  rubyforge_project:
274
- rubygems_version: 2.5.1
278
+ rubygems_version: 2.4.5
275
279
  signing_key:
276
280
  specification_version: 4
277
281
  summary: ActionSubscriber is a DSL that allows a rails app to consume messages from
@@ -298,6 +302,8 @@ test_files:
298
302
  - spec/lib/action_subscriber/middleware/error_handler_spec.rb
299
303
  - spec/lib/action_subscriber/middleware/router_spec.rb
300
304
  - spec/lib/action_subscriber/middleware/runner_spec.rb
305
+ - spec/lib/action_subscriber/publisher/async/in_memory_adapter_spec.rb
306
+ - spec/lib/action_subscriber/publisher/async_spec.rb
301
307
  - spec/lib/action_subscriber/publisher_spec.rb
302
308
  - spec/lib/action_subscriber/router_spec.rb
303
309
  - spec/lib/action_subscriber/subscribable_spec.rb