spec/acceptance/realtime/connection_failures_spec.rb in ably-0.7.2 vs spec/acceptance/realtime/connection_failures_spec.rb in ably-0.7.4

- old
+ new

@@ -64,11 +64,12 @@ 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 }, ) end - let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round } + # 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 } } let(:timer) { Hash.new } let(:client_options) do client_failure_options.merge(realtime_host: 'non.existent.host') @@ -286,25 +287,80 @@ connection.on(:suspended) { raise 'Connection should not have reached :suspended state' } connection.on(:failed) { raise 'Connection should not have reached :failed state' } end context 'when DISCONNECTED ProtocolMessage received from the server' do - it 'reconnects automatically' do + it 'reconnects automatically and immediately' do fail_if_suspended_or_failed connection.once(:connected) do connection.once(:disconnected) do - connection.once(:connected) do - state_history = connection.state_history.map { |transition| transition[:state].to_sym } - expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected]) - stop_reactor + disconnected_at = Time.now.to_f + connection.once(:connecting) do + expect(Time.now.to_f).to be_within(0.25).of(disconnected_at) + connection.once(:connected) do + state_history = connection.state_history.map { |transition| transition[:state].to_sym } + expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected]) + stop_reactor + end end end protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i) connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message end 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 }) + end + + it "retries every CONNECT_RETRY_CONFIG[:disconnected][:retry_every] seconds" do + fail_if_suspended_or_failed + + stubbed_first_attempt = false + + connection.once(:connected) do + connection.once(:disconnected) do + connection.once(:connecting) do + connection.once(:disconnected) do + disconnected_at = Time.now.to_f + connection.once(:connecting) do + expect(Time.now.to_f - disconnected_at).to be > retry_every + state_history = connection.state_history.map { |transition| transition[:state].to_sym } + expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :disconnected, :connecting]) + + # allow one more recoonect when reactor stopped + expect(connection.manager).to receive(:reconnect_transport) + stop_reactor + end + end + + # When reconnect called simply open the transport and close immediately + expect(connection.manager).to receive(:reconnect_transport) do + next if stubbed_first_attempt + + connection.manager.setup_transport do + EventMachine.next_tick do + connection.transport.unbind + stubbed_first_attempt = true + end + end + end + end + end + + protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i) + connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message + end + end + end end context 'when websocket transport is closed' do it 'reconnects automatically' do fail_if_suspended_or_failed @@ -357,10 +413,20 @@ end end end end + it 'triggers the resume callback', api_private: true do + channel.attach do + connection.transport.close_connection_after_writing + connection.on_resume do + expect(connection).to be_connected + stop_reactor + end + end + end + context 'when messages were published whilst the client was disconnected' do it 'receives the messages published whilst offline' do messages_received = false channel.subscribe('event') do |message| @@ -392,59 +458,61 @@ end end end end - context 'when failing to resume because the connection_key is not or no longer valid' do - def kill_connection_transport_and_prevent_valid_resume - connection.transport.close_connection_after_writing - connection.configure_new '0123456789abcdef', '0123456789abcdef', -1 # force the resume connection key to be invalid - end + context 'when failing to resume' do + context 'because the connection_key is not or no longer valid' do + def kill_connection_transport_and_prevent_valid_resume + connection.transport.close_connection_after_writing + connection.configure_new '0123456789abcdef', '0123456789abcdef', -1 # force the resume connection key to be invalid + end - it 'updates the connection_id and connection_key' do - connection.once(:connected) do - previous_connection_id = connection.id - previous_connection_key = connection.key - + it 'updates the connection_id and connection_key' do connection.once(:connected) do - expect(connection.key).to_not eql(previous_connection_key) - expect(connection.id).to_not eql(previous_connection_id) - stop_reactor - end + previous_connection_id = connection.id + previous_connection_key = connection.key - kill_connection_transport_and_prevent_valid_resume - end - end - - it 'detaches all channels' do - channel_count = 10 - channels = channel_count.times.map { |index| client.channel("channel-#{index}") } - when_all(*channels.map(&:attach)) do - detached_channels = [] - channels.each do |channel| - channel.on(:detached) do - detached_channels << channel - next unless detached_channels.count == channel_count - expect(detached_channels.count).to eql(channel_count) + connection.once(:connected) do + expect(connection.key).to_not eql(previous_connection_key) + expect(connection.id).to_not eql(previous_connection_id) stop_reactor end - end - kill_connection_transport_and_prevent_valid_resume + kill_connection_transport_and_prevent_valid_resume + end end - end - it 'emits an error on the channel and sets the error reason' do - client.channel(random_str).attach do |channel| - channel.on(:error) do |error| - expect(error.message).to match(/Invalid connection key/i) - expect(error.code).to eql(80008) - expect(channel.error_reason).to eql(error) - stop_reactor + it 'detaches all channels' do + channel_count = 10 + channels = channel_count.times.map { |index| client.channel("channel-#{index}") } + when_all(*channels.map(&:attach)) do + detached_channels = [] + channels.each do |channel| + channel.on(:detached) do + detached_channels << channel + next unless detached_channels.count == channel_count + expect(detached_channels.count).to eql(channel_count) + stop_reactor + end + end + + kill_connection_transport_and_prevent_valid_resume end + end - kill_connection_transport_and_prevent_valid_resume + it 'emits an error on the channel and sets the error reason' do + client.channel(random_str).attach do |channel| + channel.on(:error) do |error| + expect(error.message).to match(/Invalid connection key/i) + expect(error.code).to eql(80008) + expect(channel.error_reason).to eql(error) + stop_reactor + end + + kill_connection_transport_and_prevent_valid_resume + end end end end end @@ -459,10 +527,12 @@ 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 }, ) end - let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round } + # 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(: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 context 'with custom realtime websocket host option' do let(:expected_host) { 'this.host.does.not.exist' }