ably 0.8.5 → 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +42 -48
  4. data/SPEC.md +1099 -640
  5. data/ably.gemspec +10 -4
  6. data/lib/ably/auth.rb +155 -47
  7. data/lib/ably/exceptions.rb +2 -0
  8. data/lib/ably/models/channel_state_change.rb +2 -3
  9. data/lib/ably/models/connection_details.rb +54 -0
  10. data/lib/ably/models/protocol_message.rb +14 -4
  11. data/lib/ably/models/token_details.rb +13 -7
  12. data/lib/ably/models/token_request.rb +1 -2
  13. data/lib/ably/modules/ably.rb +3 -2
  14. data/lib/ably/modules/message_emitter.rb +1 -3
  15. data/lib/ably/modules/state_emitter.rb +2 -2
  16. data/lib/ably/realtime/auth.rb +6 -0
  17. data/lib/ably/realtime/channel/channel_manager.rb +2 -0
  18. data/lib/ably/realtime/channel.rb +15 -4
  19. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  20. data/lib/ably/realtime/client.rb +10 -3
  21. data/lib/ably/realtime/connection/connection_manager.rb +58 -54
  22. data/lib/ably/realtime/connection.rb +62 -6
  23. data/lib/ably/realtime/presence.rb +18 -5
  24. data/lib/ably/rest/channel.rb +9 -1
  25. data/lib/ably/rest/client.rb +32 -14
  26. data/lib/ably/rest/presence.rb +1 -1
  27. data/lib/ably/version.rb +1 -1
  28. data/lib/ably.rb +2 -0
  29. data/spec/acceptance/realtime/auth_spec.rb +251 -11
  30. data/spec/acceptance/realtime/channel_history_spec.rb +12 -2
  31. data/spec/acceptance/realtime/channel_spec.rb +316 -24
  32. data/spec/acceptance/realtime/client_spec.rb +93 -1
  33. data/spec/acceptance/realtime/connection_failures_spec.rb +177 -86
  34. data/spec/acceptance/realtime/connection_spec.rb +284 -60
  35. data/spec/acceptance/realtime/message_spec.rb +45 -6
  36. data/spec/acceptance/realtime/presence_history_spec.rb +4 -0
  37. data/spec/acceptance/realtime/presence_spec.rb +181 -49
  38. data/spec/acceptance/realtime/time_spec.rb +13 -0
  39. data/spec/acceptance/rest/auth_spec.rb +222 -4
  40. data/spec/acceptance/rest/channel_spec.rb +132 -1
  41. data/spec/acceptance/rest/client_spec.rb +129 -28
  42. data/spec/acceptance/rest/presence_spec.rb +7 -7
  43. data/spec/acceptance/rest/time_spec.rb +10 -0
  44. data/spec/shared/client_initializer_behaviour.rb +41 -17
  45. data/spec/spec_helper.rb +1 -0
  46. data/spec/support/debug_failure_helper.rb +16 -0
  47. data/spec/unit/models/connection_details_spec.rb +60 -0
  48. data/spec/unit/models/protocol_message_spec.rb +45 -0
  49. data/spec/unit/modules/event_emitter_spec.rb +3 -1
  50. data/spec/unit/realtime/channel_spec.rb +6 -5
  51. data/spec/unit/realtime/client_spec.rb +5 -1
  52. data/spec/unit/realtime/connection_spec.rb +5 -1
  53. data/spec/unit/realtime/realtime_spec.rb +5 -1
  54. metadata +54 -7
@@ -53,19 +53,17 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
53
53
  end
54
54
 
55
55
  context 'automatic connection retry' do
56
- let(:client_failure_options) { default_options.merge(log_level: :none) }
57
-
58
56
  context 'with invalid WebSocket host' do
59
57
  let(:retry_every_for_tests) { 0.2 }
60
58
  let(:max_time_in_state_for_tests) { 0.6 }
61
59
 
62
- before do
63
- # Reconfigure client library retry periods and timeouts so that tests run quickly
64
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
65
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
66
- disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
67
- suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
68
- )
60
+ let(:client_failure_options) do
61
+ default_options.merge(
62
+ log_level: :none,
63
+ disconnected_retry_timeout: retry_every_for_tests,
64
+ suspended_retry_timeout: retry_every_for_tests,
65
+ connection_state_ttl: max_time_in_state_for_tests
66
+ )
69
67
  end
