action_subscriber 2.5.0.pre-java → 3.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
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