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.
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 +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,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