ably-rest 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/ably-rest.gemspec +2 -1
  3. data/lib/submodules/ably-ruby/.travis.yml +6 -4
  4. data/lib/submodules/ably-ruby/CHANGELOG.md +52 -61
  5. data/lib/submodules/ably-ruby/README.md +10 -0
  6. data/lib/submodules/ably-ruby/SPEC.md +1473 -852
  7. data/lib/submodules/ably-ruby/ably.gemspec +2 -1
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +57 -25
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +34 -8
  10. data/lib/submodules/ably-ruby/lib/ably/logger.rb +10 -1
  11. data/lib/submodules/ably-ruby/lib/ably/models/auth_details.rb +42 -0
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +18 -4
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +6 -3
  14. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +1 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +12 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +101 -97
  18. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +13 -1
  19. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +7 -3
  21. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +17 -7
  22. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +29 -14
  23. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +7 -4
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -4
  25. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +7 -3
  26. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +2 -0
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +79 -31
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +62 -26
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +154 -65
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +16 -3
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +108 -49
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +165 -59
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +67 -45
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +198 -36
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +30 -6
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  42. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -3
  43. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +21 -8
  44. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -3
  45. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +2 -2
  46. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  47. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +1 -1
  48. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +26 -0
  49. data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -2
  50. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +416 -99
  51. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  52. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +1011 -160
  53. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +2 -2
  54. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +436 -97
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +52 -23
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1160 -105
  59. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +151 -22
  60. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +1 -1
  61. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +88 -27
  62. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +42 -15
  63. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +4 -4
  64. data/lib/submodules/ably-ruby/spec/rspec_config.rb +2 -1
  65. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +6 -2
  67. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +20 -4
  68. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +32 -1
  69. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +4 -11
  70. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +28 -2
  71. data/lib/submodules/ably-ruby/spec/unit/models/auth_details_spec.rb +49 -0
  72. data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +23 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +12 -1
  74. data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +15 -4
  75. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +34 -2
  76. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +73 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +64 -6
  78. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/unit/models/token_request_spec.rb +1 -1
  80. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -1
  81. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +69 -0
  82. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +149 -22
  83. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +9 -3
  84. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +1 -1
  85. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +8 -5
  86. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  87. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +4 -3
  88. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +3 -3
  90. metadata +7 -5
@@ -12,13 +12,12 @@ module Ably::Realtime
12
12
  class ConnectionManager
13
13
  # Error codes from the server that can potentially be resolved
14
14
  RESOLVABLE_ERROR_CODES = {
15
- token_expired: Ably::Rest::Middleware::Exceptions::TOKEN_EXPIRED_CODE
15
+ token_expired: Ably::Exceptions::TOKEN_EXPIRED_CODE
16
16
  }
17
17
 
18
18
  def initialize(connection)
19
19
  @connection = connection
20
20
  @timers = Hash.new { |hash, key| hash[key] = [] }
21
- @renewing_token = false
22
21
 
23
22
  connection.unsafe_on(:closed) do
24
23
  connection.reset_resume_info
@@ -48,7 +47,7 @@ module Ably::Realtime
48
47
  return
49
48
  end
50
49
 
51
- logger.debug 'ConnectionManager: Opening a websocket transport connection'
50
+ logger.debug { 'ConnectionManager: Opening a websocket transport connection' }
52
51
 
53
52
  connection.create_websocket_transport.tap do |socket_deferrable|
54
53
  socket_deferrable.callback do |websocket_transport|
@@ -60,7 +59,7 @@ module Ably::Realtime
60
59
  end
61
60
  end
62
61
 
63
- logger.debug "ConnectionManager: Setting up automatic connection timeout timer for #{realtime_request_timeout}s"
62
+ logger.debug { "ConnectionManager: Setting up automatic connection timeout timer for #{realtime_request_timeout}s" }
64
63
  create_timeout_timer_whilst_in_state(:connecting, realtime_request_timeout) do
65
64
  connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{realtime_request_timeout}s", nil, 80014)
66
65
  end
@@ -70,34 +69,72 @@ module Ably::Realtime
70
69
  #
71
70
  # @api private
72
71
  def connection_opening_failed(error)
