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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -2
- data/README.md +15 -23
- data/action_subscriber.gemspec +1 -0
- data/lib/action_subscriber.rb +11 -23
- data/lib/action_subscriber/babou.rb +2 -29
- data/lib/action_subscriber/base.rb +0 -4
- data/lib/action_subscriber/bunny/subscriber.rb +18 -4
- data/lib/action_subscriber/configuration.rb +1 -11
- data/lib/action_subscriber/default_routing.rb +6 -4
- data/lib/action_subscriber/march_hare/subscriber.rb +20 -4
- data/lib/action_subscriber/message_retry.rb +1 -1
- data/lib/action_subscriber/middleware/env.rb +3 -1
- data/lib/action_subscriber/middleware/error_handler.rb +20 -4
- data/lib/action_subscriber/rabbit_connection.rb +24 -33
- data/lib/action_subscriber/route.rb +5 -1
- data/lib/action_subscriber/route_set.rb +13 -6
- data/lib/action_subscriber/router.rb +15 -3
- data/lib/action_subscriber/version.rb +1 -1
- data/spec/integration/around_filters_spec.rb +1 -1
- data/spec/integration/at_least_once_spec.rb +1 -1
- data/spec/integration/at_most_once_spec.rb +1 -1
- data/spec/integration/automatic_reconnect_spec.rb +3 -4
- data/spec/integration/basic_subscriber_spec.rb +2 -2
- data/spec/integration/custom_actions_spec.rb +1 -1
- data/spec/integration/custom_headers_spec.rb +2 -2
- data/spec/integration/decoding_payloads_spec.rb +2 -2
- data/spec/integration/manual_acknowledgement_spec.rb +1 -1
- data/spec/integration/multiple_connections_spec.rb +36 -0
- data/spec/integration/multiple_threadpools_spec.rb +3 -3
- data/spec/lib/action_subscriber/configuration_spec.rb +1 -5
- data/spec/lib/action_subscriber/middleware/error_handler_spec.rb +15 -0
- data/spec/spec_helper.rb +8 -4
- metadata +21 -16
- data/lib/action_subscriber/publisher.rb +0 -46
- data/lib/action_subscriber/publisher/async.rb +0 -31
- data/lib/action_subscriber/publisher/async/in_memory_adapter.rb +0 -153
- data/spec/integration/inferred_routes_spec.rb +0 -53
- data/spec/lib/action_subscriber/publisher/async/in_memory_adapter_spec.rb +0 -135
- data/spec/lib/action_subscriber/publisher/async_spec.rb +0 -40
- 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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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.
|
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
|
-
|
16
|
-
|
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.
|
15
|
+
def self.subscriber_connected?
|
21
16
|
SUBSCRIBER_CONNECTION_MUTEX.synchronize do
|
22
|
-
|
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.
|
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
|
-
|
37
|
-
@
|
23
|
+
subscriber_connections.each{|_name, connection| connection.close}
|
24
|
+
@subscriber_connections = {}
|
38
25
|
end
|
39
26
|
end
|
40
27
|
|
41
|
-
def self.
|
28
|
+
def self.with_connection(name)
|
42
29
|
SUBSCRIBER_CONNECTION_MUTEX.synchronize do
|
43
|
-
|
44
|
-
|
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(
|
39
|
+
connection = ::MarchHare.connect(options)
|
55
40
|
else
|
56
|
-
connection = ::Bunny.new(
|
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
|
-
:
|
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
|
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
|
-
|
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
|
24
|
-
@
|
29
|
+
def subscriptions
|
30
|
+
@subscriptions ||= []
|
25
31
|
end
|
26
32
|
|
27
33
|
def setup_queue(route)
|
28
|
-
channel = ::ActionSubscriber::RabbitConnection.
|
34
|
+
channel = ::ActionSubscriber::RabbitConnection.with_connection(route.connection_name){ |connection| connection.create_channel }
|
29
35
|
exchange = channel.topic(route.exchange)
|
30
|
-
|
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
|
-
|
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)
|
@@ -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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
28
|
+
expect(::ActionSubscriber::RabbitConnection.with_connection(:default){|connection| connection.open?}).to eq(true)
|
30
29
|
end
|
31
30
|
|
32
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
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
|
-
::
|
22
|
-
::
|
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(
|
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
|