70
68
 
71
69
  # retry immediately after failure, then one retry every :retry_every_for_tests
@@ -110,6 +108,27 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
110
108
  end
111
109
  end
112
110
 
111
+ context 'for the first time' do
112
+ let(:client_options) do
113
+ default_options.merge(realtime_host: 'non.existent.host', disconnected_retry_timeout: 2, log_level: :error)
114
+ end
115
+
116
+ it 'reattempts connection immediately and then waits disconnected_retry_timeout for a subsequent attempt' do
117
+ expect(connection.defaults[:disconnected_retry_timeout]).to eql(2)
118
+ connection.once(:disconnected) do
119
+ started_at = Time.now.to_f
120
+ connection.once(:disconnected) do
121
+ expect(Time.now.to_f - started_at).to be < 1
122
+ started_at = Time.now.to_f
123
+ connection.once(:disconnected) do
124
+ expect(Time.now.to_f - started_at).to be > 2
125
+ stop_reactor
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
113
132
  describe '#close' do
114
133
  it 'transitions connection state to :closed' do
115
134
  connection.on(:connected) { raise 'Connection should not have reached :connected state' }
@@ -130,25 +149,50 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
130
149
  end
131
150
 
132
151
  context 'when connection state is :suspended' do
133
- it 'enters the failed state after multiple attempts if the max_time_in_state is set' do
152
+ it 'stays in the suspended state after any number of reconnection attempts' do
134
153
  connection.on(:connected) { raise 'Connection should not have reached :connected state' }
135
154
 
136
155
  connection.once(:suspended) do
137
156
  count_state_changes && start_timer
138
157
 
139
- connection.on(:failed) do
140
- expect(connection.state).to eq(:failed)
158
+ EventMachine.add_timer((retry_every_for_tests + 0.1) * 10) do
159
+ expect(connection.state).to eq(:suspended)
141
160
 
142
- expect(state_changes[:connecting]).to eql(expected_retry_attempts)
143
- expect(state_changes[:suspended]).to eql(expected_retry_attempts)
161
+ expect(state_changes[:connecting]).to be >= 10
162
+ expect(state_changes[:suspended]).to be >= 10
144
163
  expect(state_changes[:disconnected]).to eql(0)
145
164
 
146
- expect(time_passed).to be > max_time_in_state_for_tests
147
165
  stop_reactor
148
166
  end
149
167
  end
150
168
  end
151
169
 
170
+ context 'for the first time' do
171
+ let(:client_options) do
172
+ default_options.merge(suspended_retry_timeout: 2, connection_state_ttl: 0, log_level: :error)
173
+ end
174
+
175
+ it 'waits suspended_retry_timeout before attempting to reconnect' do
176
+ expect(client.connection.defaults[:suspended_retry_timeout]).to eql(2)
177
+ connection.once(:connected) do
178
+ connection.transition_state_machine :suspended
179
+ allow(connection).to receive(:current_host).and_return('does.not.exist.com')
180
+
181
+ started_at = Time.now.to_f
182
+ connection.once(:connecting) do
183
+ expect(Time.now.to_f - started_at).to be > 1.75
184
+ started_at = Time.now.to_f
185
+ connection.once(:connecting) do
186
+ expect(Time.now.to_f - started_at).to be > 1.75
187
+ connection.once(:suspended) do
188
+ stop_reactor
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+
152
196
  describe '#close' do
153
197
  it 'transitions connection state to :closed' do
154
198
  connection.on(:connected) { raise 'Connection should not have reached :connected state' }
@@ -172,6 +216,10 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
172
216
  it 'will not transition state to :close and raises a InvalidStateChange exception' do
173
217
  connection.on(:connected) { raise 'Connection should not have reached :connected state' }
174
218
 
219
+ connection.once(:suspended) do
220
+ connection.transition_state_machine :failed
221
+ end
222
+
175
223
  connection.once(:failed) do
176
224
  expect(connection.state).to eq(:failed)
177
225
  expect { connection.close }.to raise_error Ably::Exceptions::InvalidStateChange, /Unable to transition from failed => closing/
@@ -190,6 +238,10 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
190
238
  expect(connection.error_reason.code).to eql(80000)
191
239
  stop_reactor
192
240
  end