73
- if error.kind_of?(Ably::Exceptions::IncompatibleClientId)
74
- client.connection.transition_state_machine :failed, reason: error
75
- return
72
+ if error.kind_of?(Ably::Exceptions::BaseAblyException)
73
+ # Authentication errors that indicate the authentication failure is terminal should move to the failed state
74
+ if ([401, 403].include?(error.status) && !RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)) ||
75
+ (error.code == Ably::Exceptions::INVALID_CLIENT_ID)
76
+ connection.transition_state_machine :failed, reason: error
77
+ return
78
+ end
76
79
  end
77
80
 
78
- logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
81
+ logger.warn { "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}" }
79
82
  next_state = get_next_retry_state_info
80
- connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000)
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, 80000, error)
81
84
  end
82
85
 
83
86
  # Called whenever a new connection is made
84
87
  #
85
88
  # @api private
86
89
  def connected(protocol_message)
90
+ # ClientID validity is already checked as part of the incoming message processing
91
+ client.auth.configure_client_id protocol_message.connection_details.client_id
92
+
93
+ # Update the connection details and any associated defaults
94
+ connection.set_connection_details protocol_message.connection_details
95
+
87
96
  if connection.key
88
97
  if protocol_message.connection_id == connection.id
89
- logger.debug "ConnectionManager: Connection resumed successfully - ID #{connection.id} and key #{connection.key}"
90
- EventMachine.next_tick { connection.resumed }
98
+ logger.debug { "ConnectionManager: Connection resumed successfully - ID #{connection.id} and key #{connection.key}" }
99
+ EventMachine.next_tick { connection.trigger_resumed }
100
+ resend_pending_message_ack_queue
91
101
  else
92
- logger.debug "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connect ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}"
93
- detach_attached_channels protocol_message.error
102
+ 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
+ nack_messages_on_all_channels protocol_message.error
105
+ force_reattach_on_channels protocol_message.error
94
106
  end
95
107
  else
96
- logger.debug "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}"
108
+ logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
109
+ connection.reset_client_serial
97
110
  end
111
+
112
+ reattach_suspended_channels protocol_message.error
113
+
98
114
  connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
99
115
  end
100
116
 
117
+ # When connection is CONNECTED and receives an update
118
+ # Update the Connection details and emit an UPDATE event #RTN4h
119
+ def connected_update(protocol_message)
120
+ # ClientID validity is already checked as part of the incoming message processing
121
+ client.auth.configure_client_id protocol_message.connection_details.client_id
122
+
123
+ # Update the connection details and any associated defaults
124
+ connection.set_connection_details protocol_message.connection_details
125
+
126
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
127
+
128
+ state_change = Ably::Models::ConnectionStateChange.new(
129
+ current: connection.state,
130
+ previous: connection.state,
131
+ event: Ably::Realtime::Connection::EVENT(:update),
132
+ reason: protocol_message.error,
133
+ protocol_message: protocol_message
134
+ )
135
+ connection.emit :update, state_change
136
+ end
137
+
101
138
  # Ensures the underlying transport has been disconnected and all event emitter callbacks removed
102
139
  #
103
140
  # @api private
@@ -109,6 +146,12 @@ module Ably::Realtime
109
146
  end
110
147
  end
111
148
 
149
+ # @api private
150
+ def release_and_establish_new_transport
151
+ destroy_transport
152
+ setup_transport
153
+ end
154
+
112
155
  # Reconnect the {Ably::Realtime::Connection::WebsocketTransport} if possible, otherwise set up a new transport
113
156
  #
114
157
  # @api private
@@ -143,9 +186,12 @@ module Ably::Realtime
143
186
  #
144
187
  # @api private
145
188
  def fail(error)
146
- connection.logger.fatal "ConnectionManager: Connection failed - #{error}"
147
- connection.manager.destroy_transport
148
- connection.unsafe_once(:failed) { connection.emit :error, error }
189
+ connection.logger.fatal { "ConnectionManager: Connection failed - #{error}" }
190
+ destroy_transport
191
+ channels.each do |channel|
192
+ next if channel.detached? || channel.initialized?
193
+ channel.transition_state_machine :failed, reason: error if channel.can_transition_to?(:failed)
194
+ end
149
195
  end
150
196
 
151
197
  # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
@@ -153,12 +199,12 @@ module Ably::Realtime
153
199
  # @api private
154
200
  def respond_to_transport_disconnected_when_connecting(error)
155
201
  return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
