action_subscriber 2.5.0.pre-java → 3.0.0-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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/README.md +15 -23
  4. data/action_subscriber.gemspec +1 -0
  5. data/lib/action_subscriber.rb +11 -23
  6. data/lib/action_subscriber/babou.rb +2 -29
  7. data/lib/action_subscriber/base.rb +0 -4
  8. data/lib/action_subscriber/bunny/subscriber.rb +18 -4
  9. data/lib/action_subscriber/configuration.rb +1 -11
  10. data/lib/action_subscriber/default_routing.rb +6 -4
  11. data/lib/action_subscriber/march_hare/subscriber.rb +20 -4
  12. data/lib/action_subscriber/message_retry.rb +1 -1
  13. data/lib/action_subscriber/middleware/env.rb +3 -1
  14. data/lib/action_subscriber/middleware/error_handler.rb +20 -4
  15. data/lib/action_subscriber/rabbit_connection.rb +24 -33
  16. data/lib/action_subscriber/route.rb +5 -1
  17. data/lib/action_subscriber/route_set.rb +13 -6
  18. data/lib/action_subscriber/router.rb +15 -3
  19. data/lib/action_subscriber/version.rb +1 -1
  20. data/spec/integration/around_filters_spec.rb +1 -1
  21. data/spec/integration/at_least_once_spec.rb +1 -1
  22. data/spec/integration/at_most_once_spec.rb +1 -1
  23. data/spec/integration/automatic_reconnect_spec.rb +3 -4
  24. data/spec/integration/basic_subscriber_spec.rb +2 -2
  25. data/spec/integration/custom_actions_spec.rb +1 -1
  26. data/spec/integration/custom_headers_spec.rb +2 -2
  27. data/spec/integration/decoding_payloads_spec.rb +2 -2
  28. data/spec/integration/manual_acknowledgement_spec.rb +1 -1
  29. data/spec/integration/multiple_connections_spec.rb +36 -0
  30. data/spec/integration/multiple_threadpools_spec.rb +3 -3
  31. data/spec/lib/action_subscriber/configuration_spec.rb +1 -5
  32. data/spec/lib/action_subscriber/middleware/error_handler_spec.rb +15 -0
  33. data/spec/spec_helper.rb +8 -4
  34. metadata +21 -16
  35. data/lib/action_subscriber/publisher.rb +0 -46
  36. data/lib/action_subscriber/publisher/async.rb +0 -31
  37. data/lib/action_subscriber/publisher/async/in_memory_adapter.rb +0 -153
  38. data/spec/integration/inferred_routes_spec.rb +0 -53
  39. data/spec/lib/action_subscriber/publisher/async/in_memory_adapter_spec.rb +0 -135
  40. data/spec/lib/action_subscriber/publisher/async_spec.rb +0 -40
  41. data/spec/lib/action_subscriber/publisher_spec.rb +0 -35
@@ -8,10 +8,26 @@ module ActionSubscriber
8
8
  end
9
9
 
10
10
  def call(env)
11
- @app.call(env)
12
- rescue => error
13
- logger.error "FAILED #{env.message_id}"
14
- ::ActionSubscriber.configuration.error_handler.call(error, env.to_h)
11
+ job_mutex = ::Mutex.new
12
+ job_complete = ::ConditionVariable.new
13
+
14
+ job_mutex.synchronize do
15
+ ::Thread.new do
16
+ job_mutex.synchronize do
17
+ begin
18
+ @app.call(env)
19
+ rescue => error
20
+ logger.error "FAILED #{env.message_id}"
21
+ ::ActionSubscriber.configuration.error_handler.call(error, env.to_h)
22
+ ensure
23
+ job_complete.signal
24
+ end
25
+ end
26
+ end
27
+
28
+ # TODO we might want to pass a timeout to this wait so we can handle jobs that get frozen
29
+ job_complete.wait(job_mutex)
30
+ end
15
31
  end
16
32
  end
17
33
  end
@@ -3,57 +3,42 @@ require 'thread'
3
3
  module ActionSubscriber
4
4
  module RabbitConnection
5
5
  SUBSCRIBER_CONNECTION_MUTEX = ::Mutex.new
