ably-pubsub-core 0.0.1.pre

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ably/agent.rb +8 -0
  3. data/lib/ably/auth.rb +791 -0
  4. data/lib/ably/exceptions.rb +169 -0
  5. data/lib/ably/logger.rb +119 -0
  6. data/lib/ably/models/auth_details.rb +45 -0
  7. data/lib/ably/models/channel_details.rb +69 -0
  8. data/lib/ably/models/channel_metrics.rb +107 -0
  9. data/lib/ably/models/channel_occupancy.rb +50 -0
  10. data/lib/ably/models/channel_options.rb +120 -0
  11. data/lib/ably/models/channel_state_change.rb +84 -0
  12. data/lib/ably/models/channel_status.rb +63 -0
  13. data/lib/ably/models/cipher_params.rb +137 -0
  14. data/lib/ably/models/connection_details.rb +149 -0
  15. data/lib/ably/models/connection_state_change.rb +83 -0
  16. data/lib/ably/models/delta_extras.rb +28 -0
  17. data/lib/ably/models/device_details.rb +126 -0
  18. data/lib/ably/models/device_push_details.rb +94 -0
  19. data/lib/ably/models/error_info.rb +104 -0
  20. data/lib/ably/models/http_paginated_response.rb +112 -0
  21. data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
  22. data/lib/ably/models/message.rb +223 -0
  23. data/lib/ably/models/message_encoders/base.rb +135 -0
  24. data/lib/ably/models/message_encoders/base64.rb +40 -0
  25. data/lib/ably/models/message_encoders/cipher.rb +84 -0
  26. data/lib/ably/models/message_encoders/json.rb +34 -0
  27. data/lib/ably/models/message_encoders/utf8.rb +26 -0
  28. data/lib/ably/models/nil_logger.rb +20 -0
  29. data/lib/ably/models/paginated_result.rb +201 -0
  30. data/lib/ably/models/presence_message.rb +244 -0
  31. data/lib/ably/models/protocol_message.rb +288 -0
  32. data/lib/ably/models/push_channel_subscription.rb +114 -0
  33. data/lib/ably/models/stats.rb +242 -0
  34. data/lib/ably/models/stats_types.rb +107 -0
  35. data/lib/ably/models/token_details.rb +148 -0
  36. data/lib/ably/models/token_request.rb +180 -0
  37. data/lib/ably/modules/ably.rb +26 -0
  38. data/lib/ably/modules/async_wrapper.rb +69 -0
  39. data/lib/ably/modules/channels_collection.rb +105 -0
  40. data/lib/ably/modules/conversions.rb +153 -0
  41. data/lib/ably/modules/encodeable.rb +111 -0
  42. data/lib/ably/modules/enum.rb +230 -0
  43. data/lib/ably/modules/event_emitter.rb +200 -0
  44. data/lib/ably/modules/event_machine_helpers.rb +26 -0
  45. data/lib/ably/modules/exception_codes.rb +128 -0
  46. data/lib/ably/modules/http_helpers.rb +41 -0
  47. data/lib/ably/modules/message_emitter.rb +78 -0
  48. data/lib/ably/modules/message_pack.rb +14 -0
  49. data/lib/ably/modules/model_common.rb +84 -0
  50. data/lib/ably/modules/safe_deferrable.rb +83 -0
  51. data/lib/ably/modules/safe_yield.rb +41 -0
  52. data/lib/ably/modules/state_emitter.rb +181 -0
  53. data/lib/ably/modules/state_machine.rb +59 -0
  54. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  55. data/lib/ably/modules/uses_state_machine.rb +111 -0
  56. data/lib/ably/realtime/auth.rb +283 -0
  57. data/lib/ably/realtime/channel/channel_manager.rb +272 -0
  58. data/lib/ably/realtime/channel/channel_properties.rb +40 -0
  59. data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
  60. data/lib/ably/realtime/channel/publisher.rb +82 -0
  61. data/lib/ably/realtime/channel/push_channel.rb +69 -0
  62. data/lib/ably/realtime/channel.rb +440 -0
  63. data/lib/ably/realtime/channels.rb +71 -0
  64. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
  65. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
  66. data/lib/ably/realtime/client.rb +363 -0
  67. data/lib/ably/realtime/connection/connection_manager.rb +594 -0
  68. data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
  69. data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
  70. data/lib/ably/realtime/connection.rb +738 -0
  71. data/lib/ably/realtime/models/nil_channel.rb +30 -0
  72. data/lib/ably/realtime/presence/members_map.rb +395 -0
  73. data/lib/ably/realtime/presence/presence_manager.rb +78 -0
  74. data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
  75. data/lib/ably/realtime/presence.rb +502 -0
  76. data/lib/ably/realtime/push/admin.rb +75 -0
  77. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  78. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  79. data/lib/ably/realtime/push.rb +25 -0
  80. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  81. data/lib/ably/realtime.rb +60 -0
  82. data/lib/ably/rest/channel/push_channel.rb +59 -0
  83. data/lib/ably/rest/channel.rb +194 -0
  84. data/lib/ably/rest/channels.rb +47 -0
  85. data/lib/ably/rest/client.rb +727 -0
  86. data/lib/ably/rest/middleware/encoder.rb +49 -0
  87. data/lib/ably/rest/middleware/exceptions.rb +53 -0
  88. data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  89. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
  90. data/lib/ably/rest/middleware/logger.rb +58 -0
  91. data/lib/ably/rest/middleware/parse_json.rb +27 -0
  92. data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
  93. data/lib/ably/rest/presence.rb +113 -0
  94. data/lib/ably/rest/push/admin.rb +58 -0
  95. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  96. data/lib/ably/rest/push/device_registrations.rb +114 -0
  97. data/lib/ably/rest/push.rb +25 -0
  98. data/lib/ably/rest.rb +40 -0
  99. data/lib/ably/util/ably_extensions.rb +29 -0
  100. data/lib/ably/util/crypto.rb +134 -0
  101. data/lib/ably/util/pub_sub.rb +45 -0
  102. data/lib/ably/util/safe_deferrable.rb +44 -0
  103. data/lib/ably/version.rb +7 -0
  104. data/lib/ably.rb +16 -0
  105. metadata +497 -0
