ably-rest 0.9.3 → 1.0.0

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 (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