ably 1.1.0 → 1.1.4

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +27 -0
  3. data/CHANGELOG.md +68 -2
  4. data/COPYRIGHT +1 -0
  5. data/LICENSE +172 -11
  6. data/MAINTAINERS.md +1 -0
  7. data/README.md +3 -7
  8. data/SPEC.md +944 -914
  9. data/ably.gemspec +7 -7
  10. data/lib/ably/auth.rb +12 -2
  11. data/lib/ably/exceptions.rb +2 -2
  12. data/lib/ably/logger.rb +7 -1
  13. data/lib/ably/modules/state_machine.rb +1 -1
  14. data/lib/ably/realtime/channel.rb +7 -11
  15. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  16. data/lib/ably/realtime/channel/channel_properties.rb +24 -0
  17. data/lib/ably/realtime/client.rb +12 -3
  18. data/lib/ably/realtime/connection.rb +31 -19
  19. data/lib/ably/realtime/connection/connection_manager.rb +19 -3
  20. data/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  21. data/lib/ably/realtime/presence.rb +0 -14
  22. data/lib/ably/rest/channel.rb +25 -17
  23. data/lib/ably/rest/client.rb +22 -11
  24. data/lib/ably/version.rb +1 -1
  25. data/spec/acceptance/realtime/auth_spec.rb +16 -13
  26. data/spec/acceptance/realtime/channel_history_spec.rb +26 -20
  27. data/spec/acceptance/realtime/channel_spec.rb +21 -8
  28. data/spec/acceptance/realtime/client_spec.rb +80 -20
  29. data/spec/acceptance/realtime/connection_failures_spec.rb +71 -5
  30. data/spec/acceptance/realtime/connection_spec.rb +153 -26
  31. data/spec/acceptance/realtime/message_spec.rb +17 -17
  32. data/spec/acceptance/realtime/presence_history_spec.rb +0 -58
  33. data/spec/acceptance/realtime/presence_spec.rb +250 -162
  34. data/spec/acceptance/realtime/push_admin_spec.rb +49 -25
  35. data/spec/acceptance/rest/auth_spec.rb +6 -75
  36. data/spec/acceptance/rest/channel_spec.rb +79 -4
  37. data/spec/acceptance/rest/channels_spec.rb +6 -0
  38. data/spec/acceptance/rest/client_spec.rb +72 -12
  39. data/spec/acceptance/rest/message_spec.rb +8 -27
  40. data/spec/acceptance/rest/push_admin_spec.rb +67 -27
  41. data/spec/shared/client_initializer_behaviour.rb +0 -8
  42. data/spec/spec_helper.rb +2 -1
  43. data/spec/support/debug_failure_helper.rb +9 -5
  44. data/spec/support/serialization_helper.rb +21 -0
  45. data/spec/support/test_app.rb +2 -2
  46. data/spec/unit/modules/enum_spec.rb +1 -1
  47. data/spec/unit/realtime/client_spec.rb +20 -7
  48. data/spec/unit/realtime/connection_spec.rb +1 -1
  49. metadata +40 -29
  50. data/.travis.yml +0 -16
data/ably.gemspec CHANGED
@@ -20,33 +20,33 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_runtime_dependency 'eventmachine', '~> 1.2.6'
22
22
  spec.add_runtime_dependency 'em-http-request', '~> 1.1'
23
- spec.add_runtime_dependency 'statesman', '~> 1.0.0'
24
- spec.add_runtime_dependency 'faraday', '~> 0.12'
23
+ spec.add_runtime_dependency 'statesman', '~> 7.4'
24
+ spec.add_runtime_dependency 'faraday', '>= 0.12', '< 2.0.0'
25
25
  spec.add_runtime_dependency 'excon', '~> 0.55'
26
26
 
27
- if RUBY_VERSION.match(/^1/)
27
+ if RUBY_VERSION.match(/^1\./)
28
28
  spec.add_runtime_dependency 'json', '< 2.0'
29
29
  else
30
30
  spec.add_runtime_dependency 'json'
31
31
  end