241
+
242
+ connection.once(:suspended) do |connection_state_change|
243
+ connection.transition_state_machine :failed, reason: connection_state_change.reason
244
+ end
193
245
  end
194
246
  end
195
247
 
@@ -216,12 +268,16 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
216
268
  end
217
269
 
218
270
  describe '#connect' do
219
- let(:timeouts) { Ably::Realtime::Connection::ConnectionManager::TIMEOUTS }
271
+ let(:timeout) { 1.5 }
220
272
 
221
- before do
222
- stub_const 'Ably::Realtime::Connection::ConnectionManager::TIMEOUTS',
223
- Ably::Realtime::Connection::ConnectionManager::TIMEOUTS.merge(open: 1.5)
273
+ let(:client_options) do
274
+ default_options.merge(
275
+ log_level: :none,
276
+ realtime_request_timeout: timeout
277
+ )
278
+ end
224
279
 
280
+ before do
225
281
  connection.on(:connected) { raise "Connection should not open in this test as CONNECTED ProtocolMessage is never received" }
226
282
 
227
283
  connection.once(:connecting) do
@@ -231,13 +287,11 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
231
287
  end
232
288
 
233
289
  context 'connection opening times out' do
234
- let(:client_options) { client_failure_options }
235
-
236
290
  it 'attempts to reconnect' do
237
291
  started_at = Time.now
238
292
 
239
293
  connection.once(:disconnected) do
240
- expect(Time.now.to_f - started_at.to_f).to be > timeouts.fetch(:open)
294
+ expect(Time.now.to_f - started_at.to_f).to be > timeout
241
295
  connection.once(:connecting) do
242
296
  stop_reactor
243
297
  end
@@ -246,21 +300,15 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
246
300
  connection.connect
247
301
  end
248
302
 
249
- it 'calls the errback of the returned Deferrable object when first connection attempt fails' do
250
- connection.connect.errback do |error|
251
- expect(connection.state).to eq(:disconnected)
252
- stop_reactor
253
- end
254
- end
255
-
256
303
  context 'when retry intervals are stubbed to attempt reconnection quickly' do
257
- before do
258
- # Reconfigure client library retry periods and timeouts so that tests run quickly
259
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
260
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
261
- disconnected: { retry_every: 0.1, max_time_in_state: 0.2 },
262
- suspended: { retry_every: 0.1, max_time_in_state: 0.2 },
263
- )
304
+ let(:client_options) do
305
+ default_options.merge(
306
+ log_level: :error,
307
+ disconnected_retry_timeout: 0.1,
308
+ suspended_retry_timeout: 0.1,
309
+ connection_state_ttl: 0.2,
310
+ realtime_host: 'non.existent.host'
311
+ )
264
312
  end
265
313
 
266
314
  it 'never calls the provided success block', em_timeout: 10 do
@@ -268,8 +316,10 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
268
316
  raise 'success block should not have been called'
269
317
  end
270
318
 
271
- connection.once(:failed) do
272
- stop_reactor
319
+ connection.once(:suspended) do
320
+ connection.once(:suspended) do
321
+ stop_reactor
322
+ end
273
323
  end
274
324
  end
275
325
  end
@@ -315,14 +365,16 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
315
365
  context 'and subsequently fails to reconnect' do
316
366
  let(:retry_every) { 1.5 }
317
367
 
318
- before do
319
- # Reconfigure client library retry periods and timeouts so that tests run quickly
320
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
321
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
322
- disconnected: { retry_every: retry_every, max_time_in_state: 60 })
368
+ let(:client_options) do
369
+ default_options.merge(
370
+ log_level: :none,
371
+ disconnected_retry_timeout: retry_every,
372
+ suspended_retry_timeout: retry_every,
373
+ connection_state_ttl: 60
374
+ )
323
375
  end
324
376
 
325
- it "retries every #{Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG[:disconnected][:retry_every]} seconds" do
377
+ it "retries every #{Ably::Realtime::Connection::DEFAULTS.fetch(:disconnected_retry_timeout)} seconds" do
326
378
  fail_if_suspended_or_failed
327
379
 
328
380
  stubbed_first_attempt = false
@@ -382,26 +434,52 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
382
434
  end
383
435
 
384
436
  context 'after successfully reconnecting and resuming' do
385
- it 'retains connection_id and connection_key' do
386
- previous_connection_id = nil
387
- previous_connection_key = nil
388
-
437
+ it 'retains connection_id and updates the connection_key' do
389
438
  connection.once(:connected) do