6
- PUBLISHER_CONNECTION_MUTEX = ::Mutex.new
7
6
  NETWORK_RECOVERY_INTERVAL = 1.freeze
8
7
 
9
- def self.publisher_connected?
10
- publisher_connection.try(:connected?)
11
- end
12
-
13
- def self.publisher_connection
8
+ def self.setup_connection(name, settings)
14
9
  SUBSCRIBER_CONNECTION_MUTEX.synchronize do
15
- return @publisher_connection if @publisher_connection
16
- @publisher_connection = create_connection
10
+ fail ArgumentError, "a #{name} connection already exists" if subscriber_connections[name]
11
+ subscriber_connections[name] = create_connection(settings)
17
12
  end
18
13
  end
19
14
 
20
- def self.publisher_disconnect!
15
+ def self.subscriber_connected?
21
16
  SUBSCRIBER_CONNECTION_MUTEX.synchronize do
22
- if @publisher_connection && @publisher_connection.connected?
23
- @publisher_connection.close
24
- end
25
-
26
- @publisher_connection = nil
17
+ subscriber_connections.all?{|_name, connection| connection.connected?}
27
18
  end
28
19
  end
29
20
 
30
- def self.subscriber_connected?
31
- subscriber_connection.try(:connected?)
32
- end
33
-
34
- def self.subscriber_connection
21
+ def self.subscriber_disconnect!
35
22
  SUBSCRIBER_CONNECTION_MUTEX.synchronize do
36
- return @subscriber_connection if @subscriber_connection
37
- @subscriber_connection = create_connection
23
+ subscriber_connections.each{|_name, connection| connection.close}
24
+ @subscriber_connections = {}
38
25
  end
39
26
  end
40
27
 
41
- def self.subscriber_disconnect!
28
+ def self.with_connection(name)
42
29
  SUBSCRIBER_CONNECTION_MUTEX.synchronize do
43
- if @subscriber_connection && @subscriber_connection.connected?
44
- @subscriber_connection.close
45
- end
46
-
47
- @subscriber_connection = nil
30
+ fail ArgumentError, "there is no connection named #{name}" unless subscriber_connections[name]
31
+ yield(subscriber_connections[name])
48
32
  end
49
33
  end
50
34
 
51
35
  # Private API
52
- def self.create_connection
36
+ def self.create_connection(settings)
37
+ options = connection_options.merge(settings)
53
38
  if ::RUBY_PLATFORM == "java"
54
- connection = ::MarchHare.connect(connection_options)
39
+ connection = ::MarchHare.connect(options)
55
40
  else
56
- connection = ::Bunny.new(connection_options)
41
+ connection = ::Bunny.new(options)
57
42
  connection.start
58
43
  connection
59
44
  end
@@ -62,18 +47,24 @@ module ActionSubscriber
62
47
 
63
48
  def self.connection_options
64
49
  {
50
+ :automatically_recover => true,
65
51
  :continuation_timeout => ::ActionSubscriber.configuration.timeout * 1_000.0, #convert sec to ms
66
52
  :heartbeat => ::ActionSubscriber.configuration.heartbeat,
67
53
  :hosts => ::ActionSubscriber.configuration.hosts,
54
+ :network_recovery_interval => NETWORK_RECOVERY_INTERVAL,
68
55
  :pass => ::ActionSubscriber.configuration.password,
69
56
  :port => ::ActionSubscriber.configuration.port,
57
+ :recover_from_connection_close => true,
58
+ :threadpool_size => ::ActionSubscriber.configuration.threadpool_size,
70
59
  :user => ::ActionSubscriber.configuration.username,
71
60
  :vhost => ::ActionSubscriber.configuration.virtual_host,
72
- :automatically_recover => true,
73
- :network_recovery_interval => NETWORK_RECOVERY_INTERVAL,
74
- :recover_from_connection_close => true,
75
61
  }
76
62
  end
77
63
  private_class_method :connection_options
64
+
65
+ def self.subscriber_connections
66
+ @subscriber_connections ||= {}
67
+ end
68
+ private_class_method :subscriber_connections
78
69
  end
79
70
  end
@@ -2,7 +2,9 @@ module ActionSubscriber
2
2
  class Route