32
32
  spec.add_runtime_dependency 'websocket-driver', '~> 0.7'
33
- spec.add_runtime_dependency 'msgpack', '>= 0.6.2'
33
+ spec.add_runtime_dependency 'msgpack', '>= 1.3.0'
34
34
  spec.add_runtime_dependency 'addressable', '>= 2.0.0'
35
35
 
36
- spec.add_development_dependency 'bundler', '~> 1.3'
37
36
  spec.add_development_dependency 'rake', '~> 11.3'
38
37
  spec.add_development_dependency 'redcarpet', '~> 3.3'
39
38
  spec.add_development_dependency 'rspec', '~> 3.3.0' # version lock, see config.around(:example, :event_machine) in event_machine_helper.rb
40
39
  spec.add_development_dependency 'rspec-retry', '~> 0.6'
41
40
  spec.add_development_dependency 'yard', '~> 0.9'
42
41
  spec.add_development_dependency 'rspec-instafail', '~> 1.0'
42
+ spec.add_development_dependency 'bundler', '>= 1.3.0'
43
43
 
44
- if RUBY_VERSION.match(/^1/)
44
+ if RUBY_VERSION.match(/^1\./)
45
45
  spec.add_development_dependency 'public_suffix', '~> 1.4.6' # Later versions do not support Ruby 1.9
46
46
  spec.add_development_dependency 'webmock', '2.2'
47
47
  spec.add_development_dependency 'parallel_tests', '~> 2.9.0'
48
48
  else
49
- spec.add_development_dependency 'webmock', '~> 2.2'
49
+ spec.add_development_dependency 'webmock', '~> 3.11'
50
50
  spec.add_development_dependency 'coveralls'
51
51
  spec.add_development_dependency 'parallel_tests', '~> 2.22'
52
52
  if !RUBY_VERSION.match(/^2\.[0123]/)
data/lib/ably/auth.rb CHANGED
@@ -103,7 +103,6 @@ module Ably
103
103
  end
104
104
 
105
105
  if has_client_id? && !token_creatable_externally? && !token_option
106
- raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present?
107
106
  @client_id = ensure_utf_8(:client_id, client_id) if client_id
108
107
  end
109
108
 
@@ -377,7 +376,7 @@ module Ably
377
376
  # True when Token Auth is being used to authenticate with Ably
378
377
  def using_token_auth?
379
378
  return options[:use_token_auth] if options.has_key?(:use_token_auth)
380
- !!(token_option || current_token_details || has_client_id? || token_creatable_externally?)
379
+ !!(token_option || current_token_details || token_creatable_externally?)
381
380
  end
382
381
 
383
382
  def client_id
@@ -408,6 +407,17 @@ module Ably
408
407
  end
409
408
  end
410
409
 
410
+ # Extra headers that may be used during authentication
411
+ #
412
+ # @return [Hash] headers
413
+ def extra_auth_headers
414
+ if client_id && using_basic_auth?
415
+ { 'X-Ably-ClientId' => Base64.urlsafe_encode64(client_id) }
416
+ else
417
+ {}
418
+ end
419
+ end
420
+
411
421
  # Auth params used in URI endpoint for Realtime connections
412
422
  # Will reauthorize implicitly if required and capable
413
423
  #
@@ -5,7 +5,7 @@ module Ably
5
5
  TOKEN_EXPIRED_CODE = 40140..40149
6
6
 
7
7
  # Base Ably exception class that contains status and code values used by Ably
8
- # Refer to https://github.com/ably/ably-common/blob/master/protocol/errors.json
8
+ # Refer to https://github.com/ably/ably-common/blob/main/protocol/errors.json
9
9
  #
10
10
  # @!attribute [r] message
11
11
  # @return [String] Error message from Ably
@@ -116,7 +116,7 @@ module Ably
116
116
  class InvalidState < BaseAblyException; end
117
117
 
118
118
  # A generic Ably exception taht supports a status & code.
119
- # See https://github.com/ably/ably-common/blob/master/protocol/errors.json for a list of Ably errors
119
+ # See https://github.com/ably/ably-common/blob/main/protocol/errors.json for a list of Ably errors
120
120
  class Standard < BaseAblyException; end
