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,57 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Bunny
|
3
|
+
module Subscriber
|
4
|
+
def auto_pop!
|
5
|
+
# Because threadpools can be large we want to cap the number
|
6
|
+
# of times we will pop each time we poll the broker
|
7
|
+
times_to_pop = [::ActionSubscriber::Threadpool.ready_size, ::ActionSubscriber.config.times_to_pop].min
|
8
|
+
times_to_pop.times do
|
9
|
+
queues.each do |queue|
|
10
|
+
delivery_info, properties, encoded_payload = queue.pop(queue_subscription_options)
|
11
|
+
next unless encoded_payload # empty queue
|
12
|
+
::ActiveSupport::Notifications.instrument "popped_event.action_subscriber", :payload_size => encoded_payload.bytesize, :queue => queue.name
|
13
|
+
properties = {
|
14
|
+
:channel => queue.channel,
|
15
|
+
:content_type => properties[:content_type],
|
16
|
+
:delivery_tag => delivery_info.delivery_tag,
|
17
|
+
:exchange => delivery_info.exchange,
|
18
|
+
:message_id => nil,
|
19
|
+
:routing_key => delivery_info.routing_key,
|
20
|
+
}
|
21
|
+
env = ::ActionSubscriber::Middleware::Env.new(self, encoded_payload, properties)
|
22
|
+
enqueue_env(env)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def auto_subscribe!
|
28
|
+
queues.each do |queue|
|
29
|
+
queue.channel.prefetch(::ActionSubscriber.config.prefetch) if acknowledge_messages?
|
30
|
+
queue.subscribe(queue_subscription_options) do |delivery_info, properties, encoded_payload|
|
31
|
+
::ActiveSupport::Notifications.instrument "received_event.action_subscriber", :payload_size => encoded_payload.bytesize, :queue => queue.name
|
32
|
+
properties = {
|
33
|
+
:channel => queue.channel,
|
34
|
+
:content_type => properties.content_type,
|
35
|
+
:delivery_tag => delivery_info.delivery_tag,
|
36
|
+
:exchange => delivery_info.exchange,
|
37
|
+
:message_id => properties.message_id,
|
38
|
+
:routing_key => delivery_info.routing_key,
|
39
|
+
}
|
40
|
+
env = ::ActionSubscriber::Middleware::Env.new(self, encoded_payload, properties)
|
41
|
+
enqueue_env(env)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def enqueue_env(env)
|
49
|
+
::ActionSubscriber::Threadpool.pool.async(env) do |env|
|
50
|
+
::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key do
|
51
|
+
::ActionSubscriber.config.middleware.call(env)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :allow_low_priority_methods,
|
4
|
+
:decoder,
|
5
|
+
:default_exchange,
|
6
|
+
:error_handler,
|
7
|
+
:heartbeat,
|
8
|
+
:timeout,
|
9
|
+
:host,
|
10
|
+
:hosts,
|
11
|
+
:port,
|
12
|
+
:prefetch,
|
13
|
+
:times_to_pop,
|
14
|
+
:threadpool_size
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
self.allow_low_priority_methods = false
|
18
|
+
self.decoder = {
|
19
|
+
'application/json' => lambda { |payload| JSON.parse(payload) },
|
20
|
+
'text/plain' => lambda { |payload| payload.dup }
|
21
|
+
}
|
22
|
+
self.default_exchange = "events"
|
23
|
+
self.error_handler = lambda { |error, env_hash| raise }
|
24
|
+
self.heartbeat = 5
|
25
|
+
self.timeout = 1
|
26
|
+
self.host = 'localhost'
|
27
|
+
self.hosts = []
|
28
|
+
self.port = 5672
|
29
|
+
self.prefetch = 200
|
30
|
+
self.times_to_pop = 8
|
31
|
+
self.threadpool_size = 8
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Instance Methods
|
36
|
+
#
|
37
|
+
def add_decoder(decoders)
|
38
|
+
decoders.each_pair do |content_type, decoder|
|
39
|
+
unless decoder.arity == 1
|
40
|
+
raise "ActionSubscriber decoders must have an arity of 1. The #{content_type} decoder was given with arity of #{decoder.arity}."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
self.decoder.merge!(decoders)
|
45
|
+
end
|
46
|
+
|
47
|
+
def hosts
|
48
|
+
return @hosts if @hosts.size > 0
|
49
|
+
[ host ]
|
50
|
+
end
|
51
|
+
|
52
|
+
def middleware
|
53
|
+
@middleware ||= Middleware.initialize_stack
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
inspection_string = <<-INSPECT.strip_heredoc
|
58
|
+
Rabbit Host: #{host}
|
59
|
+
Rabbit Port: #{port}
|
60
|
+
Threadpool Size: #{threadpool_size}
|
61
|
+
Low Priority Subscriber: #{allow_low_priority_methods}
|
62
|
+
Decoders:
|
63
|
+
INSPECT
|
64
|
+
decoder.each_key { |key| inspection_string << " --#{key}\n" }
|
65
|
+
return inspection_string
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module DefaultRouting
|
3
|
+
def queues
|
4
|
+
@_queues ||= []
|
5
|
+
end
|
6
|
+
|
7
|
+
def setup_queue!(method_name, exchange_name)
|
8
|
+
queue_name = queue_name_for_method(method_name)
|
9
|
+
routing_key_name = routing_key_name_for_method(method_name)
|
10
|
+
|
11
|
+
channel = ::ActionSubscriber::RabbitConnection.connection.create_channel
|
12
|
+
exchange = channel.topic(exchange_name)
|
13
|
+
queue = channel.queue(queue_name)
|
14
|
+
queue.bind(exchange, :routing_key => routing_key_name)
|
15
|
+
return queue
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup_queues!
|
19
|
+
exchange_names.each do |exchange_name|
|
20
|
+
subscribable_methods.each do |method_name|
|
21
|
+
queues << setup_queue!(method_name, exchange_name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module DSL
|
3
|
+
def at_least_once!
|
4
|
+
@_acknowledge_messages = true
|
5
|
+
@_acknowledge_messages_after_processing = true
|
6
|
+
end
|
7
|
+
|
8
|
+
def at_most_once!
|
9
|
+
@_acknowledge_messages = true
|
10
|
+
@_acknowledge_messages_before_processing = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def acknowledge_messages?
|
14
|
+
!!@_acknowledge_messages
|
15
|
+
end
|
16
|
+
|
17
|
+
def acknowledge_messages_after_processing?
|
18
|
+
!!@_acknowledge_messages_after_processing
|
19
|
+
end
|
20
|
+
|
21
|
+
def acknowledge_messages_before_processing?
|
22
|
+
!!@_acknowledge_messages_before_processing
|
23
|
+
end
|
24
|
+
|
25
|
+
# Explicitly set the name of the exchange
|
26
|
+
#
|
27
|
+
def exchange_names(*names)
|
28
|
+
@_exchange_names ||= []
|
29
|
+
@_exchange_names += names.flatten.map(&:to_s)
|
30
|
+
|
31
|
+
if @_exchange_names.empty?
|
32
|
+
return [ ::ActionSubscriber.config.default_exchange ]
|
33
|
+
else
|
34
|
+
return @_exchange_names.compact.uniq
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias_method :exchange, :exchange_names
|
38
|
+
|
39
|
+
def manual_acknowledgement!
|
40
|
+
@_acknowledge_messages = true
|
41
|
+
end
|
42
|
+
|
43
|
+
def no_acknowledgement!
|
44
|
+
@_acknowledge_messages = false
|
45
|
+
end
|
46
|
+
|
47
|
+
# Explicitly set the name of a queue for the given method route
|
48
|
+
#
|
49
|
+
# Ex.
|
50
|
+
# queue_for :created, "derp.derp"
|
51
|
+
# queue_for :updated, "foo.bar"
|
52
|
+
#
|
53
|
+
def queue_for(method, queue_name)
|
54
|
+
@_queue_names ||= {}
|
55
|
+
@_queue_names[method] = queue_name
|
56
|
+
end
|
57
|
+
|
58
|
+
def queue_names
|
59
|
+
@_queue_names ||= {}
|
60
|
+
end
|
61
|
+
|
62
|
+
def queue_subscription_options
|
63
|
+
@_queue_subscription_options ||= { :manual_ack => acknowledge_messages? }
|
64
|
+
end
|
65
|
+
|
66
|
+
def remote_application_name(name = nil)
|
67
|
+
@_remote_application_name = name if name
|
68
|
+
@_remote_application_name
|
69
|
+
end
|
70
|
+
alias_method :publisher, :remote_application_name
|
71
|
+
|
72
|
+
# Explicitly set the whole routing key to use for a given method route.
|
73
|
+
#
|
74
|
+
def routing_key_for(method, routing_key_name)
|
75
|
+
@_routing_key_names ||= {}
|
76
|
+
@_routing_key_names[method] = routing_key_name
|
77
|
+
end
|
78
|
+
|
79
|
+
def routing_key_names
|
80
|
+
@_routing_key_names ||= {}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module MarchHare
|
3
|
+
module Subscriber
|
4
|
+
def auto_pop!
|
5
|
+
# Because threadpools can be large we want to cap the number
|
6
|
+
# of times we will pop each time we poll the broker
|
7
|
+
times_to_pop = [::ActionSubscriber::Threadpool.ready_size, ::ActionSubscriber.config.times_to_pop].min
|
8
|
+
times_to_pop.times do
|
9
|
+
queues.each do |queue|
|
10
|
+
header, encoded_payload = queue.pop(queue_subscription_options)
|
11
|
+
next unless encoded_payload
|
12
|
+
::ActiveSupport::Notifications.instrument "popped_event.action_subscriber", :payload_size => encoded_payload.bytesize, :queue => queue.name
|
13
|
+
properties = {
|
14
|
+
:channel => queue.channel,
|
15
|
+
:content_type => header.content_type,
|
16
|
+
:delivery_tag => header.delivery_tag,
|
17
|
+
:exchange => header.exchange,
|
18
|
+
:message_id => header.message_id,
|
19
|
+
:routing_key => header.routing_key,
|
20
|
+
}
|
21
|
+
env = ::ActionSubscriber::Middleware::Env.new(self, encoded_payload, properties)
|
22
|
+
enqueue_env(env)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
rescue ::MarchHare::ChannelAlreadyClosed => e
|
27
|
+
# The connection has gone down, we can just try again on the next pop
|
28
|
+
end
|
29
|
+
|
30
|
+
def auto_subscribe!
|
31
|
+
queues.each do |queue|
|
32
|
+
queue.channel.prefetch = ::ActionSubscriber.config.prefetch if acknowledge_messages?
|
33
|
+
queue.subscribe(queue_subscription_options) do |header, encoded_payload|
|
34
|
+
::ActiveSupport::Notifications.instrument "received_event.action_subscriber", :payload_size => encoded_payload.bytesize, :queue => queue.name
|
35
|
+
properties = {
|
36
|
+
:channel => queue.channel,
|
37
|
+
:content_type => header.content_type,
|
38
|
+
:delivery_tag => header.delivery_tag,
|
39
|
+
:exchange => header.exchange,
|
40
|
+
:message_id => header.message_id,
|
41
|
+
:routing_key => header.routing_key,
|
42
|
+
}
|
43
|
+
env = ::ActionSubscriber::Middleware::Env.new(self, encoded_payload, properties)
|
44
|
+
enqueue_env(env)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def enqueue_env(env)
|
52
|
+
::ActionSubscriber::Threadpool.pool.async(env) do |env|
|
53
|
+
::ActiveSupport::Notifications.instrument "process_event.action_subscriber", :subscriber => env.subscriber.to_s, :routing_key => env.routing_key do
|
54
|
+
::ActionSubscriber.config.middleware.call(env)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "action_subscriber/middleware/decoder"
|
2
|
+
require "action_subscriber/middleware/env"
|
3
|
+
require "action_subscriber/middleware/error_handler"
|
4
|
+
require "action_subscriber/middleware/router"
|
5
|
+
require "action_subscriber/middleware/runner"
|
6
|
+
|
7
|
+
module ActionSubscriber
|
8
|
+
module Middleware
|
9
|
+
def self.initialize_stack
|
10
|
+
builder = ::Middleware::Builder.new(:runner_class => ::ActionSubscriber::Middleware::Runner)
|
11
|
+
|
12
|
+
builder.use ErrorHandler
|
13
|
+
builder.use Decoder
|
14
|
+
|
15
|
+
builder
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Middleware
|
3
|
+
module ActiveRecord
|
4
|
+
class ConnectionManagement
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
@app.call(env)
|
11
|
+
ensure
|
12
|
+
::ActiveRecord::Base.clear_active_connections!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Middleware
|
3
|
+
module ActiveRecord
|
4
|
+
class QueryCache
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
enabled = ::ActiveRecord::Base.connection.query_cache_enabled
|
11
|
+
connection_id = ::ActiveRecord::Base.connection_id
|
12
|
+
::ActiveRecord::Base.connection.enable_query_cache!
|
13
|
+
|
14
|
+
@app.call(env)
|
15
|
+
ensure
|
16
|
+
restore_query_cache_settings(connection_id, enabled)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def restore_query_cache_settings(connection_id, enabled)
|
22
|
+
::ActiveRecord::Base.connection_id = connection_id
|
23
|
+
::ActiveRecord::Base.connection.clear_query_cache
|
24
|
+
::ActiveRecord::Base.connection.disable_query_cache! unless enabled
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Middleware
|
3
|
+
class Decoder
|
4
|
+
attr_reader :env
|
5
|
+
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
@env = env
|
12
|
+
|
13
|
+
env.payload = decoder? ? decoder.call(encoded_payload) : encoded_payload.dup
|
14
|
+
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def decoder
|
21
|
+
ActionSubscriber.config.decoder[env.content_type]
|
22
|
+
end
|
23
|
+
|
24
|
+
def decoder?
|
25
|
+
decoder.present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def encoded_payload
|
29
|
+
env.encoded_payload
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ActionSubscriber
|
2
|
+
module Middleware
|
3
|
+
class Env
|
4
|
+
attr_accessor :payload
|
5
|
+
|
6
|
+
attr_reader :content_type,
|
7
|
+
:encoded_payload,
|
8
|
+
:exchange,
|
9
|
+
:message_id,
|
10
|
+
:routing_key,
|
11
|
+
:subscriber
|
12
|
+
|
13
|
+
##
|
14
|
+
# @param subscriber [Class] the class that will handle this message
|
15
|
+
# @param encoded_payload [String] the payload as it was received from RabbitMQ
|
16
|
+
# @param properties [Hash] that must contain the following keys (as symbols)
|
17
|
+
# :channel => RabbitMQ channel for doing acknowledgement
|
18
|
+
# :content_type => String
|
19
|
+
# :delivery_tag => String (the message identifier to send back to rabbitmq for acknowledgement)
|
20
|
+
# :exchange => String
|
21
|
+
# :message_id => String
|
22
|
+
# :routing_key => String
|
23
|
+
|
24
|
+
def initialize(subscriber, encoded_payload, properties)
|
25
|
+
@channel = properties.fetch(:channel)
|
26
|
+
@content_type = properties.fetch(:content_type)
|
27
|
+
@delivery_tag = properties.fetch(:delivery_tag)
|
28
|
+
@encoded_payload = encoded_payload
|
29
|
+
@exchange = properties.fetch(:exchange)
|
30
|
+
@message_id = properties.fetch(:message_id)
|
31
|
+
@routing_key = properties.fetch(:routing_key)
|
32
|
+
@subscriber = subscriber
|
33
|
+
end
|
34
|
+
|
35
|
+
def acknowledge
|
36
|
+
acknowledge_multiple_messages = false
|
37
|
+
@channel.ack(@delivery_tag, acknowledge_multiple_messages)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the last element of the routing key to indicate which action
|
41
|
+
# to route the payload to
|
42
|
+
#
|
43
|
+
def action
|
44
|
+
routing_key.split('.').last.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def reject
|
48
|
+
requeue_message = true
|
49
|
+
@channel.reject(@delivery_tag, requeue_message)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_hash
|
53
|
+
{
|
54
|
+
:action => action,
|
55
|
+
:content_type => content_type,
|
56
|
+
:exchange => exchange,
|
57
|
+
:routing_key => routing_key,
|
58
|
+
:payload => payload
|
59
|
+
}
|
60
|
+
end
|
61
|
+
alias_method :to_h, :to_hash
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|