3
3
  attr_reader :acknowledgements,
4
4
  :action,
5
- :durable,
5
+ :concurrency,
6
+ :connection_name,
7
+ :durable,
6
8
  :exchange,
7
9
  :prefetch,
8
10
  :queue,
@@ -13,6 +15,8 @@ module ActionSubscriber
13
15
  def initialize(attributes)
14
16
  @acknowledgements = attributes.fetch(:acknowledgements)
15
17
  @action = attributes.fetch(:action)
18
+ @concurrency = attributes.fetch(:concurrency, 1)
19
+ @connection_name = attributes.fetch(:connection_name)
16
20
  @durable = attributes.fetch(:durable)
17
21
  @exchange = attributes.fetch(:exchange).to_s
18
22
  @prefetch = attributes.fetch(:prefetch) { ::ActionSubscriber.config.prefetch }
@@ -12,22 +12,29 @@ module ActionSubscriber
12
12
  @routes = routes
13
13
  end
14
14
 
15
- def setup_queues!
15
+ def setup_subscriptions!
16
+ fail ::RuntimeError, "you cannot setup queues multiple times, this should only happen once at startup" unless subscriptions.empty?
16
17
  routes.each do |route|
17
- queues[route] = setup_queue(route)
18
+ route.concurrency.times do
19
+ subscriptions << {
20
+ :route => route,
21
+ :queue => setup_queue(route),
22
+ }
23
+ end
18
24
  end
19
25
  end
20
26
 
21
27
  private
22
28
 
23
- def queues
24
- @queues ||= {}
29
+ def subscriptions
30
+ @subscriptions ||= []
25
31
  end
26
32
 
27
33
  def setup_queue(route)
28
- channel = ::ActionSubscriber::RabbitConnection.subscriber_connection.create_channel
34
+ channel = ::ActionSubscriber::RabbitConnection.with_connection(route.connection_name){ |connection| connection.create_channel }
29
35
  exchange = channel.topic(route.exchange)
30
- queue = channel.queue(route.queue, :durable => route.durable)
36
+ # TODO go to back to the old way of creating a queue?
37
+ queue = create_queue(channel, route.queue, :durable => route.durable)
31
38
  queue.bind(exchange, :routing_key => route.routing_key)
32
39
  queue
33
40
  end
@@ -12,6 +12,17 @@ module ActionSubscriber
12
12
  :exchange => "events",
13
13
  }.freeze
14
14
 
15
+ def initialize
16
+ @current_connection_name = :default
17
+ end
18
+
19
+ def connection(name, settings)
20
+ ::ActionSubscriber::RabbitConnection.setup_connection(name, settings)
21
+ @current_connection_name = name
22
+ yield
23
+ @current_connection_name = :default
24
+ end
25
+
15
26
  def default_routing_key_for(route_settings)
16
27
  [
17
28
  route_settings[:publisher],
@@ -29,8 +40,9 @@ module ActionSubscriber
29
40
  ].compact.join(".")
30
41
  end
31
42
 
32
- def default_routes_for(subscriber)
33
- subscriber.routes.each do |route|
43
+ def default_routes_for(subscriber, options = {})
44
+ options = options.merge({:connection_name => @current_connection_name})
45
+ subscriber.routes(options).each do |route|
34
46
  routes << route
35
47
  end
36
48
  end
@@ -40,7 +52,7 @@ module ActionSubscriber
40
52
  end
41
53
 
42
54
  def route(subscriber, action, options = {})
43
- route_settings = DEFAULT_SETTINGS.merge(options).merge(:subscriber => subscriber, :action => action)
55
+ route_settings = DEFAULT_SETTINGS.merge(:connection_name => @current_connection_name).merge(options).merge(:subscriber => subscriber, :action => action)
44
56
  route_settings[:routing_key] ||= default_routing_key_for(route_settings)
45
57
  route_settings[:queue] ||= default_queue_for(route_settings)
46
58
  routes << Route.new(route_settings)
@@ -1,3 +1,3 @@
1
1
  module ActionSubscriber
2
- VERSION = "2.5.0.pre"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -32,7 +32,7 @@ describe "subscriber filters", :integration => true do
32
32
  it "runs multiple around filters" do