121
121
 
122
122
  # The HTTP request has returned a 500 error
data/lib/ably/logger.rb CHANGED
@@ -20,6 +20,8 @@ module Ably
20
20
  ensure_logger_interface_is_valid
21
21
 
22
22
  @logger.level = log_level
23
+
24
+ @log_mutex = Mutex.new
23
25
  end
24
26
 
25
27
  # The logger used by this class, defaults to {http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html Ruby Logger}
@@ -38,7 +40,9 @@ module Ably
38
40
  %w(fatal error warn info debug).each do |method_name|
39
41
  define_method(method_name) do |*args, &block|
40
42
  begin
41
- logger.public_send(method_name, *args, &block)
43
+ log_mutex.synchronize do
44
+ logger.public_send(method_name, *args, &block)
45
+ end
42
46
  rescue StandardError => e
43
47
  logger.error "Logger: Failed to log #{method_name} block - #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
44
48
  end
@@ -46,6 +50,8 @@ module Ably
46
50
  end
47
51
 
48
52
  private
53
+ attr_reader :log_mutex
54
+
49
55
  def client
50
56
  @client
51
57
  end
@@ -23,7 +23,7 @@ module Ably::Modules
23
23
  def transition_state(state, *args)
24
24
  unless result = transition_to(state.to_sym, *args)
25
25
  exception = exception_for_state_change_to(state)
26
- logger.fatal { "#{self.class}: #{exception.message}" }
26
+ logger.fatal { "#{self.class}: #{exception.message}\n#{caller[0..20].join("\n")}" }
27
27
  end
28
28
  result
29
29
  end
@@ -79,6 +79,10 @@ module Ably
79
79
  # @return [Hash]
80
80
  attr_reader :options
81
81
 
82
+ # Properties of a channel and its state
83
+ # @return [{Ably::Realtime::Channel::ChannelProperties}]
84
+ attr_reader :properties
85
+
82
86
  # When a channel failure occurs this attribute contains the Ably Exception
83
87
  # @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException]
84
88
  attr_reader :error_reason
@@ -88,11 +92,6 @@ module Ably
88
92
  # @api private
89
93
  attr_reader :manager
90
94
 
91
- # Serial number assigned to this channel when it was attached
92
- # @return [Integer]
93
- # @api private
94
- attr_reader :attached_serial
95
-
96
95
  # Initialize a new Channel object
97
96
  #
98
97
  # @param client [Ably::Rest::Client]
@@ -112,6 +111,7 @@ module Ably
112
111
  @state = STATE(state_machine.current_state)
113
112
  @manager = ChannelManager.new(self, client.connection)
114
113
  @push = PushChannel.new(self)
114
+ @properties = ChannelProperties.new(self)
115
115
 
116
116
  setup_event_handlers
117
117
  setup_presence
@@ -292,7 +292,7 @@ module Ably
292
292
  error = Ably::Exceptions::InvalidRequest.new('option :until_attach is invalid as the channel is not attached' )
293
293
  return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
294
294
  end
295
- options[:from_serial] = attached_serial
295
+ options[:from_serial] = properties.attach_serial
296
296
  end
297
297
 
298
298
  async_wrap(callback) do
@@ -319,11 +319,6 @@ module Ably
319
319
  @error_reason = nil
320
320
  end
321
321
 
322
- # @api private
323
- def set_attached_serial(serial)
324
- @attached_serial = serial
325
- end
326
-
327
322
  # @api private
328
323
  def update_options(channel_options)
329
324
  @options = channel_options.clone.freeze
@@ -372,3 +367,4 @@ end
372
367
  require 'ably/realtime/channel/channel_manager'
373
368
  require 'ably/realtime/channel/channel_state_machine'
374
369
  require 'ably/realtime/channel/push_channel'
370
+ require 'ably/realtime/channel/channel_properties'
@@ -37,7 +37,7 @@ module Ably::Realtime
37
37
  # library, such as returning to attached whne detach has failed