@@ -0,0 +1,594 @@
1
+ require 'ably/rest/middleware/exceptions'
2
+
3
+ module Ably::Realtime
4
+ class Connection
5
+ # ConnectionManager is responsible for all actions relating to underlying connection and transports,
6
+ # such as opening, closing, attempting reconnects etc.
7
+ # Connection state changes are performed by this class and executed from {ConnectionStateMachine}
8
+ #
9
+ # This is a private class and should never be used directly by developers as the API is likely to change in future.
10
+ #
11
+ # @api private
12
+ class ConnectionManager
13
+ # Error codes from the server that can potentially be resolved
14
+ RESOLVABLE_ERROR_CODES = {
15
+ token_expired: Ably::Exceptions::TOKEN_EXPIRED_CODE
16
+ }
17
+ using Ably::Util::AblyExtensions
18
+
19
+ def initialize(connection)
20
+ @connection = connection
21
+ @timers = Hash.new { |hash, key| hash[key] = [] }
22
+
23
+ # RTN8c, RTN9c
24
+ connection.unsafe_on(:closing, :closed, :suspended, :failed) do
25
+ connection.reset_resume_info
26
+ end
27
+
28
+ connection.unsafe_once(:connecting) do
29
+ close_connection_when_reactor_is_stopped
30
+ end
31
+
32
+ EventMachine.next_tick do
33
+ # Connect once Connection object is initialised
34
+ connection.connect if client.auto_connect && connection.can_transition_to?(:connecting)
35
+ end
36
+ end
37
+
38
+ # Creates and sets up a new {Ably::Realtime::Connection::WebsocketTransport} available on attribute #transport
39
+ #
40
+ # @yield [Ably::Realtime::Connection::WebsocketTransport] block is called with new websocket transport
41
+ # @api private
42
+ def setup_transport
43
+ if transport && !transport.ready_for_release?
44
+ raise RuntimeError, 'Existing WebsocketTransport is connected, and must be closed first'
45
+ end
46
+
47
+ unless client.auth.authentication_security_requirements_met?
48
+ connection.transition_state_machine :failed, reason: Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, Ably::Exceptions::Codes::INVALID_USE_OF_BASIC_AUTH_OVER_NONTLS_TRANSPORT)
49
+ return
50
+ end
51
+
52
+ logger.debug { 'ConnectionManager: Opening a websocket transport connection' }
53
+
54
+ # The socket attempt can fail at the same time as a timer firing so ensure
55
+ # only one outcome is processed from this setup attempt
56
+ setup_attempt_status = {}
57
+ setup_failed = lambda do
58
+ return true if setup_attempt_status[:failed]
59
+ setup_attempt_status[:failed] = true
60
+ false
61
+ end
62
+
63
+ connection.create_websocket_transport.tap do |socket_deferrable|
64
+ socket_deferrable.callback do |websocket_transport|
65
+ subscribe_to_transport_events websocket_transport
66
+ yield websocket_transport if block_given?
67
+ end
68
+ socket_deferrable.errback do |error|
69
+ next if setup_failed.call
70
+ connection_opening_failed error
71
+ end
72
+ end
73
+
74
+ # The connection request timeout must be marginally higher than the REST request timeout to ensure
75
+ # any HTTP auth request failure due to timeout triggers before the connection timer kicks in
76
+ logger.debug { "ConnectionManager: Setting up automatic connection timeout timer for #{realtime_request_timeout}s" }
77
+ create_timeout_timer_whilst_in_state(:connecting, realtime_request_timeout) do
78
+ next if setup_failed.call
79
+ connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{realtime_request_timeout}s", nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT)
80
+ end
81
+ end
82
+
83
+ # Called by the transport when a connection attempt fails
84
+ #
85
+ # @api private
86
+ def connection_opening_failed(error)
87
+ if error.kind_of?(Ably::Exceptions::BaseAblyException)
88
+ # Authentication errors that indicate the authentication failure is terminal should move to the failed state
89
+ if ([401, 403].include?(error.status) && !RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)) ||
90
+ (error.code == Ably::Exceptions::Codes::INVALID_CLIENT_ID)
91
+ connection.transition_state_machine :failed, reason: error
92
+ return
93
+ end
94
+ end
95
+
96
+ logger.warn { "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}" }
97
+ next_state = get_next_retry_state_info
98
+
99
+ if connection.state == next_state.fetch(:state)
100
+ logger.error { "ConnectionManager: Skipping next retry state after connection opening failed as already in state #{next_state}\n#{caller[0..20].join("\n")}" }
101
+ else
102
+ 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)
103
+ end
104
+ end
105
+
106
+ # Called whenever a new connection is made
107
+ #
108
+ # @api private
109
+ def connected(protocol_message)
110
+ # ClientID validity is already checked as part of the incoming message processing
111
+ client.auth.configure_client_id protocol_message.connection_details.client_id
112
+
113
+ # Update the connection details and any associated defaults
114
+ connection.set_connection_details protocol_message.connection_details
115
+
116
+ is_connection_resume_or_recover_attempt = !connection.key.nil_or_empty? || !client.recover.nil_or_empty?
117
+
118
+ # RTN15c7, RTN16d
119
+ failed_resume_or_recover = !protocol_message.connection_id == connection.id && !protocol_message.error.nil?
120
+ if is_connection_resume_or_recover_attempt and failed_resume_or_recover # RTN15c7
121
+ connection.reset_client_msg_serial
122
+ end
123
+ client.disable_automatic_connection_recovery # RTN16k, explicitly setting null, so it won't be used for subsequent connection requests
124
+
125
+ if connection.key
126
+ if protocol_message.connection_id == connection.id
127
+ logger.debug { "ConnectionManager: Connection resumed successfully - ID #{connection.id} and key #{connection.key}" }
128
+ resend_pending_message_ack_queue
129
+ else
130
+ nack_messages_on_all_channels protocol_message.error
131
+ end
132
+ else
133
+ logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_details.connection_key}" }
134
+ end
135
+
136
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key
137
+ force_reattach_on_channels protocol_message.error # irrespective of connection success/failure, reattach channels
138
+ end
139
+
140
+ # When connection is CONNECTED and receives an update
141
+ # Update the Connection details and emit an UPDATE event #RTN4h
142
+ def connected_update(protocol_message)
143
+ # ClientID validity is already checked as part of the incoming message processing
144
+ client.auth.configure_client_id protocol_message.connection_details.client_id
145
+
146
+ # Update the connection details and any associated defaults
147
+ connection.set_connection_details protocol_message.connection_details
148
+
149
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key
150
+
151
+ state_change = Ably::Models::ConnectionStateChange.new(
152
+ current: connection.state,
153
+ previous: connection.state,
154
+ event: Ably::Realtime::Connection::EVENT(:update),
155
+ reason: protocol_message.error,
156
+ protocol_message: protocol_message
157
+ )
158
+ connection.emit :update, state_change
159
+ end
160
+
161
+ # Ensures the underlying transport has been disconnected and all event emitter callbacks removed
162
+ #
163
+ # @api private
164
+ def destroy_transport
165
+ if transport
166
+ unsubscribe_from_transport_events transport
167
+ transport.close_connection
168
+ connection.release_websocket_transport
169
+ end
170
+ end
171
+
172
+ # @api private
173
+ def release_and_establish_new_transport
174
+ destroy_transport
175
+ setup_transport
176
+ end
177
+
178
+ # Reconnect the {Ably::Realtime::Connection::WebsocketTransport} if possible, otherwise set up a new transport
179
+ #
180
+ # @api private
181
+ def reconnect_transport
182
+ if !transport || transport.disconnected?
183
+ setup_transport
184
+ else
185
+ transport.reconnect connection.current_host, connection.port
186
+ end
187
+ end
188
+
189
+ # Send a Close {Ably::Models::ProtocolMessage} to the server and release the transport
190
+ #
191
+ # @api private
192
+ def close_connection
193
+ connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
194
+
195
+ create_timeout_timer_whilst_in_state(:closing, realtime_request_timeout) do
196
+ force_close_connection if connection.closing?
197
+ end
198
+ end
199
+
200
+ # Close the underlying transport immediately and set the connection state to closed
201
+ #
202
+ # @api private
203
+ def force_close_connection
204
+ destroy_transport
205
+ connection.transition_state_machine :closed
206
+ end
207
+
208
+ # Connection has failed
209
+ #
210
+ # @api private
211
+ def fail(error)
212
+ connection.logger.fatal { "ConnectionManager: Connection failed - #{error}" }
213
+ destroy_transport
214
+ channels.each do |channel|
215
+ next if channel.detached? || channel.initialized?
216
+ channel.transition_state_machine :failed, reason: error if channel.can_transition_to?(:failed)
217
+ end
218
+ end
219
+
220
+ # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
221
+ #
222
+ # @api private
223
+ def respond_to_transport_disconnected_when_connecting(error)
224
+ return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
225
+ return if currently_renewing_token? # do not always reattempt connection or change state as client may be re-authorising
226
+
227
+ if error.kind_of?(Ably::Models::ErrorInfo)
228
+ if RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)
229
+ next_state = get_next_retry_state_info(1)
230
+ logger.debug { "ConnectionManager: Transport disconnected because of token expiry, pausing #{next_state.fetch(:pause)}s before reattempting to connect" }
231
+ EventMachine.add_timer(next_state.fetch(:pause)) { renew_token_and_reconnect error }
232
+ return
233
+ end
234
+ end
235
+
236
+ if connection.state == :suspended
237
+ return if connection_retry_for(:suspended)
238
+ elsif connection.state == :disconnected
239
+ return if connection_retry_for(:disconnected)
240
+ end
241
+
242
+ # Fallback if no other criteria met
243
+ connection.transition_state_machine :failed, reason: error
244
+ end
245
+
246
+ # When a connection is disconnected after connecting, attempt reconnect and/or set state to :suspended or :failed
247
+ #
248
+ # @api private
249
+ def respond_to_transport_disconnected_whilst_connected(error)
250
+ unless connection.disconnected? || connection.suspended?
251
+ logger.warn { "ConnectionManager: Connection #{"to #{connection.transport.url}" if connection.transport} was disconnected unexpectedly" }
252
+ else
253
+ logger.debug { "ConnectionManager: Transport disconnected whilst connection in #{connection.state} state" }
254
+ end
255
+
256
+ if error.kind_of?(Ably::Models::ErrorInfo) && !RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)
257
+ logger.error { "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}" }
258
+ end
259
+
260
+ destroy_transport
261
+ respond_to_transport_disconnected_when_connecting error
262
+ end
263
+
264
+ # {Ably::Models::ProtocolMessage ProtocolMessage Error} received from server.
265
+ # Some error states can be resolved by the client library.
266
+ #
267
+ # @api private
268
+ def error_received_from_server(error)
269
+ case error.code
270
+ when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
271
+ next_state = get_next_retry_state_info(1)
272
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: error
273
+ else
274
+ logger.error { "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state" }
275
+ connection.transition_state_machine :failed, reason: error
276
+ end
277
+ end
278
+
279
+ # Number of consecutive attempts for provided state
280
+ #
281
+ # @return [Integer]
282
+ #
283
+ # @api private
284
+ def retry_count_for_state(state)
285
+ retries_for_state(state, ignore_states: [:connecting]).count
286
+ end
287
+
288
+ # Any message sent before an ACK/NACK was received on the previous transport
289
+ # need to be resent to the Ably service so that a subsequent ACK/NACK is received.
290
+ # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
291
+ # based on the message serial numbers
292
+ #
293
+ # @api private
294
+ def resend_pending_message_ack_queue
295
+ connection.__pending_message_ack_queue__.delete_if do |protocol_message|
296
+ if protocol_message.ack_required?
297
+ connection.__outgoing_message_queue__ << protocol_message
298
+ connection.__outgoing_protocol_msgbus__.publish :protocol_message
299
+ true
300
+ end
301
+ end
302
+ end
303
+
304
+ # @api private
305
+ def suspend_active_channels(error)
306
+ channels.select do |channel|
307
+ channel.attached? || channel.attaching? || channel.detaching?
308
+ end.each do |channel|
309
+ channel.transition_state_machine! :suspended, reason: error
310
+ end
311
+ end
312
+
313
+ # @api private
314
+ def detach_active_channels
315
+ channels.select do |channel|
316
+ channel.attached? || channel.attaching? || channel.detaching?
317
+ end.each do |channel|
318
+ channel.transition_state_machine! :detaching # will always move to detached immediately if connection is closed
319
+ end
320
+ end
321
+
322
+ # @api private
323
+ def fail_active_channels(error)
324
+ channels.select do |channel|
325
+ channel.attached? || channel.attaching? || channel.detaching? || channel.suspended?
326
+ end.each do |channel|
327
+ channel.transition_state_machine! :failed, reason: error
328
+ end
329
+ end
330
+
331
+ # @api private
332
+ def reintialize_failed_chanels
333
+ channels.select do |channel|
334
+ channel.failed?
335
+ end.each do |channel|
336
+ channel.transition_state_machine :initialized
337
+ end
338
+ end
339
+
340
+ # When continuity on a connection is lost all messages
341
+ # whether queued or awaiting an ACK must be NACK'd as we now have a new connection
342
+ def nack_messages_on_all_channels(error)
343
+ channels.each do |channel|
344
+ channel.manager.fail_messages_awaiting_ack error, immediately: true
345
+ channel.manager.fail_queued_messages error
346
+ end
347
+ end
348
+
349
+ # Liveness timer ensures a connection that has not heard from Ably in heartbeat_interval
350
+ # is moved to the disconnected state automatically
351
+ def reset_liveness_timer
352
+ @liveness_timer.cancel if @liveness_timer
353
+ @liveness_timer = EventMachine::Timer.new(connection.heartbeat_interval + 0.1) do
354
+ if connection.connected? && (connection.time_since_connection_confirmed_alive? >= connection.heartbeat_interval)
355
+ msg = "No activity seen from realtime in #{connection.heartbeat_interval}; assuming connection has dropped";
356
+ error = Ably::Exceptions::ConnectionTimeout.new(msg, Ably::Exceptions::Codes::DISCONNECTED, 408)
357
+ connection.transition_state_machine! :disconnected, reason: error
358
+ end
359
+ end
360
+ end
361
+
362
+ private
363
+ def connection
364
+ @connection
365
+ end
366
+
367
+ # Timers used to manage connection state, for internal use by the client library
368
+ #
369
+ # @return [Hash]
370
+ #
371
+ def timers
372
+ @timers
373
+ end
374
+
375
+ def transport
376
+ connection.transport
377
+ end
378
+
379
+ def client
380
+ connection.client
381
+ end
382
+
383
+ def channels
384
+ client.channels
385
+ end
386
+
387
+ def realtime_request_timeout
388
+ connection.defaults.fetch(:realtime_request_timeout)
389
+ end
390
+
391
+ def retry_timeout_for(state)
392
+ connection.defaults.fetch("#{state}_retry_timeout".to_sym) { raise ArgumentError.new("#{state} does not have a configured retry timeout") }
393
+ end
394
+
395
+ def state_has_retry_timeout?(state)
396
+ connection.defaults.has_key?("#{state}_retry_timeout".to_sym)
397
+ end
398
+
399
+ # Create a timer that will execute in timeout_in seconds.
400
+ # If the connection state changes however, cancel the timer
401
+ def create_timeout_timer_whilst_in_state(timer_id, timeout_in)
402
+ raise ArgumentError, 'Block required' unless block_given?
403
+
404
+ timers[timer_id] << EventMachine::Timer.new(timeout_in) do
405
+ yield
406
+ end
407
+ connection.unsafe_once_state_changed { clear_timers timer_id }
408
+ end
409
+
410
+ def clear_timers(key)
411
+ timers.fetch(key, []).each(&:cancel)
412
+ end
413
+
414
+ def get_next_retry_state_info(allow_extra_immediate_retries = 0)
415
+ retry_state = if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
416
+ :suspended
417
+ else
418
+ :disconnected
419
+ end
420
+ {
421
+ state: retry_state,
422
+ pause: next_retry_pause(retry_state, allow_extra_immediate_retries)
423
+ }
424
+ end
425
+
426
+ def next_retry_pause(retry_state, allow_extra_immediate_retries = 0)
427
+ return nil unless state_has_retry_timeout?(retry_state)
428
+
429
+ if retries_for_state(retry_state, ignore_states: [:connecting]).count <= allow_extra_immediate_retries
430
+ 0
431
+ else
432
+ retry_timeout_for(retry_state)
433
+ end
434
+ end
435
+
436
+ def connection_retry_from_suspended_state?
437
+ !retries_for_state(:suspended, ignore_states: [:connecting]).empty?
438
+ end
439
+
440
+ def time_passed_since_disconnected
441
+ time_spent_attempting_state(:disconnected, ignore_states: [:connecting])
442
+ end
443
+
444
+ # Reattempt a connection with a delay based on the configured retry timeout for +from_state+
445
+ #
446
+ # @return [Boolean] True if a connection attempt has been set up, false if no further connection attempts can be made for this state
447
+ #
448
+ def connection_retry_for(from_state)
449
+ if can_reattempt_connect_for_state?(from_state)
450
+ if connection.state == :disconnected && retries_for_state(from_state, ignore_states: [:connecting]).empty?
451
+ logger.debug { "ConnectionManager: Will attempt reconnect immediately as no previous reconnect attempts made in state #{from_state}" }
452
+ EventMachine.next_tick { connection.connect }
453
+ else
454
+ logger.debug { "ConnectionManager: Pausing for #{retry_timeout_for(from_state)}s before attempting to reconnect" }
455
+ create_timeout_timer_whilst_in_state(from_state, retry_timeout_for(from_state)) do
456
+ connection.connect if connection.state == from_state
457
+ end
458
+ end
459
+ true
460
+ end
461
+ end
462
+
463
+ # True if the client library has not exceeded the configured max_time_in_state for the current State
464
+ # For example, if the state is disconnected, and has been in a cycle of disconnected > connect > disconnected
465
+ # so long as the time in this cycle of states is less than max_time_in_state, this will return true
466
+ def can_reattempt_connect_for_state?(state)
467
+ case state
468
+ when :disconnected
469
+ time_spent_attempting_state(:disconnected, ignore_states: [:connecting]) < connection.connection_state_ttl
470
+ when :suspended
471
+ true # suspended state remains indefinitely
472
+ else
473
+ raise ArgumentError, "Connections in state '#{state}' cannot be reattempted"
474
+ end
475
+ end
476
+
477
+ # Returns a float representing the amount of time passed since the first consecutive attempt of this state
478
+ #
479
+ # @param (see #retries_for_state)
480
+ # @return [Float] time passed in seconds
481
+ #
482
+ def time_spent_attempting_state(state, options)
483
+ states = retries_for_state(state, options)
484
+ if states.empty?
485
+ 0
486
+ else
487
+ Time.now.to_f - states.last[:transitioned_at].to_f
488
+ end.to_f
489
+ end
490
+
491
+ # Checks the state change history for the current connection and returns all matching consecutive states.
492
+ # This is useful to determine the number of retries of a particular state on a connection.
493
+ #
494
+ # @param state [Symbol]
495
+ # @param options [Hash]
496
+ # @option options [Array<Symbol>] :ignore_states states that should be ignored when determining consecutive historical retries for `state`.
497
+ # For example, when working out :connecting attempts, :disconnect state changes should be ignored as they are a side effect of a failed :connecting
498
+ #
499
+ # @return [Array<Hash>] Array of consecutive state attempts matching `state` in order of transitioned_at desc
500
+ #
501
+ def retries_for_state(state, options)
502
+ ignore_states = options.fetch(:ignore_states, [])
503
+ allowed_states = Array(state) + Array(ignore_states)
504
+
505
+ state_history_ordered = connection.state_history.reverse
506
+ last_state = state_history_ordered.first
507
+
508
+ # If this method is called after the transition has been persisted to memory,
509
+ # then we need to ignore the current transition when reviewing the number of retries
510
+ if last_state[:state].to_sym == state && last_state.fetch(:transitioned_at).to_f > Time.now.to_f - 0.1
511
+ state_history_ordered.shift
512
+ end
513
+
514
+ state_history_ordered.take_while do |transition|
515
+ allowed_states.include?(transition[:state].to_sym)
516
+ end.select do |transition|
517
+ transition[:state] == state
518
+ end
519
+ end
520
+
521
+ def subscribe_to_transport_events(transport)
522
+ transport.__incoming_protocol_msgbus__.unsafe_on(:protocol_message) do |protocol_message|
523
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
524
+ end
525
+
526
+ transport.unsafe_on(:disconnected) do |reason|
527
+ if connection.closing?
528
+ connection.transition_state_machine :closed
529
+ elsif !connection.closed? && !connection.disconnected? && !connection.failed? && !connection.suspended?
530
+ exception = if reason
531
+ Ably::Exceptions::TransportClosed.new(reason, nil, Ably::Exceptions::Codes::DISCONNECTED)
532
+ else
533
+ Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, Ably::Exceptions::Codes::DISCONNECTED)
534
+ end
535
+ next_state = get_next_retry_state_info
536
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: exception
537
+ end
538
+ end
539
+ end
540
+
541
+ def renew_token_and_reconnect(error)
542
+ if client.auth.token_renewable?
543
+ if currently_renewing_token?
544
+ logger.error { 'ConnectionManager: Attempting to renew token whilst another token renewal is underway. Aborting current renew token request' }
545
+ return
546
+ end
547
+
548
+ logger.info { "ConnectionManager: Token has expired and is renewable, renewing token now" }
549
+
550
+ # Authorize implicitly reconnects, see #RTC8
551
+ client.auth.authorize.tap do |authorize_deferrable|
552
+ authorize_deferrable.callback do |token_details|
553
+ logger.info { 'ConnectionManager: Token renewed succesfully following expiration' }
554
+ end
555
+ end
556
+ else
557
+ logger.error { "ConnectionManager: Token has expired and is not renewable - #{error}" }
558
+ connection.transition_state_machine :failed, reason: error
559
+ end
560
+ end
561
+
562
+ def unsubscribe_from_transport_events(transport)
563
+ transport.__incoming_protocol_msgbus__.unsubscribe
564
+ transport.off
565
+ logger.debug { "ConnectionManager: Unsubscribed from all events from current transport" }
566
+ end
567
+
568
+ def close_connection_when_reactor_is_stopped
569
+ EventMachine.add_shutdown_hook do
570
+ connection.close unless connection.closed? || connection.failed?
571
+ end
572
+ end
573
+
574
+ def currently_renewing_token?
575
+ client.auth.authorization_in_flight?
576
+ end
577
+
578
+ # When reconnected if channel is in ATTACHING, ATTACHED or SUSPENDED state
579
+ # it should explicitly be re-attached by sending a new ATTACH to Ably
580
+ # Spec : RTN15c6, RTN15c7
581
+ def force_reattach_on_channels(error)
582
+ channels.select do |channel|
583
+ channel.attached? || channel.attaching? || channel.suspended?
584
+ end.each do |channel|
585
+ channel.manager.request_reattach error
586
+ end
587
+ end
588
+
589
+ def logger
590
+ connection.logger
591
+ end
592
+ end
593
+ end
594
+ end