390
- previous_connection_id = connection.id
391
- previous_connection_key = connection.key
439
+ previous_connection_id = connection.id
392
440
  connection.transport.close_connection_after_writing
393
441
 
442
+ expect(connection).to receive(:configure_new).with(previous_connection_id, anything, anything).and_call_original
443
+
394
444
  connection.once(:connected) do
395
- # Connection key left part should match new connection key left part i.e.
396
- # wVIsgTHAB1UvXh7z-1991d8586 becomes wVIsgTHAB1UvXh7z-1990d8586 after resume
397
- expect(connection.key[/^\w{5,}-/, 0]).to_not be_nil
398
- expect(connection.key[/^\w{5,}-/, 0]).to eql(previous_connection_key[/^\w{5,}-/, 0])
445
+ expect(connection.key).to_not be_nil
399
446
  expect(connection.id).to eql(previous_connection_id)
400
447
  stop_reactor
401
448
  end
402
449
  end
403
450
  end
404
451
 
452
+ it 'emits any error received from Ably but leaves the channels attached' do
453
+ emitted_error = nil
454
+ channel.attach do
455
+ connection.transport.close_connection_after_writing
456
+
457
+ connection.once(:connecting) do
458
+ connection.__incoming_protocol_msgbus__.unsubscribe
459
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
460
+ allow(protocol_message).to receive(:error).and_return(Ably::Exceptions::Standard.new('Injected error'))
461
+ end
462
+ # Create a new message dispatcher that subscribes to ProtocolMessages after the previous subscription allowing us
463
+ # to modify the ProtocolMessage
464
+ Ably::Realtime::Client::IncomingMessageDispatcher.new(client, connection)
465
+ end
466
+
467
+ connection.once(:connected) do
468
+ EM.add_timer(0.5) do
469
+ expect(emitted_error).to be_a(Ably::Exceptions::Standard)
470
+ expect(emitted_error.message).to match(/Injected error/)
471
+ expect(connection.error_reason).to be_a(Ably::Exceptions::Standard)
472
+ expect(channel).to be_attached
473
+ stop_reactor
474
+ end
475
+ end
476
+
477
+ connection.once(:error) do |error|
478
+ emitted_error = error
479
+ end
480
+ end
481
+ end
482
+
405
483
  it 'retains channel subscription state' do
406
484
  messages_received = false
407
485
 
@@ -527,27 +605,28 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
527
605
  end
528
606
 
529
607
  describe 'fallback host feature' do
530
- let(:retry_every_for_tests) { 0.1 }
531
- let(:max_time_in_state_for_tests) { 0.3 }
532
-
533
- before do
534
- # Reconfigure client library retry periods and timeouts so that tests run quickly
535
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
536
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
537
- disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
538
- suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
539
- )
608
+ let(:retry_every_for_tests) { 0.2 }
609
+ let(:max_time_in_state_for_tests) { 0.59 }
610
+
611
+ let(:timeout_options) do
612
+ default_options.merge(
613
+ environment: :production,
614
+ log_level: :none,
615
+ disconnected_retry_timeout: retry_every_for_tests,
616
+ suspended_retry_timeout: retry_every_for_tests,
617
+ connection_state_ttl: max_time_in_state_for_tests
618
+ )
540
619
  end
541
620
 
542
621
  # Retry immediately and then wait retry_every before every subsequent attempt
543
- let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
622
+ let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
544
623
 
545
624
  let(:retry_count_for_one_state) { 1 + expected_retry_attempts } # initial connect then disconnected
546
- let(:retry_count_for_all_states) { 1 + expected_retry_attempts * 2 } # initial connection, disconnected & then suspended
625
+ let(:retry_count_for_all_states) { 1 + expected_retry_attempts + 1 } # initial connection, disconnected & then one suspended attempt
547
626
 
548
627
  context 'with custom realtime websocket host option' do
549
628
  let(:expected_host) { 'this.host.does.not.exist' }
550
- let(:client_options) { default_options.merge(realtime_host: expected_host, :environment => :production, log_level: :none) }
629
+ let(:client_options) { timeout_options.merge(realtime_host: expected_host) }
551
630
 
552
631
  it 'never uses a fallback host' do
553
632
  expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