156
- return unless can_retry_connection? # do not always reattempt connection or change state as client may be re-authorising
202
+ return if currently_renewing_token? # do not always reattempt connection or change state as client may be re-authorising
157
203
 
158
204
  if error.kind_of?(Ably::Models::ErrorInfo)
159
205
  if RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)
160
- next_state = get_next_retry_state_info
161
- logger.debug "ConnectionManager: Transport disconnected because of token expiry, pausing #{next_state.fetch(:pause)}s before reattempting to connect"
206
+ next_state = get_next_retry_state_info(1)
207
+ logger.debug { "ConnectionManager: Transport disconnected because of token expiry, pausing #{next_state.fetch(:pause)}s before reattempting to connect" }
162
208
  EventMachine.add_timer(next_state.fetch(:pause)) { renew_token_and_reconnect error }
163
209
  return
164
210
  end
@@ -179,14 +225,13 @@ module Ably::Realtime
179
225
  # @api private
180
226
  def respond_to_transport_disconnected_whilst_connected(error)
181
227
  unless connection.disconnected? || connection.suspended?
182
- logger.warn "ConnectionManager: Connection #{"to #{connection.transport.url}" if connection.transport} was disconnected unexpectedly"
228
+ logger.warn { "ConnectionManager: Connection #{"to #{connection.transport.url}" if connection.transport} was disconnected unexpectedly" }
183
229
  else
184
- logger.debug "ConnectionManager: Transport disconnected whilst connection in #{connection.state} state"
230
+ logger.debug { "ConnectionManager: Transport disconnected whilst connection in #{connection.state} state" }
185
231
  end
186
232
 
187
233
  if error.kind_of?(Ably::Models::ErrorInfo) && !RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)
188
- connection.emit :error, error
189
- logger.error "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}"
234
+ logger.error { "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}" }
190
235
  end
191
236
 
192
237
  destroy_transport
@@ -200,10 +245,10 @@ module Ably::Realtime
200
245
  def error_received_from_server(error)
201
246
  case error.code
202
247
  when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
203
- next_state = get_next_retry_state_info
248
+ next_state = get_next_retry_state_info(1)
204
249
  connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: error
205
250
  else
206
- logger.error "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state"
251
+ logger.error { "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state" }
207
252
  connection.transition_state_machine :failed, reason: error
208
253
  end
209
254
  end
@@ -215,6 +260,71 @@ module Ably::Realtime
215
260
  retries_for_state(state, ignore_states: [:connecting]).count
216
261
  end
217
262
 
263
+ # Any message sent before an ACK/NACK was received on the previous transport
264
+ # need to be resent to the Ably service so that a subsequent ACK/NACK is received.
265
+ # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
266
+ # base on the serial numbers
267
+ #
268
+ # @api private
269
+ def resend_pending_message_ack_queue
270
+ connection.__pending_message_ack_queue__.delete_if do |protocol_message|
271
+ if protocol_message.ack_required?
272
+ connection.__outgoing_message_queue__ << protocol_message
273
+ connection.__outgoing_protocol_msgbus__.publish :protocol_message
274
+ true
275
+ end
276
+ end
277
+ end
278
+
279
+ # @api private
280
+ def suspend_active_channels(error)
281
+ channels.select do |channel|
282
+ channel.attached? || channel.attaching? || channel.detaching?
283
+ end.each do |channel|
284
+ channel.transition_state_machine! :suspended, reason: error
285
+ end
286
+ end
287
+
288
+ # @api private
289
+ def detach_active_channels
290
+ channels.select do |channel|
291
+ channel.attached? || channel.attaching? || channel.detaching?
292
+ end.each do |channel|
293
+ channel.transition_state_machine! :detaching # will always move to detached immediately if connection is closed
294
+ end
295
+ end
296
+
297
+ # @api private
298
+ def fail_active_channels(error)
299
+ channels.select do |channel|
300
+ channel.attached? || channel.attaching? || channel.detaching? || channel.suspended?
301
+ end.each do |channel|
302
+ channel.transition_state_machine! :failed, reason: error
303
+ end
304
+ end
305
+
306
+ # When continuity on a connection is lost all messages
307
+ # whether queued or awaiting an ACK must be NACK'd as we now have a new connection
308
+ def nack_messages_on_all_channels(error)
309
+ channels.each do |channel|
310
+ channel.manager.fail_messages_awaiting_ack error, immediately: true
311
+ channel.manager.fail_queued_messages error
312
+ end
313
+ end
314
+
315
+ # Liveness timer ensures a connection that has not heard from Ably in heartbeat_interval
316
+ # is moved to the disconnected state automatically
317
+ def reset_liveness_timer
318
+ @liveness_timer.cancel if @liveness_timer
319
+ @liveness_timer = EventMachine::Timer.new(connection.heartbeat_interval + 0.1) do
320
+ if connection.connected? && (connection.time_since_connection_confirmed_alive? >= connection.heartbeat_interval)
321
+ msg = "No activity seen from realtime in #{connection.heartbeat_interval}; assuming connection has dropped";
322
+ error = Ably::Exceptions::ConnectionTimeout.new(msg, 80003, 408)
323
+ connection.transition_state_machine! :disconnected, reason: error
324
+ end
325
+ end
326
+ end
327
+
218
328
  private