33
33
  $messages = [] #testing the order of things
34
34
  ::ActionSubscriber.auto_subscribe!
35
- ::ActionSubscriber::Publisher.publish("insta.first", "hEY Guyz!", "events")
35
+ ::ActivePublisher.publish("insta.first", "hEY Guyz!", "events")
36
36
 
37
37
  verify_expectation_within(1.0) do
38
38
  expect($messages).to eq [:whisper_before, :yell_before, "hEY Guyz!", :yell_after, :whisper_after]
@@ -17,7 +17,7 @@ describe "at_least_once! mode", :integration => true do
17
17
 
18
18
  it "retries a failed job until it succeeds" do
19
19
  ::ActionSubscriber.auto_subscribe!
20
- ::ActionSubscriber::Publisher.publish("gorby_puff.grumpy", "GrumpFace", "events")
20
+ ::ActivePublisher.publish("gorby_puff.grumpy", "GrumpFace", "events")
21
21
 
22
22
  verify_expectation_within(2.0) do
23
23
  expect($messages).to eq Set.new(["GrumpFace::0","GrumpFace::1","GrumpFace::2"])
@@ -17,7 +17,7 @@ describe "at_most_once! mode", :integration => true do
17
17
 
18
18
  it "does not retry a failed message" do
19
19
  ::ActionSubscriber.auto_subscribe!
20
- ::ActionSubscriber::Publisher.publish("pokemon.caught_em_all", "All Pokemon have been caught", "events")
20
+ ::ActivePublisher.publish("pokemon.caught_em_all", "All Pokemon have been caught", "events")
21
21
 
22
22
  verify_expectation_within(1.0) do
23
23
  expect($messages.size).to eq 1
@@ -7,7 +7,6 @@ class GusSubscriber < ActionSubscriber::Base
7
7
  end
8
8
 
9
9
  describe "Automatically reconnect on connection failure", :integration => true, :slow => true do
10
- let(:connection) { subscriber.connection }
11
10
  let(:draw_routes) do
12
11
  ::ActionSubscriber.draw_routes do
13
12
  default_routes_for GusSubscriber
@@ -18,7 +17,7 @@ describe "Automatically reconnect on connection failure", :integration => true,
18
17
 
19
18
  it "reconnects when a connection drops" do
20
19
  ::ActionSubscriber::auto_subscribe!
21
- ::ActionSubscriber::Publisher.publish("gus.spoke", "First", "events")
20
+ ::ActivePublisher.publish("gus.spoke", "First", "events")
22
21
  verify_expectation_within(5.0) do
23
22
  expect($messages).to eq(Set.new(["First"]))
24
23
  end
@@ -26,10 +25,10 @@ describe "Automatically reconnect on connection failure", :integration => true,
26
25
  close_all_connections!
27
26
  sleep 5.0
28
27
  verify_expectation_within(5.0) do
29
- expect(connection).to be_open
28
+ expect(::ActionSubscriber::RabbitConnection.with_connection(:default){|connection| connection.open?}).to eq(true)
30
29
  end
31
30
 
32
- ::ActionSubscriber::Publisher.publish("gus.spoke", "Second", "events")
31
+ ::ActivePublisher.publish("gus.spoke", "Second", "events")
33
32
  verify_expectation_within(5.0) do
34
33
  expect($messages).to eq(Set.new(["First", "Second"]))
35
34
  end
@@ -13,7 +13,7 @@ describe "A Basic Subscriber", :integration => true do
13
13
 
14
14
  context "ActionSubscriber.auto_pop!" do
15
15
  it "routes messages to the right place" do
16
- ::ActionSubscriber::Publisher.publish("basic_push.booked", "Ohai Booked", "events")
16
+ ::ActivePublisher.publish("basic_push.booked", "Ohai Booked", "events")
17
17
 
18
18
  verify_expectation_within(2.0) do
19
19
  ::ActionSubscriber.auto_pop!
@@ -25,7 +25,7 @@ describe "A Basic Subscriber", :integration => true do
25
25
  context "ActionSubscriber.auto_subscribe!" do
26
26
  it "routes messages to the right place" do