38
38
  if attached_protocol_message
39
39
  update_presence_sync_state_following_attached attached_protocol_message
40
- channel.set_attached_serial attached_protocol_message.channel_serial
40
+ channel.properties.set_attach_serial(attached_protocol_message.channel_serial)
41
41
  end
42
42
  end
43
43
 
@@ -76,7 +76,7 @@ module Ably::Realtime
76
76
  update_presence_sync_state_following_attached protocol_message
77
77
  end
78
78
 
79
- channel.set_attached_serial protocol_message.channel_serial
79
+ channel.properties.set_attach_serial(protocol_message.channel_serial)
80
80
  end
81
81
 
82
82
  # Handle DETACED messages, see #RTL13 for server-initated detaches
@@ -0,0 +1,24 @@
1
+ module Ably::Realtime
2
+ class Channel
3
+ # Represents properties of a channel and its state
4
+ class ChannelProperties
5
+ # {Ably::Realtime::Channel} this object associated with
6
+ # @return [Ably::Realtime::Channel]
7
+ attr_reader :channel
8
+
9
+ # Contains the last channelSerial received in an ATTACHED ProtocolMesage for the channel, see RTL15a
10
+ #
11
+ # @return [String]
12
+ attr_reader :attach_serial
13
+
14
+ def initialize(channel)
15
+ @channel = channel
16
+ end
17
+
18
+ # @api private
19
+ def set_attach_serial(attach_serial)
20
+ @attach_serial = attach_serial
21
+ end
22
+ end
23
+ end
24
+ end
@@ -65,6 +65,10 @@ module Ably
65
65
  # @return [String,Nil]
66
66
  attr_reader :recover
67
67
 
68
+ # Additional parameters to be sent in the querystring when initiating a realtime connection
69
+ # @return [Hash]
70
+ attr_reader :transport_params
71
+
68
72
  def_delegators :auth, :client_id, :auth_options
69
73
  def_delegators :@rest_client, :encoders
70
74
  def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary?
@@ -82,6 +86,7 @@ module Ably
82
86
  # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection
83
87
  # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery
84
88
  # @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect.
89
+ # @option options [Hash] :transport_params Additional parameters to be sent in the querystring when initiating a realtime connection. Keys are Strings, values are Stringifiable(a value must respond to #to_s)
85
90
  #
86
91
  # @option options [Integer] :channel_retry_timeout (15 seconds). When a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
87
92
  # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in seconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
@@ -109,10 +114,10 @@ module Ably
109
114
  end
110
115
  end
111
116
 
117
+ @transport_params = options.delete(:transport_params).to_h.each_with_object({}) do |(key, value), acc|
118
+ acc[key.to_s] = value.to_s
119
+ end
112
120
  @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self))
113
- @auth = Ably::Realtime::Auth.new(self)
114
- @channels = Ably::Realtime::Channels.new(self)
115
- @connection = Ably::Realtime::Connection.new(self, options)
116
121
  @echo_messages = rest_client.options.fetch(:echo_messages, true) == false ? false : true
117
122
  @queue_messages = rest_client.options.fetch(:queue_messages, true) == false ? false : true
118
123
  @custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host]
@@ -120,6 +125,10 @@ module Ably
120
125
  @recover = rest_client.options[:recover]
121
126
 
122
127
  raise ArgumentError, "Recovery key '#{recover}' is invalid" if recover && !recover.match(Connection::RECOVER_REGEX)
128
+
129
+ @auth = Ably::Realtime::Auth.new(self)
130
+ @channels = Ably::Realtime::Channels.new(self)
131
+ @connection = Ably::Realtime::Connection.new(self, options)
123
132
  end
124
133
 
125
134
  # Return a {Ably::Realtime::Channel Realtime Channel} for the given name
@@ -66,7 +66,7 @@ module Ably
66
66
  ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
67
67
 
68
68
  # Expected format for a connection recover key