219
329
  def connection
220
330
  @connection
@@ -265,7 +375,7 @@ module Ably::Realtime
265
375
  timers.fetch(key, []).each(&:cancel)
266
376
  end
267
377
 
268
- def get_next_retry_state_info
378
+ def get_next_retry_state_info(allow_extra_immediate_retries = 0)
269
379
  retry_state = if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
270
380
  :suspended
271
381
  else
@@ -273,14 +383,14 @@ module Ably::Realtime
273
383
  end
274
384
  {
275
385
  state: retry_state,
276
- pause: next_retry_pause(retry_state)
386
+ pause: next_retry_pause(retry_state, allow_extra_immediate_retries)
277
387
  }
278
388
  end
279
389
 
280
- def next_retry_pause(retry_state)
390
+ def next_retry_pause(retry_state, allow_extra_immediate_retries = 0)
281
391
  return nil unless state_has_retry_timeout?(retry_state)
282
392
 
283
- if retries_for_state(retry_state, ignore_states: [:connecting]).empty?
393
+ if retries_for_state(retry_state, ignore_states: [:connecting]).count <= allow_extra_immediate_retries
284
394
  0
285
395
  else
286
396
  retry_timeout_for(retry_state)
@@ -302,10 +412,10 @@ module Ably::Realtime
302
412
  def connection_retry_for(from_state)
303
413
  if can_reattempt_connect_for_state?(from_state)
304
414
  if connection.state == :disconnected && retries_for_state(from_state, ignore_states: [:connecting]).empty?
305
- logger.debug "ConnectionManager: Will attempt reconnect immediately as no previous reconnect attempts made in state #{from_state}"
415
+ logger.debug { "ConnectionManager: Will attempt reconnect immediately as no previous reconnect attempts made in state #{from_state}" }
306
416
  EventMachine.next_tick { connection.connect }
307
417
  else
308
- logger.debug "ConnectionManager: Pausing for #{retry_timeout_for(from_state)}s before attempting to reconnect"
418
+ logger.debug { "ConnectionManager: Pausing for #{retry_timeout_for(from_state)}s before attempting to reconnect" }
309
419
  create_timeout_timer_whilst_in_state(from_state, retry_timeout_for(from_state)) do
310
420
  connection.connect if connection.state == from_state
311
421
  end
@@ -380,7 +490,7 @@ module Ably::Realtime
380
490
  transport.unsafe_on(:disconnected) do |reason|
381
491
  if connection.closing?
382
492
  connection.transition_state_machine :closed
383
- elsif !connection.closed? && !connection.disconnected?
493
+ elsif !connection.closed? && !connection.disconnected? && !connection.failed? && !connection.suspended?
384
494
  exception = if reason
385
495
  Ably::Exceptions::TransportClosed.new(reason, nil, 80003)
386
496
  else
@@ -394,35 +504,21 @@ module Ably::Realtime
394
504
 
395
505
  def renew_token_and_reconnect(error)
396
506
  if client.auth.token_renewable?
397
- if @renewing_token
398
- logger.error 'ConnectionManager: Attempting to renew token whilst another token renewal is underway. Aborting current renew token request'
507
+ if currently_renewing_token?
508
+ logger.error { 'ConnectionManager: Attempting to renew token whilst another token renewal is underway. Aborting current renew token request' }
399
509
  return
400
510
  end
401
511
 
402
- @renewing_token = true
403
- logger.info "ConnectionManager: Token has expired and is renewable, renewing token now"
512
+ logger.info { "ConnectionManager: Token has expired and is renewable, renewing token now" }
404
513
 