27
27
  ::ActionSubscriber.auto_subscribe!
28
- ::ActionSubscriber::Publisher.publish("basic_push.booked", "Ohai Booked", "events")
28
+ ::ActivePublisher.publish("basic_push.booked", "Ohai Booked", "events")
29
29
 
30
30
  verify_expectation_within(2.0) do
31
31
  expect($messages).to eq(Set.new(["Ohai Booked"]))
@@ -15,7 +15,7 @@ describe "A subscriber with a custom action", :integration => true do
15
15
 
16
16
  it "routes the message to the selected action" do
17
17
  ::ActionSubscriber.auto_subscribe!
18
- ::ActionSubscriber::Publisher.publish("react.javascript_framework", "Another?!?!", "events")
18
+ ::ActivePublisher.publish("react.javascript_framework", "Another?!?!", "events")
19
19
 
20
20
  verify_expectation_within(2.0) do
21
21
  expect($messages).to eq(Set.new(["Another?!?!"]))
@@ -15,7 +15,7 @@ describe "Custom Headers Are Published and Received", :integration => true do
15
15
  let(:headers) { { "Custom" => "content/header" } }
16
16
 
17
17
  it "works for auto_pop!" do
18
- ::ActionSubscriber::Publisher.publish("pikitis.prank.pulled", "Yo Knope!", "events", :headers => headers)
18
+ ::ActivePublisher.publish("pikitis.prank.pulled", "Yo Knope!", "events", :headers => headers)
19
19
  verify_expectation_within(2.0) do
20
20
  ::ActionSubscriber.auto_pop!
21
21
  expect($messages).to eq(Set.new([headers]))
@@ -24,7 +24,7 @@ describe "Custom Headers Are Published and Received", :integration => true do
24
24
 
25
25
  it "works for auto_subscriber!" do
26
26
  ::ActionSubscriber.auto_subscribe!
27
- ::ActionSubscriber::Publisher.publish("pikitis.prank.pulled", "Yo Knope!", "events", :headers => headers)
27
+ ::ActivePublisher.publish("pikitis.prank.pulled", "Yo Knope!", "events", :headers => headers)
28
28
  verify_expectation_within(2.0) do
29
29
  expect($messages).to eq(Set.new([headers]))
30
30
  end
@@ -20,7 +20,7 @@ describe "Payload Decoding", :integration => true do
20
20
 
21
21
  it "decodes json by default" do
22
22
  ::ActionSubscriber.auto_subscribe!
23
- ::ActionSubscriber::Publisher.publish("twitter.tweet", json_string, "events", :content_type => "application/json")
23
+ ::ActivePublisher.publish("twitter.tweet", json_string, "events", :content_type => "application/json")
24
24
 
25
25
  verify_expectation_within(2.0) do
