spec/acceptance/realtime/connection_failures_spec.rb in ably-0.8.5 vs spec/acceptance/realtime/connection_failures_spec.rb in ably-0.8.6
- old
+ new
@@ -51,23 +51,21 @@
end
end
end
context 'automatic connection retry' do
- let(:client_failure_options) { default_options.merge(log_level: :none) }
-
context 'with invalid WebSocket host' do
let(:retry_every_for_tests) { 0.2 }
let(:max_time_in_state_for_tests) { 0.6 }
- before do
- # Reconfigure client library retry periods and timeouts so that tests run quickly
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
- disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
- suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
- )
+ let(:client_failure_options) do
+ default_options.merge(
+ log_level: :none,
+ disconnected_retry_timeout: retry_every_for_tests,
+ suspended_retry_timeout: retry_every_for_tests,
+ connection_state_ttl: max_time_in_state_for_tests
+ )
end
# retry immediately after failure, then one retry every :retry_every_for_tests
let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
let(:state_changes) { Hash.new { |hash, key| hash[key] = 0 } }
@@ -108,10 +106,31 @@
expect(time_passed).to be > max_time_in_state_for_tests
stop_reactor
end
end
+ context 'for the first time' do
+ let(:client_options) do
+ default_options.merge(realtime_host: 'non.existent.host', disconnected_retry_timeout: 2, log_level: :error)
+ end
+
+ it 'reattempts connection immediately and then waits disconnected_retry_timeout for a subsequent attempt' do
+ expect(connection.defaults[:disconnected_retry_timeout]).to eql(2)
+ connection.once(:disconnected) do
+ started_at = Time.now.to_f
+ connection.once(:disconnected) do
+ expect(Time.now.to_f - started_at).to be < 1
+ started_at = Time.now.to_f
+ connection.once(:disconnected) do
+ expect(Time.now.to_f - started_at).to be > 2
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
describe '#close' do
it 'transitions connection state to :closed' do
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
connection.on(:failed) { raise 'Connection should not have reached :failed state yet' }
@@ -128,29 +147,54 @@
end
end
end
context 'when connection state is :suspended' do
- it 'enters the failed state after multiple attempts if the max_time_in_state is set' do
+ it 'stays in the suspended state after any number of reconnection attempts' do
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
connection.once(:suspended) do
count_state_changes && start_timer
- connection.on(:failed) do
- expect(connection.state).to eq(:failed)
+ EventMachine.add_timer((retry_every_for_tests + 0.1) * 10) do
+ expect(connection.state).to eq(:suspended)
- expect(state_changes[:connecting]).to eql(expected_retry_attempts)
- expect(state_changes[:suspended]).to eql(expected_retry_attempts)
+ expect(state_changes[:connecting]).to be >= 10
+ expect(state_changes[:suspended]).to be >= 10
expect(state_changes[:disconnected]).to eql(0)
- expect(time_passed).to be > max_time_in_state_for_tests
stop_reactor
end
end
end
+ context 'for the first time' do
+ let(:client_options) do
+ default_options.merge(suspended_retry_timeout: 2, connection_state_ttl: 0, log_level: :error)
+ end
+
+ it 'waits suspended_retry_timeout before attempting to reconnect' do
+ expect(client.connection.defaults[:suspended_retry_timeout]).to eql(2)
+ connection.once(:connected) do
+ connection.transition_state_machine :suspended
+ allow(connection).to receive(:current_host).and_return('does.not.exist.com')
+
+ started_at = Time.now.to_f
+ connection.once(:connecting) do
+ expect(Time.now.to_f - started_at).to be > 1.75
+ started_at = Time.now.to_f
+ connection.once(:connecting) do
+ expect(Time.now.to_f - started_at).to be > 1.75
+ connection.once(:suspended) do
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+ end
+
describe '#close' do
it 'transitions connection state to :closed' do
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
connection.once(:suspended) do
@@ -170,10 +214,14 @@
context 'when connection state is :failed' do
describe '#close' do
it 'will not transition state to :close and raises a InvalidStateChange exception' do
connection.on(:connected) { raise 'Connection should not have reached :connected state' }
+ connection.once(:suspended) do
+ connection.transition_state_machine :failed
+ end
+
connection.once(:failed) do
expect(connection.state).to eq(:failed)
expect { connection.close }.to raise_error Ably::Exceptions::InvalidStateChange, /Unable to transition from failed => closing/
stop_reactor
end
@@ -188,10 +236,14 @@
error = connection_state_change.reason
expect(connection.error_reason).to eq(error)
expect(connection.error_reason.code).to eql(80000)
stop_reactor
end
+
+ connection.once(:suspended) do |connection_state_change|
+ connection.transition_state_machine :failed, reason: connection_state_change.reason
+ end
end
end
it 'is reset to nil when :connected' do
connection.once(:disconnected) do |error|
@@ -214,64 +266,62 @@
end
end
end
describe '#connect' do
- let(:timeouts) { Ably::Realtime::Connection::ConnectionManager::TIMEOUTS }
+ let(:timeout) { 1.5 }
- before do
- stub_const 'Ably::Realtime::Connection::ConnectionManager::TIMEOUTS',
- Ably::Realtime::Connection::ConnectionManager::TIMEOUTS.merge(open: 1.5)
+ let(:client_options) do
+ default_options.merge(
+ log_level: :none,
+ realtime_request_timeout: timeout
+ )
+ end
+ before do
connection.on(:connected) { raise "Connection should not open in this test as CONNECTED ProtocolMessage is never received" }
connection.once(:connecting) do
# don't process any incoming ProtocolMessages so the connection never opens
connection.__incoming_protocol_msgbus__.unsubscribe
end
end
context 'connection opening times out' do
- let(:client_options) { client_failure_options }
-
it 'attempts to reconnect' do
started_at = Time.now
connection.once(:disconnected) do
- expect(Time.now.to_f - started_at.to_f).to be > timeouts.fetch(:open)
+ expect(Time.now.to_f - started_at.to_f).to be > timeout
connection.once(:connecting) do
stop_reactor
end
end
connection.connect
end
- it 'calls the errback of the returned Deferrable object when first connection attempt fails' do
- connection.connect.errback do |error|
- expect(connection.state).to eq(:disconnected)
- stop_reactor
- end
- end
-
context 'when retry intervals are stubbed to attempt reconnection quickly' do
- before do
- # Reconfigure client library retry periods and timeouts so that tests run quickly
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
- disconnected: { retry_every: 0.1, max_time_in_state: 0.2 },
- suspended: { retry_every: 0.1, max_time_in_state: 0.2 },
- )
+ let(:client_options) do
+ default_options.merge(
+ log_level: :error,
+ disconnected_retry_timeout: 0.1,
+ suspended_retry_timeout: 0.1,
+ connection_state_ttl: 0.2,
+ realtime_host: 'non.existent.host'
+ )
end
it 'never calls the provided success block', em_timeout: 10 do
connection.connect do
raise 'success block should not have been called'
end
- connection.once(:failed) do
- stop_reactor
+ connection.once(:suspended) do
+ connection.once(:suspended) do
+ stop_reactor
+ end
end
end
end
end
end
@@ -313,18 +363,20 @@
end
context 'and subsequently fails to reconnect' do
let(:retry_every) { 1.5 }
- before do
- # Reconfigure client library retry periods and timeouts so that tests run quickly
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
- disconnected: { retry_every: retry_every, max_time_in_state: 60 })
+ let(:client_options) do
+ default_options.merge(
+ log_level: :none,
+ disconnected_retry_timeout: retry_every,
+ suspended_retry_timeout: retry_every,
+ connection_state_ttl: 60
+ )
end
- it "retries every #{Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG[:disconnected][:retry_every]} seconds" do
+ it "retries every #{Ably::Realtime::Connection::DEFAULTS.fetch(:disconnected_retry_timeout)} seconds" do
fail_if_suspended_or_failed
stubbed_first_attempt = false
connection.once(:connected) do
@@ -380,30 +432,56 @@
end
end
end
context 'after successfully reconnecting and resuming' do
- it 'retains connection_id and connection_key' do
- previous_connection_id = nil
- previous_connection_key = nil
-
+ it 'retains connection_id and updates the connection_key' do
connection.once(:connected) do
- previous_connection_id = connection.id
- previous_connection_key = connection.key
+ previous_connection_id = connection.id
connection.transport.close_connection_after_writing
+ expect(connection).to receive(:configure_new).with(previous_connection_id, anything, anything).and_call_original
+
connection.once(:connected) do
- # Connection key left part should match new connection key left part i.e.
- # wVIsgTHAB1UvXh7z-1991d8586 becomes wVIsgTHAB1UvXh7z-1990d8586 after resume
- expect(connection.key[/^\w{5,}-/, 0]).to_not be_nil
- expect(connection.key[/^\w{5,}-/, 0]).to eql(previous_connection_key[/^\w{5,}-/, 0])
+ expect(connection.key).to_not be_nil
expect(connection.id).to eql(previous_connection_id)
stop_reactor
end
end
end
+ it 'emits any error received from Ably but leaves the channels attached' do
+ emitted_error = nil
+ channel.attach do
+ connection.transport.close_connection_after_writing
+
+ connection.once(:connecting) do
+ connection.__incoming_protocol_msgbus__.unsubscribe
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ allow(protocol_message).to receive(:error).and_return(Ably::Exceptions::Standard.new('Injected error'))
+ end
+ # Create a new message dispatcher that subscribes to ProtocolMessages after the previous subscription allowing us
+ # to modify the ProtocolMessage
+ Ably::Realtime::Client::IncomingMessageDispatcher.new(client, connection)
+ end
+
+ connection.once(:connected) do
+ EM.add_timer(0.5) do
+ expect(emitted_error).to be_a(Ably::Exceptions::Standard)
+ expect(emitted_error.message).to match(/Injected error/)
+ expect(connection.error_reason).to be_a(Ably::Exceptions::Standard)
+ expect(channel).to be_attached
+ stop_reactor
+ end
+ end
+
+ connection.once(:error) do |error|
+ emitted_error = error
+ end
+ end
+ end
+
it 'retains channel subscription state' do
messages_received = false
channel.subscribe('event') do |message|
expect(message.data).to eql('message')
@@ -525,73 +603,80 @@
end
end
end
describe 'fallback host feature' do
- let(:retry_every_for_tests) { 0.1 }
- let(:max_time_in_state_for_tests) { 0.3 }
+ let(:retry_every_for_tests) { 0.2 }
+ let(:max_time_in_state_for_tests) { 0.59 }
- before do
- # Reconfigure client library retry periods and timeouts so that tests run quickly
- stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
- Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
- disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
- suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
- )
+ let(:timeout_options) do
+ default_options.merge(
+ environment: :production,
+ log_level: :none,
+ disconnected_retry_timeout: retry_every_for_tests,
+ suspended_retry_timeout: retry_every_for_tests,
+ connection_state_ttl: max_time_in_state_for_tests
+ )
end
# Retry immediately and then wait retry_every before every subsequent attempt
- let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
+ let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
let(:retry_count_for_one_state) { 1 + expected_retry_attempts } # initial connect then disconnected
- let(:retry_count_for_all_states) { 1 + expected_retry_attempts * 2 } # initial connection, disconnected & then suspended
+ let(:retry_count_for_all_states) { 1 + expected_retry_attempts + 1 } # initial connection, disconnected & then one suspended attempt
context 'with custom realtime websocket host option' do
let(:expected_host) { 'this.host.does.not.exist' }
- let(:client_options) { default_options.merge(realtime_host: expected_host, :environment => :production, log_level: :none) }
+ let(:client_options) { timeout_options.merge(realtime_host: expected_host) }
it 'never uses a fallback host' do
expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
expect(host).to eql(expected_host)
raise EventMachine::ConnectionError
end
- connection.on(:failed) do
- stop_reactor
+ connection.once(:suspended) do
+ connection.once(:suspended) do
+ stop_reactor
+ end
end
end
end
context 'with custom realtime websocket port option' do
let(:custom_port) { 666}
- let(:client_options) { default_options.merge(tls_port: custom_port, :environment => :production, log_level: :none) }
+ let(:client_options) { timeout_options.merge(tls_port: custom_port) }
it 'never uses a fallback host' do
expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host, port|
expect(port).to eql(custom_port)
raise EventMachine::ConnectionError
end
- connection.on(:failed) do
- stop_reactor
+ connection.once(:suspended) do
+ connection.once(:suspended) do
+ stop_reactor
+ end
end
end
end
context 'with non-production environment' do
let(:environment) { 'sandbox' }
let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" }
- let(:client_options) { default_options.merge(environment: environment, log_level: :none) }
+ let(:client_options) { timeout_options.merge(environment: environment) }
it 'never uses a fallback host' do
expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
expect(host).to eql(expected_host)
raise EventMachine::ConnectionError
end
- connection.on(:failed) do
- stop_reactor
+ connection.once(:suspended) do
+ connection.once(:suspended) do
+ stop_reactor
+ end
end
end
end
context 'with production environment' do
@@ -599,11 +684,11 @@
before do
stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
end
let(:expected_host) { Ably::Realtime::Client::DOMAIN }
- let(:client_options) { default_options.merge(environment: nil, log_level: :none) }
+ let(:client_options) { timeout_options.merge(environment: nil) }
let(:fallback_hosts_used) { Array.new }
context 'when the Internet is down' do
before do
@@ -614,12 +699,14 @@
expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
expect(host).to eql(expected_host)
raise EventMachine::ConnectionError
end
- connection.on(:failed) do
- stop_reactor
+ connection.once(:suspended) do
+ connection.once(:suspended) do
+ stop_reactor
+ end
end
end
end
context 'when the Internet is up' do
@@ -627,43 +714,47 @@
allow(connection).to receive(:internet_up?).and_yield(true)
end
it 'uses a fallback host on every subsequent disconnected attempt until suspended' do
request = 0
- # Expect retry attempts + 1 attempt for the next state
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state + 1).times do |host|
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state).times do |host|
if request == 0
expect(host).to eql(expected_host)
else
fallback_hosts_used << host
end
request += 1
raise EventMachine::ConnectionError
end
- connection.on(:suspended) do
+ connection.once(:suspended) do
fallback_hosts_used.pop # remove suspended attempt host
expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
stop_reactor
end
end
it 'uses the primary host when suspended, and a fallback host on every subsequent suspended attempt' do
request = 0
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
+ expect(EventMachine).to receive(:connect).at_least(:once) do |host|
if request == 0 || request == expected_retry_attempts + 1
expect(host).to eql(expected_host)
else
expect(custom_hosts).to include(host)
- fallback_hosts_used << host
+ fallback_hosts_used << host if @suspended
end
request += 1
raise EventMachine::ConnectionError
end
- connection.on(:failed) do
- expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
- stop_reactor
+ connection.on(:suspended) do
+ @suspended ||= 0
+ @suspended += 1
+
+ if @suspended > 3
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
+ stop_reactor
+ end
end
end
end
end
end