ably 0.8.15 → 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.
- checksums.yaml +4 -4
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -2
- data/README.md +5 -1
- data/SPEC.md +1473 -852
- data/ably.gemspec +11 -8
- data/lib/ably/auth.rb +90 -53
- data/lib/ably/exceptions.rb +37 -8
- data/lib/ably/logger.rb +10 -1
- data/lib/ably/models/auth_details.rb +42 -0
- data/lib/ably/models/channel_state_change.rb +18 -4
- data/lib/ably/models/connection_details.rb +6 -3
- data/lib/ably/models/connection_state_change.rb +4 -3
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/message.rb +17 -1
- data/lib/ably/models/message_encoders/base.rb +103 -82
- data/lib/ably/models/message_encoders/base64.rb +1 -1
- data/lib/ably/models/presence_message.rb +16 -1
- data/lib/ably/models/protocol_message.rb +20 -3
- data/lib/ably/models/token_details.rb +11 -1
- data/lib/ably/models/token_request.rb +16 -6
- data/lib/ably/modules/async_wrapper.rb +7 -3
- data/lib/ably/modules/encodeable.rb +51 -12
- data/lib/ably/modules/enum.rb +17 -7
- data/lib/ably/modules/event_emitter.rb +29 -14
- data/lib/ably/modules/model_common.rb +13 -21
- data/lib/ably/modules/state_emitter.rb +7 -4
- data/lib/ably/modules/state_machine.rb +2 -4
- data/lib/ably/modules/uses_state_machine.rb +7 -3
- data/lib/ably/realtime.rb +2 -0
- data/lib/ably/realtime/auth.rb +102 -42
- data/lib/ably/realtime/channel.rb +68 -26
- data/lib/ably/realtime/channel/channel_manager.rb +154 -65
- data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
- data/lib/ably/realtime/client.rb +18 -3
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
- data/lib/ably/realtime/connection.rb +108 -49
- data/lib/ably/realtime/connection/connection_manager.rb +167 -61
- data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
- data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
- data/lib/ably/realtime/presence.rb +70 -45
- data/lib/ably/realtime/presence/members_map.rb +201 -36
- data/lib/ably/realtime/presence/presence_manager.rb +30 -6
- data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
- data/lib/ably/rest.rb +2 -2
- data/lib/ably/rest/channel.rb +5 -5
- data/lib/ably/rest/client.rb +31 -27
- data/lib/ably/rest/middleware/exceptions.rb +1 -3
- data/lib/ably/rest/middleware/logger.rb +2 -2
- data/lib/ably/rest/presence.rb +2 -2
- data/lib/ably/util/pub_sub.rb +1 -1
- data/lib/ably/util/safe_deferrable.rb +26 -0
- data/lib/ably/version.rb +2 -2
- data/spec/acceptance/realtime/auth_spec.rb +470 -111
- data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
- data/spec/acceptance/realtime/channel_spec.rb +1017 -168
- data/spec/acceptance/realtime/client_spec.rb +6 -6
- data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
- data/spec/acceptance/realtime/connection_spec.rb +424 -105
- data/spec/acceptance/realtime/message_spec.rb +52 -23
- data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
- data/spec/acceptance/realtime/presence_spec.rb +1110 -96
- data/spec/acceptance/rest/auth_spec.rb +222 -59
- data/spec/acceptance/rest/base_spec.rb +1 -1
- data/spec/acceptance/rest/channel_spec.rb +1 -2
- data/spec/acceptance/rest/client_spec.rb +104 -48
- data/spec/acceptance/rest/message_spec.rb +42 -15
- data/spec/acceptance/rest/presence_spec.rb +4 -11
- data/spec/rspec_config.rb +2 -1
- data/spec/shared/client_initializer_behaviour.rb +2 -2
- data/spec/shared/safe_deferrable_behaviour.rb +6 -2
- data/spec/spec_helper.rb +4 -2
- data/spec/support/debug_failure_helper.rb +20 -4
- data/spec/support/event_machine_helper.rb +32 -1
- data/spec/unit/auth_spec.rb +4 -11
- data/spec/unit/logger_spec.rb +28 -2
- data/spec/unit/models/auth_details_spec.rb +49 -0
- data/spec/unit/models/channel_state_change_spec.rb +23 -3
- data/spec/unit/models/connection_details_spec.rb +12 -1
- data/spec/unit/models/connection_state_change_spec.rb +15 -4
- data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
- data/spec/unit/models/message_spec.rb +153 -0
- data/spec/unit/models/presence_message_spec.rb +192 -0
- data/spec/unit/models/protocol_message_spec.rb +64 -6
- data/spec/unit/models/token_details_spec.rb +75 -0
- data/spec/unit/models/token_request_spec.rb +74 -0
- data/spec/unit/modules/async_wrapper_spec.rb +2 -1
- data/spec/unit/modules/enum_spec.rb +69 -0
- data/spec/unit/modules/event_emitter_spec.rb +149 -22
- data/spec/unit/modules/state_emitter_spec.rb +9 -3
- data/spec/unit/realtime/client_spec.rb +1 -1
- data/spec/unit/realtime/connection_spec.rb +8 -5
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
- data/spec/unit/realtime/presence_spec.rb +4 -3
- data/spec/unit/rest/client_spec.rb +1 -1
- data/spec/unit/util/crypto_spec.rb +3 -3
- metadata +22 -19
@@ -74,7 +74,12 @@ module Ably::Realtime
|
|
74
74
|
|
75
75
|
def setup_event_handlers
|
76
76
|
connection.unsafe_on(:connected) do
|
77
|
-
|
77
|
+
# Give connection manager enough time to prevent message delivery if necessary
|
78
|
+
# For example, if reconnecting and connection and channel state is lost,
|
79
|
+
# then the queued messages must be NACK'd
|
80
|
+
EventMachine.next_tick do
|
81
|
+
deliver_queued_protocol_messages
|
82
|
+
end
|
78
83
|
end
|
79
84
|
end
|
80
85
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
1
3
|
module Ably
|
2
4
|
module Realtime
|
3
5
|
# The Connection class represents the connection associated with an Ably Realtime instance.
|
@@ -25,8 +27,6 @@ module Ably
|
|
25
27
|
# Connection::STATE.Closed
|
26
28
|
# Connection::STATE.Failed
|
27
29
|
#
|
28
|
-
# Connection emit errors - use `on(:error)` to subscribe to errors
|
29
|
-
#
|
30
30
|
# @example
|
31
31
|
# client = Ably::Realtime::Client.new('key.id:secret')
|
32
32
|
# client.connection.on(:connected) do
|
@@ -42,7 +42,8 @@ module Ably
|
|
42
42
|
include Ably::Modules::SafeYield
|
43
43
|
extend Ably::Modules::Enum
|
44
44
|
|
45
|
-
#
|
45
|
+
# ConnectionState
|
46
|
+
# The permited states for this connection
|
46
47
|
STATE = ruby_enum('STATE',
|
47
48
|
:initialized,
|
48
49
|
:connecting,
|
@@ -53,6 +54,13 @@ module Ably
|
|
53
54
|
:closed,
|
54
55
|
:failed
|
55
56
|
)
|
57
|
+
|
58
|
+
# ConnectionEvent
|
59
|
+
# The permitted connection events that are emitted for this connection
|
60
|
+
EVENT = ruby_enum('EVENT',
|
61
|
+
STATE.to_sym_arr + [:update]
|
62
|
+
)
|
63
|
+
|
56
64
|
include Ably::Modules::StateEmitter
|
57
65
|
include Ably::Modules::UsesStateMachine
|
58
66
|
ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
|
@@ -62,11 +70,13 @@ module Ably
|
|
62
70
|
|
63
71
|
# Defaults for automatic connection recovery and timeouts
|
64
72
|
DEFAULTS = {
|
73
|
+
channel_retry_timeout: 15, # when a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
|
65
74
|
disconnected_retry_timeout: 15, # when the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
|
66
75
|
suspended_retry_timeout: 30, # when the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
|
67
76
|
connection_state_ttl: 120, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected
|
68
|
-
max_connection_state_ttl: nil, # allow a max TTL to be passed in for CI test purposes thus overiding any connection_state_ttl sent from Ably
|
77
|
+
max_connection_state_ttl: nil, # allow a max TTL to be passed in, usually for CI test purposes thus overiding any connection_state_ttl sent from Ably
|
69
78
|
realtime_request_timeout: 10, # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage
|
79
|
+
websocket_heartbeats_disabled: false,
|
70
80
|
}.freeze
|
71
81
|
|
72
82
|
# A unique public identifier for this connection, used to identify this member in presence events and messages
|
@@ -122,9 +132,9 @@ module Ably
|
|
122
132
|
# @api public
|
123
133
|
def initialize(client, options)
|
124
134
|
@client = client
|
125
|
-
@client_serial = -1
|
126
135
|
@__outgoing_message_queue__ = []
|
127
136
|
@__pending_message_ack_queue__ = []
|
137
|
+
reset_client_serial
|
128
138
|
|
129
139
|
@defaults = DEFAULTS.dup
|
130
140
|
options.each do |key, val|
|
@@ -150,7 +160,9 @@ module Ably
|
|
150
160
|
#
|
151
161
|
def close(&success_block)
|
152
162
|
unless closing? || closed?
|
153
|
-
|
163
|
+
unless can_transition_to?(:closing)
|
164
|
+
return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:closing))
|
165
|
+
end
|
154
166
|
transition_state_machine :closing
|
155
167
|
end
|
156
168
|
deferrable_for_state_change_to(STATE.Closed, &success_block)
|
@@ -170,8 +182,11 @@ module Ably
|
|
170
182
|
#
|
171
183
|
def connect(&success_block)
|
172
184
|
unless connecting? || connected?
|
173
|
-
|
174
|
-
|
185
|
+
unless can_transition_to?(:connecting)
|
186
|
+
return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, exception_for_state_change_to(:connecting))
|
187
|
+
end
|
188
|
+
# If connect called in a suspended block, we want to ensure the other callbacks have finished their work first
|
189
|
+
EventMachine.next_tick { transition_state_machine :connecting if can_transition_to?(:connecting) }
|
175
190
|
end
|
176
191
|
|
177
192
|
Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
|
@@ -181,12 +196,12 @@ module Ably
|
|
181
196
|
succeed_callback = deferrable.method(:succeed)
|
182
197
|
fail_callback = deferrable.method(:fail)
|
183
198
|
|
184
|
-
|
199
|
+
unsafe_once(:connected) do
|
185
200
|
deferrable.succeed
|
186
201
|
off(&fail_callback)
|
187
202
|
end
|
188
203
|
|
189
|
-
|
204
|
+
unsafe_once(:failed, :closed, :closing) do
|
190
205
|
deferrable.fail
|
191
206
|
off(&succeed_callback)
|
192
207
|
end
|
@@ -206,38 +221,53 @@ module Ably
|
|
206
221
|
# puts "Ping took #{elapsed_s}s"
|
207
222
|
# end
|
208
223
|
#
|
209
|
-
# @return [
|
224
|
+
# @return [Ably::Util::SafeDeferrable]
|
210
225
|
#
|
211
226
|
def ping(&block)
|
212
|
-
|
213
|
-
|
227
|
+
if initialized? || suspended? || closing? || closed? || failed?
|
228
|
+
error = Ably::Models::ErrorInfo.new(message: "Cannot send a ping when the connection is #{state}", code: 80003)
|
229
|
+
return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
|
230
|
+
end
|
214
231
|
|
215
|
-
|
216
|
-
|
232
|
+
Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
|
233
|
+
started = nil
|
234
|
+
finished = false
|
235
|
+
ping_id = SecureRandom.hex(16)
|
236
|
+
heartbeat_action = Ably::Models::ProtocolMessage::ACTION.Heartbeat
|
237
|
+
|
238
|
+
wait_for_ping = Proc.new do |protocol_message|
|
239
|
+
next if finished
|
240
|
+
if protocol_message.action == heartbeat_action && protocol_message.id == ping_id
|
241
|
+
finished = true
|
242
|
+
__incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
|
243
|
+
time_passed = Time.now.to_f - started.to_f
|
244
|
+
deferrable.succeed time_passed
|
245
|
+
safe_yield block, time_passed if block_given?
|
246
|
+
end
|
247
|
+
end
|
217
248
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
__incoming_protocol_msgbus__.
|
223
|
-
time_passed = Time.now.to_f - started.to_f
|
224
|
-
safe_yield block, time_passed if block_given?
|
249
|
+
once_or_if(STATE.Connected) do
|
250
|
+
next if finished
|
251
|
+
started = Time.now
|
252
|
+
send_protocol_message action: heartbeat_action.to_i, id: ping_id
|
253
|
+
__incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
|
225
254
|
end
|
226
|
-
end
|
227
255
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
end
|
256
|
+
once_or_if([:suspended, :closing, :closed, :failed]) do
|
257
|
+
next if finished
|
258
|
+
finished = true
|
259
|
+
deferrable.fail Ably::Models::ErrorInfo.new(message: "Ping failed as connection has changed state to #{state}", code: 80003)
|
260
|
+
end
|
234
261
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
262
|
+
EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do
|
263
|
+
next if finished
|
264
|
+
finished = true
|
265
|
+
__incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
|
266
|
+
error_msg = "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s"
|
267
|
+
logger.warn { error_msg }
|
268
|
+
deferrable.fail Ably::Models::ErrorInfo.new(message: error_msg, code: 50003)
|
269
|
+
safe_yield block, nil if block_given?
|
270
|
+
end
|
241
271
|
end
|
242
272
|
end
|
243
273
|
|
@@ -364,7 +394,7 @@ module Ably
|
|
364
394
|
Ably::Models::ProtocolMessage.new(protocol_message, logger: logger).tap do |message|
|
365
395
|
add_message_to_outgoing_queue message
|
366
396
|
notify_message_dispatcher_of_new_message message
|
367
|
-
logger.debug
|
397
|
+
logger.debug { "Connection: Prot msg queued =>: #{message.action} #{message}" }
|
368
398
|
end
|
369
399
|
end
|
370
400
|
end
|
@@ -387,21 +417,24 @@ module Ably
|
|
387
417
|
client.auth.auth_params.tap do |auth_deferrable|
|
388
418
|
auth_deferrable.callback do |auth_params|
|
389
419
|
url_params = auth_params.merge(
|
390
|
-
format:
|
391
|
-
echo:
|
392
|
-
v:
|
393
|
-
lib:
|
420
|
+
format: client.protocol,
|
421
|
+
echo: client.echo_messages,
|
422
|
+
v: Ably::PROTOCOL_VERSION,
|
423
|
+
lib: client.rest_client.lib_version_id,
|
394
424
|
)
|
395
425
|
|
426
|
+
# Use native websocket heartbeats if possible
|
427
|
+
url_params['heartbeats'] = 'false' unless defaults.fetch(:websocket_heartbeats_disabled)
|
428
|
+
|
396
429
|
url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
|
397
430
|
|
398
431
|
if connection_resumable?
|
399
432
|
url_params.merge! resume: key, connection_serial: serial
|
400
|
-
logger.debug "Resuming connection key #{key} with serial #{serial}"
|
433
|
+
logger.debug { "Resuming connection key #{key} with serial #{serial}" }
|
401
434
|
elsif connection_recoverable?
|
402
|
-
url_params.merge! recover: connection_recover_parts[:recover],
|
403
|
-
logger.debug "Recovering connection with key #{client.recover}"
|
404
|
-
|
435
|
+
url_params.merge! recover: connection_recover_parts[:recover], connectionSerial: connection_recover_parts[:connection_serial]
|
436
|
+
logger.debug { "Recovering connection with key #{client.recover}" }
|
437
|
+
unsafe_once(:connected, :closed, :failed) do
|
405
438
|
client.disable_automatic_connection_recovery
|
406
439
|
end
|
407
440
|
end
|
@@ -412,7 +445,7 @@ module Ably
|
|
412
445
|
|
413
446
|
determine_host do |host|
|
414
447
|
begin
|
415
|
-
logger.debug "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}"
|
448
|
+
logger.debug { "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}" }
|
416
449
|
@transport = create_transport(host, port, url) do |websocket_transport|
|
417
450
|
websocket_deferrable.succeed websocket_transport
|
418
451
|
end
|
@@ -451,7 +484,7 @@ module Ably
|
|
451
484
|
|
452
485
|
# Executes registered callbacks for a successful connection resume event
|
453
486
|
# @api private
|
454
|
-
def
|
487
|
+
def trigger_resumed
|
455
488
|
resume_callbacks.each(&:call)
|
456
489
|
end
|
457
490
|
|
@@ -490,6 +523,33 @@ module Ably
|
|
490
523
|
@connection_state_ttl = val
|
491
524
|
end
|
492
525
|
|
526
|
+
# @api private
|
527
|
+
def heartbeat_interval
|
528
|
+
# See RTN23a
|
529
|
+
(details && details.max_idle_interval).to_i +
|
530
|
+
defaults.fetch(:realtime_request_timeout)
|
531
|
+
end
|
532
|
+
|
533
|
+
# Resets the client serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage}
|
534
|
+
# (see #client_serial)
|
535
|
+
# @api private
|
536
|
+
def reset_client_serial
|
537
|
+
@client_serial = -1
|
538
|
+
end
|
539
|
+
|
540
|
+
# When a hearbeat or any other message from Ably is received
|
541
|
+
# we know it's alive, see #RTN23
|
542
|
+
# @api private
|
543
|
+
def set_connection_confirmed_alive
|
544
|
+
@last_liveness_event = Time.now
|
545
|
+
manager.reset_liveness_timer
|
546
|
+
end
|
547
|
+
|
548
|
+
# @api private
|
549
|
+
def time_since_connection_confirmed_alive?
|
550
|
+
Time.now.to_i - @last_liveness_event.to_i
|
551
|
+
end
|
552
|
+
|
493
553
|
# As we are using a state machine, do not allow change_state to be used
|
494
554
|
# #transition_state_machine must be used instead
|
495
555
|
private :change_state
|
@@ -500,9 +560,8 @@ module Ably
|
|
500
560
|
# Note that this is different to the connection serial that contains the last known serial number
|
501
561
|
# received from the server.
|
502
562
|
#
|
503
|
-
# A
|
504
|
-
# A connection serial guarantees the server has received the message and is thus used for connection
|
505
|
-
# recovery and resumes.
|
563
|
+
# A message serial number does not guarantee a message has been received, only sent.
|
564
|
+
# A connection serial guarantees the server has received the message and is thus used for connection recovery and resumes.
|
506
565
|
# @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
|
507
566
|
def client_serial
|
508
567
|
@client_serial
|
@@ -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::
|
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::
|
74
|
-
|
75
|
-
|
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.
|
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
|
93
|
-
|
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
|
-
|
148
|
-
|
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
|
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
|
-
|
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]).
|
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
|
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
|
-
|
403
|
-
logger.info "ConnectionManager: Token has expired and is renewable, renewing token now"
|
404
|
-
|
405
|
-
client.auth.authorise(nil, force: true).tap do |authorise_deferrable|
|
406
|
-
authorise_deferrable.callback do |token_details|
|
407
|
-
logger.info 'ConnectionManager: Token renewed succesfully following expiration'
|
408
|
-
|
409
|
-
connection.once_state_changed { @renewing_token = false }
|
512
|
+
logger.info { "ConnectionManager: Token has expired and is renewable, renewing token now" }
|
410
513
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
end
|
416
|
-
end
|
417
|
-
|
418
|
-
authorise_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
|
514
|
+
# Authorize implicitly reconnects, see #RTC8
|
515
|
+
client.auth.authorize.tap do |authorize_deferrable|
|
516
|
+
authorize_deferrable.callback do |token_details|
|
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
|
443
|
-
|
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
|
-
|
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
|
-
|
451
|
-
channel.manager.suspend error
|
557
|
+
channel.manager.request_reattach reason: error
|
452
558
|
end
|
453
559
|
end
|
454
560
|
|