514
+ # Authorize implicitly reconnects, see #RTC8
405
515
  client.auth.authorize.tap do |authorize_deferrable|
406
516
  authorize_deferrable.callback do |token_details|
407
- logger.info 'ConnectionManager: Token renewed succesfully following expiration'
408
-
409
- connection.once_state_changed { @renewing_token = false }
410
-
411
- if token_details && !token_details.expired?
412
- connection.connect
413
- else
414
- connection.transition_state_machine :failed, reason: error unless connection.failed?
415
- end
416
- end
417
-
418
- authorize_deferrable.errback do |auth_error|
419
- @renewing_token = false
420
- logger.error "ConnectionManager: Error authorising following token expiry: #{auth_error}"
421
- connection.transition_state_machine :failed, reason: auth_error
517
+ logger.info { 'ConnectionManager: Token renewed succesfully following expiration' }
422
518
  end
423
519
  end
424
520
  else
425
- logger.error "ConnectionManager: Token has expired and is not renewable - #{error}"
521
+ logger.error { "ConnectionManager: Token has expired and is not renewable - #{error}" }
426
522
  connection.transition_state_machine :failed, reason: error
427
523
  end
428
524
  end
@@ -430,7 +526,7 @@ module Ably::Realtime
430
526
  def unsubscribe_from_transport_events(transport)
431
527
  transport.__incoming_protocol_msgbus__.unsubscribe
432
528
  transport.off
433
- logger.debug "ConnectionManager: Unsubscribed from all events from current transport"
529
+ logger.debug { "ConnectionManager: Unsubscribed from all events from current transport" }
434
530
  end
435
531
 
436
532
  def close_connection_when_reactor_is_stopped
@@ -439,16 +535,26 @@ module Ably::Realtime
439
535
  end
440
536
  end
441
537
 
442
- def can_retry_connection?
443
- !@renewing_token
538
+ def currently_renewing_token?
539
+ client.auth.authorization_in_flight?
540
+ end
541
+
542
+ def reattach_suspended_channels(error)
543
+ channels.select do |channel|
544
+ channel.suspended?
545
+ end.each do |channel|
546
+ channel.transition_state_machine :attaching
547
+ end
444
548
  end
445
549
 
446
- def detach_attached_channels(error)
550
+ # When continuity on a connection is lost all messages
551
+ # Channels in the ATTACHED or ATTACHING state should explicitly be re-attached
552
+ # by sending a new ATTACH to Ably
553
+ def force_reattach_on_channels(error)
447
554
  channels.select do |channel|
448
555
  channel.attached? || channel.attaching?
449
556
  end.each do |channel|
450
- logger.warn "Force detaching channel '#{channel.name}': #{error}"
451
- channel.manager.suspend error
557
+ channel.manager.request_reattach reason: error
452
558
  end
453
559
  end
454
560
 
@@ -47,8 +47,7 @@ module Ably::Realtime
47
47
  after_transition(to: [:connected]) do |connection, current_transition|
48
48
  error = current_transition.metadata.reason
49
49
  if is_error_type?(error)
50
- connection.logger.warn "ConnectionManager: Connected with error - #{error.message}"
51
- connection.emit :error, error
50
+ connection.logger.warn { "ConnectionManager: Connected with error - #{error.message}" }
52
51
  end
53
52
  end
54
53
 
@@ -62,6 +61,11 @@ module Ably::Realtime
62
61
  connection.manager.respond_to_transport_disconnected_whilst_connected err
63
62
  end
64
63
 
64
+ after_transition(to: [:suspended]) do |connection, current_transition|
65
+ err = error_from_state_change(current_transition)
66
+ connection.manager.suspend_active_channels err
67
+ end
68
+
65
69
  after_transition(to: [:disconnected, :suspended]) do |connection|
66
70
  connection.manager.destroy_transport # never reuse a transport if the connection has failed
67
71
  end
@@ -71,6 +75,18 @@ module Ably::Realtime
71
75
  connection.manager.fail err
72
76
  end
73
77
 
