ably 0.8.15 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|