69
- RECOVER_REGEX = /^(?<recover>[\w!-]+):(?<connection_serial>\-?\w+)$/
69
+ RECOVER_REGEX = /^(?<recover>[^:]+):(?<connection_serial>[^:]+):(?<msg_serial>\-?\d+)$/
70
70
 
71
71
  # Defaults for automatic connection recovery and timeouts
72
72
  DEFAULTS = {
@@ -137,7 +137,6 @@ module Ably
137
137
  @client = client
138
138
  @__outgoing_message_queue__ = []
139
139
  @__pending_message_ack_queue__ = []
140
- reset_client_serial
141
140
 
142
141
  @defaults = DEFAULTS.dup
143
142
  options.each do |key, val|
@@ -145,12 +144,25 @@ module Ably
145
144
  end if options.kind_of?(Hash)
146
145
  @defaults.freeze
147
146
 
147
+ # If a recover client options is provided, then we need to ensure that the msgSerial matches the
148
+ # recover serial immediately at client library instantiation. This is done immediately so that any queued
149
+ # publishes use the correct serial number for these queued messages as well.
150
+ # There is no harm if the msgSerial is higher than expected if the recover fails.
151
+ recovery_msg_serial = connection_recover_parts && connection_recover_parts[:msg_serial].to_i
152
+ if recovery_msg_serial
153
+ @client_msg_serial = recovery_msg_serial
154
+ else
155
+ reset_client_msg_serial
156
+ end
157
+
148
158
  Client::IncomingMessageDispatcher.new client, self
149
159
  Client::OutgoingMessageDispatcher.new client, self
150
160
 
151
161
  @state_machine = ConnectionStateMachine.new(self)
152
162
  @state = STATE(state_machine.current_state)
153
163
  @manager = ConnectionManager.new(self)
164
+
165
+ @current_host = client.endpoint.host
154
166
  end
155
167
 
156
168
  # Causes the connection to close, entering the closed state, from any state except
@@ -303,18 +315,17 @@ module Ably
303
315
  # @!attribute [r] recovery_key
304
316
  # @return [String] recovery key that can be used by another client to recover this connection with the :recover option
305
317
  def recovery_key
306
- "#{key}:#{serial}" if connection_resumable?
318
+ "#{key}:#{serial}:#{client_msg_serial}" if connection_resumable?
307
319
  end
308
320
 
309
321
  # Following a new connection being made, the connection ID, connection key
310
- # and message serial need to match the details provided by the server.
322
+ # and connection serial need to match the details provided by the server.
311
323
  #
312
324
  # @return [void]
313
325
  # @api private
314
326
  def configure_new(connection_id, connection_key, connection_serial)
315
327
  @id = connection_id
316
328
  @key = connection_key
317
- @client_serial = connection_serial
318
329
 
319
330
  update_connection_serial connection_serial
320
331
  end
@@ -420,10 +431,10 @@ module Ably
420
431
  client.auth.auth_params.tap do |auth_deferrable|
421
432
  auth_deferrable.callback do |auth_params|
422
433
  url_params = auth_params.merge(
423
- format: client.protocol,
424
- echo: client.echo_messages,
425
- v: Ably::PROTOCOL_VERSION,
426
- lib: client.rest_client.lib_version_id,
434
+ 'format' => client.protocol,
435
+ 'echo' => client.echo_messages,
436
+ 'v' => Ably::PROTOCOL_VERSION,
437
+ 'lib' => client.rest_client.lib_version_id,
427
438
  )
428
439
 
429
440
  # Use native websocket heartbeats if possible, but allow Ably protocol heartbeats
@@ -434,6 +445,7 @@ module Ably
434
445
  end
435
446
 
436
447
  url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
448
+ url_params.merge!(client.transport_params)
437
449
 
438
450
  if connection_resumable?
439
451
  url_params.merge! resume: key, connection_serial: serial
@@ -542,11 +554,11 @@ module Ably
542
554
  defaults.fetch(:realtime_request_timeout)
543
555
  end
544
556
 
545
- # Resets the client serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage}
546
- # (see #client_serial)
557
+ # Resets the client message serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage}
558
+ # (see #client_msg_serial)
547
559
  # @api private
548
- def reset_client_serial
549
- @client_serial = -1
560
+ def reset_client_msg_serial
561
+ @client_msg_serial = -1
550
562
  end
551
563
 
552
564
  # When a hearbeat or any other message from Ably is received
@@ -568,15 +580,15 @@ module Ably
568
580
 
569
581
  private
570
582
 
571
- # The client serial is incremented for every message that is published that requires an ACK.
583
+ # The client message serial (msgSerial) is incremented for every message that is published that requires an ACK.
572
584
  # Note that this is different to the connection serial that contains the last known serial number
573
585
  # received from the server.
574
586
  #
575
587
  # A message serial number does not guarantee a message has been received, only sent.
576
588
  # A connection serial guarantees the server has received the message and is thus used for connection recovery and resumes.
577
589
  # @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
578
- def client_serial
579
- @client_serial
590
+ def client_msg_serial
591
+ @client_msg_serial
580
592
  end
581
593
 
582
594
  def resume_callbacks
@@ -601,11 +613,11 @@ module Ably
601
613
  end
602
614
 
603
615
  def add_message_serial_to(protocol_message)
604
- @client_serial += 1
605
- protocol_message[:msgSerial] = client_serial
616
+ @client_msg_serial += 1
617
+ protocol_message[:msgSerial] = client_msg_serial
606
618
  yield
607
619
  rescue StandardError => e
608
- @client_serial -= 1
620
+ @client_msg_serial -= 1
609
621
  raise e
610
622
  end
611
623
 
@@ -49,18 +49,31 @@ module Ably::Realtime
49
49
 
50
50
  logger.debug { 'ConnectionManager: Opening a websocket transport connection' }
51
51
 
52
+ # The socket attempt can fail at the same time as a timer firing so ensure
53
+ # only one outcome is processed from this setup attempt
54
+ setup_attempt_status = {}
55
+ setup_failed = lambda do
56
+ return true if setup_attempt_status[:failed]
57
+ setup_attempt_status[:failed] = true
58
+ false
59
+ end
60
+
52
61
  connection.create_websocket_transport.tap do |socket_deferrable|
53
62
  socket_deferrable.callback do |websocket_transport|
54
63
  subscribe_to_transport_events websocket_transport
55
64
  yield websocket_transport if block_given?
56
65
  end
57
66
  socket_deferrable.errback do |error|
67
+ next if setup_failed.call
58
68
  connection_opening_failed error
59
69
  end
60
70
  end
61
71
 
72
+ # The connection request timeout must be marginally higher than the REST request timeout to ensure
73
+ # any HTTP auth request failure due to timeout triggers before the connection timer kicks in
62
74
  logger.debug { "ConnectionManager: Setting up automatic connection timeout timer for #{realtime_request_timeout}s" }
63
75
  create_timeout_timer_whilst_in_state(:connecting, realtime_request_timeout) do
76
+ next if setup_failed.call
64
77
  connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{realtime_request_timeout}s", nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT)
65
78
  end
66
79
  end
@@ -80,7 +93,12 @@ module Ably::Realtime
80
93
 
81
94
  logger.warn { "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}" }
82
95
  next_state = get_next_retry_state_info
83
- connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error)
96
+
97
+ if connection.state == next_state.fetch(:state)
98
+ logger.error { "ConnectionManager: Skipping next retry state after connection opening failed as already in state #{next_state}\n#{caller[0..20].join("\n")}" }
99
+ else
100
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error)
101
+ end
84
102
  end
85
103
 
86
104
  # Called whenever a new connection is made
@@ -100,13 +118,11 @@ module Ably::Realtime
100
118
  resend_pending_message_ack_queue
101
119
  else
102
120
  logger.debug { "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connection ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
103
- connection.reset_client_serial
104
121
  nack_messages_on_all_channels protocol_message.error
105
122
  force_reattach_on_channels protocol_message.error
106
123
  end
107
124
  else
108
125
  logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
109
- connection.reset_client_serial
110
126
  end
111
127
 
112
128
  reattach_suspended_channels protocol_message.error