78
+ after_transition(to: [:failed]) do |connection, current_transition|
79
+ err = error_from_state_change(current_transition)
80
+ connection.manager.fail_active_channels err
81
+ end
82
+
83
+ # RTN7C - If a connection enters the SUSPENDED, CLOSED or FAILED state...
84
+ # the client should consider the delivery of those messages as failed
85
+ after_transition(to: [:suspended, :closed, :failed]) do |connection, current_transition|
86
+ err = error_from_state_change(current_transition)
87
+ connection.manager.nack_messages_on_all_channels err
88
+ end
89
+
74
90
  after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
75
91
  connection.manager.force_close_connection
76
92
  end
@@ -83,6 +99,10 @@ module Ably::Realtime
83
99
  connection.manager.destroy_transport
84
100
  end
85
101
 
102
+ after_transition(to: [:closed]) do |connection|
103
+ connection.manager.detach_active_channels
104
+ end
105
+
86
106
  # Transitions responsible for updating connection#error_reason
87
107
  before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
88
108
  err = error_from_state_change(current_transition)
@@ -91,7 +111,6 @@ module Ably::Realtime
91
111
 
92
112
  before_transition(to: [:connected, :closed]) do |connection, current_transition|
93
113
  err = error_from_state_change(current_transition)
94
-
95
114
  if err
96
115
  connection.set_failed_connection_error_reason err
97
116
  else
@@ -119,14 +119,14 @@ module Ably::Realtime
119
119
  when :msgpack
120
120
  driver.binary(object.to_msgpack.unpack('C*'))
121
121
  else
122
- client.logger.fatal "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket"
122
+ client.logger.fatal { "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket" }
123
123
  end
124
124
  end
125
125
 
126
126
  def setup_event_handlers
127
127
  __outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
128
128
  send_object protocol_message
129
- client.logger.debug "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}"
129
+ client.logger.debug { "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}" }
130
130
  end
131
131
  end
132
132
 
@@ -147,32 +147,41 @@ module Ably::Realtime
147
147
  @driver = WebSocket::Driver.client(self)
148
148
 
149
149
  driver.on("open") do
150
- logger.debug "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message"
150
+ logger.debug { "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message" }
151
151
  end
152
152
 
153
153
  driver.on("message") do |event|
154
154
  event_data = parse_event_data(event.data).freeze
155
155
  protocol_message = Ably::Models::ProtocolMessage.new(event_data, logger: logger)
156
156
  action_name = Ably::Models::ProtocolMessage::ACTION[event_data['action']] rescue event_data['action']
157
- logger.debug "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}"
157
+ logger.debug { "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}" }
158
158
 
159
159
  if protocol_message.invalid?
160
- error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nMessage has been discarded", 400, 80013)
161
- connection.emit :error, error
162
- logger.fatal "WebsocketTransport: #{error.message}"
160
+ error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nConnection moving to the failed state as the protocol is invalid and unsupported", 400, 80013)
161
+ logger.fatal { "WebsocketTransport: #{error.message}" }
162
+ failed_protocol_message = Ably::Models::ProtocolMessage.new(
163
+ action: Ably::Models::ProtocolMessage::ACTION.Error,
164
+ error: error.as_json,
165
+ logger: logger
166
+ )
167
+ __incoming_protocol_msgbus__.publish :protocol_message, failed_protocol_message
163
168
  else
164
169
  __incoming_protocol_msgbus__.publish :protocol_message, protocol_message
165
170
  end
166
171
  end
167
172
 
173
+ driver.on("ping") do
174
+ __incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Heartbeat, source: :websocket)
175
+ end
176
+
168
177
  driver.on("error") do |error|
169
- logger.error "WebsocketTransport: Protocol Error on transports - #{error.message}"
178
+ logger.error { "WebsocketTransport: Protocol Error on transports - #{error.message}" }
170
179
  end
171
180
 
172
181
  @reason_closed = nil
173
182
  driver.on("closed") do |event|
174
183
  @reason_closed = "#{event.code}: #{event.reason}"
175
- logger.warn "WebsocketTransport: Driver reported transport as closed - #{reason_closed}"
184
+ logger.warn { "WebsocketTransport: Driver reported transport as closed - #{reason_closed}" }
176
185
  end
177
186
  end
178
187
 
@@ -192,7 +201,7 @@ module Ably::Realtime
192
201
  when :msgpack
193
202
  MessagePack.unpack(data.pack('C*'))
194
203
  else
195
- client.logger.fatal "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}"
204
+ client.logger.fatal { "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}" }
196
205
  data
197
206
  end
198
207
  end