26
26
  expect($messages).to eq Set.new([{
@@ -38,7 +38,7 @@ describe "Payload Decoding", :integration => true do
38
38
 
39
39
  it "it decodes the payload using the custom decoder" do
40
40
  ::ActionSubscriber.auto_subscribe!
41
- ::ActionSubscriber::Publisher.publish("twitter.tweet", json_string, "events", :content_type => content_type)
41
+ ::ActivePublisher.publish("twitter.tweet", json_string, "events", :content_type => content_type)
42
42
 
43
43
  verify_expectation_within(2.0) do
44
44
  expect($messages).to eq Set.new([{
@@ -22,7 +22,7 @@ describe "Manual Message Acknowledgment", :integration => true do
22
22
 
23
23
  it "retries rejected messages and stops retrying acknowledged messages" do
24
24
  ::ActionSubscriber.auto_subscribe!
25
- ::ActionSubscriber::Publisher.publish("bacon.served", "BACON!", "events")
25
+ ::ActivePublisher.publish("bacon.served", "BACON!", "events")
26
26
 
27
27
  verify_expectation_within(2.0) do
28
28
  expect($messages).to eq(Set.new(["BACON!::0", "BACON!::1", "BACON!::2"]))
@@ -0,0 +1,36 @@
1
+ class MultipleConnectionsSubscriber < ::ActionSubscriber::Base
2
+ MUTEX = ::Mutex.new
3
+ at_least_once!
4
+
5
+ def burp
6
+ MUTEX.synchronize do
7
+ $messages << payload
8
+ end
9
+ end
10
+ end
11
+
12
+ describe "Separate connections to get multiple threadpools", :integration => true do
13
+ let(:draw_routes) do
14
+ ::ActionSubscriber.draw_routes do
15
+ connection(:background_work, :thread_pool_size => 20) do
16
+ route MultipleConnectionsSubscriber, :burp,
17
+ :acknowledgements => true,
18
+ :concurrency => 20
19
+ end
20
+ route MultipleConnectionsSubscriber, :burp,
21
+ :acknowledgements => true,
22
+ :concurrency => 8 # match the default threadpool size
23
+ end
24
+ end
25
+
26
+ it "spreads the load across multiple threadpools and consumer" do
27
+ ::ActionSubscriber.auto_subscribe!
28
+ 1.upto(10).each do |i|
29
+ ::ActivePublisher.publish("multiple_connections.burp", "belch#{i}", "events")
30
+ end
31
+
32
+ verify_expectation_within(5.0) do
33
+ expect($messages).to eq(Set.new(["belch1", "belch2", "belch3", "belch4", "belch5", "belch6", "belch7", "belch8", "belch9", "belch10"]))
34
+ end
35
+ end
36
+ end
@@ -18,11 +18,11 @@ describe "Separate Threadpools for Different Message", :integration => true do
18
18
  end
19
19
 
20
20
  it "processes messages in separate threadpools based on the routes" do
21
- ::ActionSubscriber.auto_subscribe!
22
- ::ActionSubscriber::Publisher.publish("different_threadpools.one", "ONE", "events")
23
- ::ActionSubscriber::Publisher.publish("different_threadpools.two", "TWO", "events")
21
+ ::ActivePublisher.publish("different_threadpools.one", "ONE", "events")
22
+ ::ActivePublisher.publish("different_threadpools.two", "TWO", "events")
24
23
 
25
24
  verify_expectation_within(2.0) do
25
+ ::ActionSubscriber.auto_pop!
26
26
  expect($messages).to eq(Set.new(["ONE","TWO"]))
27
27
  end
28
28
  end
@@ -1,17 +1,13 @@
1
1
  describe ::ActionSubscriber::Configuration do
2
2
  describe "default values" do
3
3
  specify { expect(subject.allow_low_priority_methods).to eq(false) }
4
- specify { expect(subject.async_publisher).to eq("memory") }
5
- specify { expect(subject.async_publisher_drop_messages_when_queue_full).to eq(false) }
6
- specify { expect(subject.async_publisher_max_queue_size).to eq(1_000_000) }
7
- specify { expect(subject.async_publisher_supervisor_interval).to eq(200) }
8
4
  specify { expect(subject.default_exchange).to eq("events") }
9
5
  specify { expect(subject.heartbeat).to eq(5) }
10
6
  specify { expect(subject.host).to eq("localhost") }
11
7
  specify { expect(subject.mode).to eq('subscribe') }
12
8
  specify { expect(subject.pop_interval).to eq(100) }
13
9
  specify { expect(subject.port).to eq(5672) }
14
- specify { expect(subject.prefetch).to eq(5) }
10
+ specify { expect(subject.prefetch).to eq(2) }
15
11
  specify { expect(subject.seconds_to_wait_for_graceful_shutdown).to eq(30) }
16
12
  specify { expect(subject.threadpool_size).to eq(8) }
17
13
  specify { expect(subject.timeout).to eq(1) }
@@ -19,4 +19,19 @@ describe ActionSubscriber::Middleware::ErrorHandler do
19
19
  subject.call(env)
20
20
  end
21
21
  end
22
+
23
+ context "many concurrent threads" do
24
+ it "handles the race conditions without raising exceptions" do
25
+ no_op = lambda{ nil }
26
+ threads = 1.upto(100).map do
27
+ ::Thread.new do
28
+ ::Thread.current.abort_on_exception = true
29
+ 100.times do
30
+ described_class.new(no_op).call(env)
31
+ end
32
+ end
33
+ end
34
+ threads.each(&:join)
35
+ end
36
+ end
22
37
  end