action_subscriber 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE +20 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +122 -0
  8. data/Rakefile +8 -0
  9. data/action_subscriber.gemspec +38 -0
  10. data/examples/at_least_once.rb +17 -0
  11. data/examples/at_most_once.rb +15 -0
  12. data/examples/basic_subscriber.rb +30 -0
  13. data/examples/message_acknowledgement.rb +19 -0
  14. data/lib/action_subscriber.rb +93 -0
  15. data/lib/action_subscriber/base.rb +83 -0
  16. data/lib/action_subscriber/bunny/subscriber.rb +57 -0
  17. data/lib/action_subscriber/configuration.rb +68 -0
  18. data/lib/action_subscriber/default_routing.rb +26 -0
  19. data/lib/action_subscriber/dsl.rb +83 -0
  20. data/lib/action_subscriber/march_hare/subscriber.rb +60 -0
  21. data/lib/action_subscriber/middleware.rb +18 -0
  22. data/lib/action_subscriber/middleware/active_record/connection_management.rb +17 -0
  23. data/lib/action_subscriber/middleware/active_record/query_cache.rb +29 -0
  24. data/lib/action_subscriber/middleware/decoder.rb +33 -0
  25. data/lib/action_subscriber/middleware/env.rb +65 -0
  26. data/lib/action_subscriber/middleware/error_handler.rb +16 -0
  27. data/lib/action_subscriber/middleware/router.rb +17 -0
  28. data/lib/action_subscriber/middleware/runner.rb +16 -0
  29. data/lib/action_subscriber/rabbit_connection.rb +40 -0
  30. data/lib/action_subscriber/railtie.rb +13 -0
  31. data/lib/action_subscriber/rspec.rb +91 -0
  32. data/lib/action_subscriber/subscribable.rb +118 -0
  33. data/lib/action_subscriber/threadpool.rb +29 -0
  34. data/lib/action_subscriber/version.rb +3 -0
  35. data/spec/integration/basic_subscriber_spec.rb +42 -0
  36. data/spec/lib/action_subscriber/base_spec.rb +18 -0
  37. data/spec/lib/action_subscriber/configuration_spec.rb +32 -0
  38. data/spec/lib/action_subscriber/dsl_spec.rb +143 -0
  39. data/spec/lib/action_subscriber/middleware/active_record/connection_management_spec.rb +17 -0
  40. data/spec/lib/action_subscriber/middleware/active_record/query_cache_spec.rb +49 -0
  41. data/spec/lib/action_subscriber/middleware/decoder_spec.rb +31 -0
  42. data/spec/lib/action_subscriber/middleware/env_spec.rb +60 -0
  43. data/spec/lib/action_subscriber/middleware/error_handler_spec.rb +35 -0
  44. data/spec/lib/action_subscriber/middleware/router_spec.rb +24 -0
  45. data/spec/lib/action_subscriber/middleware/runner_spec.rb +6 -0
  46. data/spec/lib/action_subscriber/subscribable_spec.rb +128 -0
  47. data/spec/lib/action_subscriber/threadpool_spec.rb +35 -0
  48. data/spec/spec_helper.rb +26 -0
  49. data/spec/support/user_subscriber.rb +6 -0
  50. 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,3 @@
1
+ module ActionSubscriber
2
+ VERSION = "1.0.3"
3
+ 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