action_subscriber 1.0.3-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.
- 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 +255 -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
|