action_subscriber 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/LICENSE +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +122 -0
- data/Rakefile +8 -0
- data/action_subscriber.gemspec +38 -0
- data/examples/at_least_once.rb +17 -0
- data/examples/at_most_once.rb +15 -0
- data/examples/basic_subscriber.rb +30 -0
- data/examples/message_acknowledgement.rb +19 -0
- data/lib/action_subscriber.rb +93 -0
- data/lib/action_subscriber/base.rb +83 -0
- data/lib/action_subscriber/bunny/subscriber.rb +57 -0
- data/lib/action_subscriber/configuration.rb +68 -0
- data/lib/action_subscriber/default_routing.rb +26 -0
- data/lib/action_subscriber/dsl.rb +83 -0
- data/lib/action_subscriber/march_hare/subscriber.rb +60 -0
- data/lib/action_subscriber/middleware.rb +18 -0
- data/lib/action_subscriber/middleware/active_record/connection_management.rb +17 -0
- data/lib/action_subscriber/middleware/active_record/query_cache.rb +29 -0
- data/lib/action_subscriber/middleware/decoder.rb +33 -0
- data/lib/action_subscriber/middleware/env.rb +65 -0
- data/lib/action_subscriber/middleware/error_handler.rb +16 -0
- data/lib/action_subscriber/middleware/router.rb +17 -0
- data/lib/action_subscriber/middleware/runner.rb +16 -0
- data/lib/action_subscriber/rabbit_connection.rb +40 -0
- data/lib/action_subscriber/railtie.rb +13 -0
- data/lib/action_subscriber/rspec.rb +91 -0
- data/lib/action_subscriber/subscribable.rb +118 -0
- data/lib/action_subscriber/threadpool.rb +29 -0
- data/lib/action_subscriber/version.rb +3 -0
- data/spec/integration/basic_subscriber_spec.rb +42 -0
- data/spec/lib/action_subscriber/base_spec.rb +18 -0
- data/spec/lib/action_subscriber/configuration_spec.rb +32 -0
- data/spec/lib/action_subscriber/dsl_spec.rb +143 -0
- data/spec/lib/action_subscriber/middleware/active_record/connection_management_spec.rb +17 -0
- data/spec/lib/action_subscriber/middleware/active_record/query_cache_spec.rb +49 -0
- data/spec/lib/action_subscriber/middleware/decoder_spec.rb +31 -0
- data/spec/lib/action_subscriber/middleware/env_spec.rb +60 -0
- data/spec/lib/action_subscriber/middleware/error_handler_spec.rb +35 -0
- data/spec/lib/action_subscriber/middleware/router_spec.rb +24 -0
- data/spec/lib/action_subscriber/middleware/runner_spec.rb +6 -0
- data/spec/lib/action_subscriber/subscribable_spec.rb +128 -0
- data/spec/lib/action_subscriber/threadpool_spec.rb +35 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/user_subscriber.rb +6 -0
- metadata +257 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Middleware
|
3
|
+
class ErrorHandler
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
@app.call(env)
|
10
|
+
rescue => error
|
11
|
+
env.reject if env.subscriber.acknowledge_messages_after_processing?
|
12
|
+
::ActionSubscriber.configuration.error_handler.call(error, env.to_h)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Middleware
|
3
|
+
class Router
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
subscriber = env.subscriber.new(env)
|
10
|
+
|
11
|
+
env.acknowledge if env.subscriber.acknowledge_messages_before_processing?
|
12
|
+
subscriber.public_send(env.action)
|
13
|
+
env.acknowledge if env.subscriber.acknowledge_messages_after_processing?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'middleware/runner'
|
2
|
+
|
3
|
+
module ActionSubscriber
|
4
|
+
module Middleware
|
5
|
+
class Runner < ::Middleware::Runner
|
6
|
+
# Override the default middleware runner so we can ensure that the
|
7
|
+
# router is the last thing called in the stack.
|
8
|
+
#
|
9
|
+
def initialize(stack)
|
10
|
+
stack << ::ActionSubscriber::Middleware::Router
|
11
|
+
|
12
|
+
super(stack)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module ActionSubscriber
|
4
|
+
module RabbitConnection
|
5
|
+
CONNECTION_MUTEX = ::Mutex.new
|
6
|
+
|
7
|
+
def self.connect!
|
8
|
+
CONNECTION_MUTEX.synchronize do
|
9
|
+
return @connection if @connection
|
10
|
+
if ::RUBY_PLATFORM == "java"
|
11
|
+
@connection = ::MarchHare.connect(connection_options)
|
12
|
+
else
|
13
|
+
@connection = ::Bunny.new(connection_options)
|
14
|
+
@connection.start
|
15
|
+
end
|
16
|
+
@connection
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.connected?
|
21
|
+
connection && connection.connected?
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.connection
|
25
|
+
connect!
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.connection_options
|
29
|
+
{
|
30
|
+
:heartbeat => ::ActionSubscriber.configuration.heartbeat,
|
31
|
+
:hosts => ::ActionSubscriber.configuration.hosts,
|
32
|
+
:port => ::ActionSubscriber.configuration.port,
|
33
|
+
:continuation_timeout => ::ActionSubscriber.configuration.timeout * 1_000.0, #convert sec to ms
|
34
|
+
:automatically_recover => true,
|
35
|
+
:network_recovery_interval => 1,
|
36
|
+
:recover_from_connection_close => true,
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
config.action_subscriber = ::ActionSubscriber.config
|
4
|
+
|
5
|
+
::ActiveSupport.on_load(:active_record) do
|
6
|
+
require "action_subscriber/middleware/active_record/connection_management"
|
7
|
+
require "action_subscriber/middleware/active_record/query_cache"
|
8
|
+
|
9
|
+
::ActionSubscriber.config.middleware.use ::ActionSubscriber::Middleware::ActiveRecord::ConnectionManagement
|
10
|
+
::ActionSubscriber.config.middleware.use ::ActionSubscriber::Middleware::ActiveRecord::QueryCache
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
|
3
|
+
module ActionSubscriber
|
4
|
+
module RSpec
|
5
|
+
class FakeChannel # A class that quacks like a RabbitMQ Channel
|
6
|
+
def ack(delivery_tag, acknowledge_multiple)
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def reject(delivery_tag, requeue_message)
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
PROPERTIES_DEFAULTS = {
|
16
|
+
:channel => FakeChannel.new,
|
17
|
+
:content_type => "text/plain",
|
18
|
+
:delivery_tag => "XYZ",
|
19
|
+
:exchange => "events",
|
20
|
+
:message_id => "MSG-123",
|
21
|
+
:routing_key => "amigo.user.created",
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# Create a new subscriber instance. Available options are:
|
25
|
+
#
|
26
|
+
# * :acknowledger - the object that should receive ack/reject calls for this message (only useful for testing manual acknowledgment)
|
27
|
+
# * :content_type - defaults to text/plain
|
28
|
+
# * :encoded_payload - the encoded payload object to pass into the instance.
|
29
|
+
# * :exchange - defaults to "events"
|
30
|
+
# * :message_id - defaults to "MSG-123"
|
31
|
+
# * :payload - the payload object to pass to the instance.
|
32
|
+
# * :routing_key - defaults to amigo.user.created
|
33
|
+
# * :subscriber - the class constant corresponding to the subscriber. `described_class` is the default.
|
34
|
+
#
|
35
|
+
# Example
|
36
|
+
#
|
37
|
+
# describe UserSubscriber do
|
38
|
+
# subject { mock_subscriber(:payload => proto) }
|
39
|
+
#
|
40
|
+
# it 'logs the user create event' do
|
41
|
+
# SomeLogger.should_receive(:log)
|
42
|
+
# subject.created
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
def mock_subscriber(opts = {})
|
47
|
+
encoded_payload = opts.fetch(:encoded_payload) { double('encoded payload').as_null_object }
|
48
|
+
subscriber_class = opts.fetch(:subscriber) { described_class }
|
49
|
+
properties = PROPERTIES_DEFAULTS.merge(opts.slice(:channel,
|
50
|
+
:content_type,
|
51
|
+
:delivery_tag,
|
52
|
+
:exchange,
|
53
|
+
:message_id,
|
54
|
+
:routing_key))
|
55
|
+
|
56
|
+
|
57
|
+
env = ActionSubscriber::Middleware::Env.new(subscriber_class, encoded_payload, properties)
|
58
|
+
env.payload = opts.fetch(:payload) { double('payload').as_null_object }
|
59
|
+
|
60
|
+
return subscriber_class.new(env)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
::RSpec.configure do |config|
|
66
|
+
config.include ActionSubscriber::RSpec
|
67
|
+
|
68
|
+
shared_context 'action subscriber middleware env' do
|
69
|
+
let(:app) { Proc.new { |inner_env| inner_env } }
|
70
|
+
let(:env) { ActionSubscriber::Middleware::Env.new(UserSubscriber, 'encoded payload', message_properties) }
|
71
|
+
let(:message_properties) {{
|
72
|
+
:channel => ::ActionSubscriber::RSpec::FakeChannel.new,
|
73
|
+
:content_type => "text/plain",
|
74
|
+
:delivery_tag => "XYZ",
|
75
|
+
:exchange => "events",
|
76
|
+
:message_id => "MSG-123",
|
77
|
+
:routing_key => "amigo.user.created",
|
78
|
+
}}
|
79
|
+
end
|
80
|
+
|
81
|
+
shared_examples_for 'an action subscriber middleware' do
|
82
|
+
include_context 'action subscriber middleware env'
|
83
|
+
|
84
|
+
subject { described_class.new(app) }
|
85
|
+
|
86
|
+
it "calls the stack" do
|
87
|
+
expect(app).to receive(:call).with(env)
|
88
|
+
subject.call(env)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Subscribable
|
3
|
+
def allow_low_priority_methods?
|
4
|
+
!!(::ActionSubscriber.configuration.allow_low_priority_methods)
|
5
|
+
end
|
6
|
+
|
7
|
+
def filter_low_priority_methods(methods)
|
8
|
+
if allow_low_priority_methods?
|
9
|
+
return methods
|
10
|
+
else
|
11
|
+
return methods - methods.grep(/_low/)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_queue_name(method_name)
|
16
|
+
[
|
17
|
+
local_application_name,
|
18
|
+
remote_application_name,
|
19
|
+
resource_name,
|
20
|
+
method_name
|
21
|
+
].compact.join('.')
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_routing_key_name(method_name)
|
25
|
+
[
|
26
|
+
remote_application_name,
|
27
|
+
resource_name,
|
28
|
+
method_name
|
29
|
+
].compact.join('.')
|
30
|
+
end
|
31
|
+
|
32
|
+
def local_application_name(reload = false)
|
33
|
+
if reload || @_local_application_name.nil?
|
34
|
+
@_local_application_name = case
|
35
|
+
when ENV['APP_NAME'] then
|
36
|
+
ENV['APP_NAME'].to_s.dup
|
37
|
+
when defined?(::Rails) then
|
38
|
+
::Rails.application.class.parent_name.dup
|
39
|
+
else
|
40
|
+
raise "Define an application name (ENV['APP_NAME'])"
|
41
|
+
end
|
42
|
+
|
43
|
+
@_local_application_name.downcase!
|
44
|
+
end
|
45
|
+
|
46
|
+
@_local_application_name
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
inspection_string = "#{self.name}\n"
|
51
|
+
exchange_names.each do |exchange_name|
|
52
|
+
inspection_string << " -- exchange: #{exchange_name}\n"
|
53
|
+
subscribable_methods.each do |method|
|
54
|
+
inspection_string << " -- method: #{method}\n"
|
55
|
+
inspection_string << " -- queue: #{queue_names[method]}\n"
|
56
|
+
inspection_string << " -- routing_key: #{routing_key_names[method]}\n"
|
57
|
+
inspection_string << "\n"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
return inspection_string
|
61
|
+
end
|
62
|
+
|
63
|
+
# Build the `queue` for a given method.
|
64
|
+
#
|
65
|
+
# If the queue name is not set, the queue name is
|
66
|
+
# "local.remote.resoure.action"
|
67
|
+
#
|
68
|
+
# Example
|
69
|
+
# "bob.alice.user.created"
|
70
|
+
#
|
71
|
+
def queue_name_for_method(method_name)
|
72
|
+
return queue_names[method_name] if queue_names[method_name]
|
73
|
+
|
74
|
+
queue_name = generate_queue_name(method_name)
|
75
|
+
queue_for(method_name, queue_name)
|
76
|
+
return queue_name
|
77
|
+
end
|
78
|
+
|
79
|
+
# The name of the resource respresented by this subscriber.
|
80
|
+
# If the class name were `UserSubscriber` the resource_name would be `user`.
|
81
|
+
#
|
82
|
+
def resource_name
|
83
|
+
@_resource_name ||= self.name.underscore.gsub(/_subscriber/, '').to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
# Build the `routing_key` for a given method.
|
87
|
+
#
|
88
|
+
# If the routing_key name is not set, the routing_key name is
|
89
|
+
# "remote.resoure.action"
|
90
|
+
#
|
91
|
+
# Example
|
92
|
+
# "amigo.user.created"
|
93
|
+
#
|
94
|
+
def routing_key_name_for_method(method_name)
|
95
|
+
return routing_key_names[method_name] if routing_key_names[method_name]
|
96
|
+
|
97
|
+
routing_key_name = generate_routing_key_name(method_name)
|
98
|
+
routing_key_for(method_name, routing_key_name)
|
99
|
+
return routing_key_name
|
100
|
+
end
|
101
|
+
|
102
|
+
def subscribable_methods
|
103
|
+
return @_subscribable_methods if @_subscribable_methods
|
104
|
+
|
105
|
+
methods = instance_methods
|
106
|
+
methods -= ::Object.instance_methods
|
107
|
+
|
108
|
+
self.included_modules.each do |mod|
|
109
|
+
methods -= mod.instance_methods
|
110
|
+
end
|
111
|
+
|
112
|
+
@_subscribable_methods = filter_low_priority_methods(methods)
|
113
|
+
@_subscribable_methods.sort!
|
114
|
+
|
115
|
+
return @_subscribable_methods
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
class Threadpool
|
3
|
+
##
|
4
|
+
# Class Methods
|
5
|
+
#
|
6
|
+
def self.busy?
|
7
|
+
(pool.pool_size == pool.busy_size)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.perform_async(*args)
|
11
|
+
self.pool.async.perform(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.pool
|
15
|
+
@pool ||= ::Lifeguard::InfiniteThreadpool.new(
|
16
|
+
:pool_size => ::ActionSubscriber.config.threadpool_size
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.ready?
|
21
|
+
!busy?
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.ready_size
|
25
|
+
ready_size = pool.pool_size - pool.busy_size
|
26
|
+
return ready_size >= 0 ? ready_size : 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class BasicPushSubscriber < ActionSubscriber::Base
|
4
|
+
BOOKED_MESSAGES = []
|
5
|
+
CANCELLED_MESSAGES = []
|
6
|
+
|
7
|
+
publisher :greg
|
8
|
+
|
9
|
+
# queue => alice.greg.basic_push.booked
|
10
|
+
# routing_key => greg.basic_push.booked
|
11
|
+
def booked
|
12
|
+
BOOKED_MESSAGES << payload
|
13
|
+
end
|
14
|
+
|
15
|
+
queue_for :cancelled, "basic.cancelled"
|
16
|
+
routing_key_for :cancelled, "basic.cancelled"
|
17
|
+
|
18
|
+
def cancelled
|
19
|
+
CANCELLED_MESSAGES << payload
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "A Basic Subscriber using Push API", :integration => true do
|
24
|
+
let(:connection) { subscriber.connection }
|
25
|
+
let(:subscriber) { BasicPushSubscriber }
|
26
|
+
|
27
|
+
it "messages are routed to the right place" do
|
28
|
+
::ActionSubscriber.start_queues
|
29
|
+
|
30
|
+
channel = connection.create_channel
|
31
|
+
exchange = channel.topic("events")
|
32
|
+
exchange.publish("Ohai Booked", :routing_key => "greg.basic_push.booked")
|
33
|
+
exchange.publish("Ohai Cancelled", :routing_key => "basic.cancelled")
|
34
|
+
|
35
|
+
::ActionSubscriber.auto_pop!
|
36
|
+
|
37
|
+
expect(subscriber::BOOKED_MESSAGES).to eq(["Ohai Booked"])
|
38
|
+
expect(subscriber::CANCELLED_MESSAGES).to eq(["Ohai Cancelled"])
|
39
|
+
|
40
|
+
connection.close
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestObject < ActionSubscriber::Base
|
4
|
+
exchange :events
|
5
|
+
|
6
|
+
def created
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe ActionSubscriber::Base do
|
11
|
+
describe "inherited" do
|
12
|
+
context "when a class has inherited from action subscriber base" do
|
13
|
+
it "adds the class to the intherited classes collection" do
|
14
|
+
expect(described_class.inherited_classes).to include(TestObject)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ::ActionSubscriber::Configuration do
|
4
|
+
describe "default values" do
|
5
|
+
specify { expect(subject.allow_low_priority_methods).to eq(false) }
|
6
|
+
specify { expect(subject.default_exchange).to eq("events") }
|
7
|
+
specify { expect(subject.host).to eq("localhost") }
|
8
|
+
specify { expect(subject.port).to eq(5672) }
|
9
|
+
specify { expect(subject.threadpool_size).to eq(8) }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "add_decoder" do
|
13
|
+
it "add the decoder to the registry" do
|
14
|
+
subject.add_decoder({"application/protobuf" => lambda { |payload| "foo"} })
|
15
|
+
expect(subject.decoder).to include("application/protobuf")
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises an error when decoder does not have arity of 1' do
|
19
|
+
expect {
|
20
|
+
subject.add_decoder("foo" => lambda { |*args| })
|
21
|
+
}.to raise_error(/The foo decoder was given with arity of -1/)
|
22
|
+
|
23
|
+
expect {
|
24
|
+
subject.add_decoder("foo" => lambda { })
|
25
|
+
}.to raise_error(/The foo decoder was given with arity of 0/)
|
26
|
+
|
27
|
+
expect {
|
28
|
+
subject.add_decoder("foo" => lambda { |a,b| })
|
29
|
+
}.to raise_error(/The foo decoder was given with arity of 2/)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|