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