@@ -555,15 +634,17 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
555
634
  raise EventMachine::ConnectionError
556
635
  end
557
636
 
558
- connection.on(:failed) do
559
- stop_reactor
637
+ connection.once(:suspended) do
638
+ connection.once(:suspended) do
639
+ stop_reactor
640
+ end
560
641
  end
561
642
  end
562
643
  end
563
644
 
564
645
  context 'with custom realtime websocket port option' do
565
646
  let(:custom_port) { 666}
566
- let(:client_options) { default_options.merge(tls_port: custom_port, :environment => :production, log_level: :none) }
647
+ let(:client_options) { timeout_options.merge(tls_port: custom_port) }
567
648
 
568
649
  it 'never uses a fallback host' do
569
650
  expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host, port|
@@ -571,8 +652,10 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
571
652
  raise EventMachine::ConnectionError
572
653
  end
573
654
 
574
- connection.on(:failed) do
575
- stop_reactor
655
+ connection.once(:suspended) do
656
+ connection.once(:suspended) do
657
+ stop_reactor
658
+ end
576
659
  end
577
660
  end
578
661
  end
@@ -580,7 +663,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
580
663
  context 'with non-production environment' do
581
664
  let(:environment) { 'sandbox' }
582
665
  let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" }
583
- let(:client_options) { default_options.merge(environment: environment, log_level: :none) }
666
+ let(:client_options) { timeout_options.merge(environment: environment) }
584
667
 
585
668
  it 'never uses a fallback host' do
586
669
  expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
@@ -588,8 +671,10 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
588
671
  raise EventMachine::ConnectionError
589
672
  end
590
673
 
591
- connection.on(:failed) do
592
- stop_reactor
674
+ connection.once(:suspended) do
675
+ connection.once(:suspended) do
676
+ stop_reactor
677
+ end
593
678
  end
594
679
  end
595
680
  end
@@ -601,7 +686,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
601
686
  end
602
687
 
603
688
  let(:expected_host) { Ably::Realtime::Client::DOMAIN }
604
- let(:client_options) { default_options.merge(environment: nil, log_level: :none) }
689
+ let(:client_options) { timeout_options.merge(environment: nil) }
605
690
 
606
691
  let(:fallback_hosts_used) { Array.new }
607
692
 
@@ -616,8 +701,10 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
616
701
  raise EventMachine::ConnectionError
617
702
  end
618
703
 
619
- connection.on(:failed) do
620
- stop_reactor
704
+ connection.once(:suspended) do
705
+ connection.once(:suspended) do
706
+ stop_reactor
707
+ end
621
708
  end
622
709
  end
623
710
  end
@@ -629,8 +716,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
629
716
 
630
717
  it 'uses a fallback host on every subsequent disconnected attempt until suspended' do
631
718
  request = 0
632
- # Expect retry attempts + 1 attempt for the next state
633
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state + 1).times do |host|
719
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state).times do |host|
634
720
  if request == 0
635
721
  expect(host).to eql(expected_host)
636
722
  else
@@ -640,7 +726,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
640
726
  raise EventMachine::ConnectionError
641
727
  end
642
728
 
643
- connection.on(:suspended) do
729
+ connection.once(:suspended) do
644
730
  fallback_hosts_used.pop # remove suspended attempt host
645
731
  expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
646
732
  stop_reactor
@@ -649,20 +735,25 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
649
735
 
650
736
  it 'uses the primary host when suspended, and a fallback host on every subsequent suspended attempt' do
651
737
  request = 0
652
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
738
+ expect(EventMachine).to receive(:connect).at_least(:once) do |host|
653
739
  if request == 0 || request == expected_retry_attempts + 1
654
740
  expect(host).to eql(expected_host)
655
741
  else
656
742
  expect(custom_hosts).to include(host)
657
- fallback_hosts_used << host
743
+ fallback_hosts_used << host if @suspended
658
744
  end
659
745
  request += 1
660
746
  raise EventMachine::ConnectionError
661
747
  end
662
748
 
663
- connection.on(:failed) do
664
- expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
665
- stop_reactor
749
+ connection.on(:suspended) do
750
+ @suspended ||= 0
751
+ @suspended += 1
752
+
753
+ if @suspended > 3
754
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
755
+ stop_reactor
756
+ end
666
757
  end
667
758
  end
668
759
  end