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.
- checksums.yaml +7 -0
- data/lib/ably/agent.rb +8 -0
- data/lib/ably/auth.rb +791 -0
- data/lib/ably/exceptions.rb +169 -0
- data/lib/ably/logger.rb +119 -0
- data/lib/ably/models/auth_details.rb +45 -0
- data/lib/ably/models/channel_details.rb +69 -0
- data/lib/ably/models/channel_metrics.rb +107 -0
- data/lib/ably/models/channel_occupancy.rb +50 -0
- data/lib/ably/models/channel_options.rb +120 -0
- data/lib/ably/models/channel_state_change.rb +84 -0
- data/lib/ably/models/channel_status.rb +63 -0
- data/lib/ably/models/cipher_params.rb +137 -0
- data/lib/ably/models/connection_details.rb +149 -0
- data/lib/ably/models/connection_state_change.rb +83 -0
- data/lib/ably/models/delta_extras.rb +28 -0
- data/lib/ably/models/device_details.rb +126 -0
- data/lib/ably/models/device_push_details.rb +94 -0
- data/lib/ably/models/error_info.rb +104 -0
- data/lib/ably/models/http_paginated_response.rb +112 -0
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
- data/lib/ably/models/message.rb +223 -0
- data/lib/ably/models/message_encoders/base.rb +135 -0
- data/lib/ably/models/message_encoders/base64.rb +40 -0
- data/lib/ably/models/message_encoders/cipher.rb +84 -0
- data/lib/ably/models/message_encoders/json.rb +34 -0
- data/lib/ably/models/message_encoders/utf8.rb +26 -0
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_result.rb +201 -0
- data/lib/ably/models/presence_message.rb +244 -0
- data/lib/ably/models/protocol_message.rb +288 -0
- data/lib/ably/models/push_channel_subscription.rb +114 -0
- data/lib/ably/models/stats.rb +242 -0
- data/lib/ably/models/stats_types.rb +107 -0
- data/lib/ably/models/token_details.rb +148 -0
- data/lib/ably/models/token_request.rb +180 -0
- data/lib/ably/modules/ably.rb +26 -0
- data/lib/ably/modules/async_wrapper.rb +69 -0
- data/lib/ably/modules/channels_collection.rb +105 -0
- data/lib/ably/modules/conversions.rb +153 -0
- data/lib/ably/modules/encodeable.rb +111 -0
- data/lib/ably/modules/enum.rb +230 -0
- data/lib/ably/modules/event_emitter.rb +200 -0
- data/lib/ably/modules/event_machine_helpers.rb +26 -0
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/http_helpers.rb +41 -0
- data/lib/ably/modules/message_emitter.rb +78 -0
- data/lib/ably/modules/message_pack.rb +14 -0
- data/lib/ably/modules/model_common.rb +84 -0
- data/lib/ably/modules/safe_deferrable.rb +83 -0
- data/lib/ably/modules/safe_yield.rb +41 -0
- data/lib/ably/modules/state_emitter.rb +181 -0
- data/lib/ably/modules/state_machine.rb +59 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +111 -0
- data/lib/ably/realtime/auth.rb +283 -0
- data/lib/ably/realtime/channel/channel_manager.rb +272 -0
- data/lib/ably/realtime/channel/channel_properties.rb +40 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
- data/lib/ably/realtime/channel/publisher.rb +82 -0
- data/lib/ably/realtime/channel/push_channel.rb +69 -0
- data/lib/ably/realtime/channel.rb +440 -0
- data/lib/ably/realtime/channels.rb +71 -0
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
- data/lib/ably/realtime/client.rb +363 -0
- data/lib/ably/realtime/connection/connection_manager.rb +594 -0
- data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
- data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
- data/lib/ably/realtime/connection.rb +738 -0
- data/lib/ably/realtime/models/nil_channel.rb +30 -0
- data/lib/ably/realtime/presence/members_map.rb +395 -0
- data/lib/ably/realtime/presence/presence_manager.rb +78 -0
- data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
- data/lib/ably/realtime/presence.rb +502 -0
- data/lib/ably/realtime/push/admin.rb +75 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/realtime/push.rb +25 -0
- data/lib/ably/realtime/recovery_key_context.rb +36 -0
- data/lib/ably/realtime.rb +60 -0
- data/lib/ably/rest/channel/push_channel.rb +59 -0
- data/lib/ably/rest/channel.rb +194 -0
- data/lib/ably/rest/channels.rb +47 -0
- data/lib/ably/rest/client.rb +727 -0
- data/lib/ably/rest/middleware/encoder.rb +49 -0
- data/lib/ably/rest/middleware/exceptions.rb +53 -0
- data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
- data/lib/ably/rest/middleware/logger.rb +58 -0
- data/lib/ably/rest/middleware/parse_json.rb +27 -0
- data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
- data/lib/ably/rest/presence.rb +113 -0
- data/lib/ably/rest/push/admin.rb +58 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +114 -0
- data/lib/ably/rest/push.rb +25 -0
- data/lib/ably/rest.rb +40 -0
- data/lib/ably/util/ably_extensions.rb +29 -0
- data/lib/ably/util/crypto.rb +134 -0
- data/lib/ably/util/pub_sub.rb +45 -0
- data/lib/ably/util/safe_deferrable.rb +44 -0
- data/lib/ably/version.rb +7 -0
- data/lib/ably.rb +16 -0
- 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
|