ably 0.1.6 → 0.2.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/.gitignore +2 -0
- data/.travis.yml +9 -0
- data/LICENSE.txt +1 -1
- data/README.md +8 -1
- data/Rakefile +10 -0
- data/ably.gemspec +18 -18
- data/lib/ably.rb +6 -5
- data/lib/ably/auth.rb +11 -14
- data/lib/ably/exceptions.rb +18 -15
- data/lib/ably/logger.rb +102 -0
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/message.rb +19 -5
- data/lib/ably/models/message_encoders/base.rb +107 -0
- data/lib/ably/models/message_encoders/base64.rb +39 -0
- data/lib/ably/models/message_encoders/cipher.rb +80 -0
- data/lib/ably/models/message_encoders/json.rb +33 -0
- data/lib/ably/models/message_encoders/utf8.rb +33 -0
- data/lib/ably/models/paginated_resource.rb +23 -6
- data/lib/ably/models/presence_message.rb +19 -7
- data/lib/ably/models/protocol_message.rb +5 -4
- data/lib/ably/models/token.rb +2 -2
- data/lib/ably/modules/channels_collection.rb +0 -3
- data/lib/ably/modules/conversions.rb +3 -3
- data/lib/ably/modules/encodeable.rb +68 -0
- data/lib/ably/modules/event_emitter.rb +10 -4
- data/lib/ably/modules/event_machine_helpers.rb +6 -4
- data/lib/ably/modules/http_helpers.rb +7 -2
- data/lib/ably/modules/model_common.rb +2 -0
- data/lib/ably/modules/state_emitter.rb +10 -1
- data/lib/ably/realtime.rb +19 -12
- data/lib/ably/realtime/channel.rb +26 -13
- data/lib/ably/realtime/client.rb +31 -7
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -3
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +13 -4
- data/lib/ably/realtime/connection.rb +152 -46
- data/lib/ably/realtime/connection/connection_manager.rb +168 -0
- data/lib/ably/realtime/connection/connection_state_machine.rb +56 -33
- data/lib/ably/realtime/connection/websocket_transport.rb +56 -29
- data/lib/ably/{models → realtime/models}/nil_channel.rb +1 -1
- data/lib/ably/realtime/presence.rb +38 -13
- data/lib/ably/rest.rb +7 -5
- data/lib/ably/rest/channel.rb +24 -3
- data/lib/ably/rest/client.rb +56 -17
- data/lib/ably/rest/middleware/encoder.rb +49 -0
- data/lib/ably/rest/middleware/exceptions.rb +3 -2
- data/lib/ably/rest/middleware/logger.rb +37 -0
- data/lib/ably/rest/presence.rb +10 -2
- data/lib/ably/util/crypto.rb +57 -29
- data/lib/ably/util/pub_sub.rb +11 -0
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_spec.rb +65 -7
- data/spec/acceptance/realtime/connection_spec.rb +123 -27
- data/spec/acceptance/realtime/message_spec.rb +319 -34
- data/spec/acceptance/realtime/presence_history_spec.rb +58 -0
- data/spec/acceptance/realtime/presence_spec.rb +160 -18
- data/spec/acceptance/rest/auth_spec.rb +93 -49
- data/spec/acceptance/rest/base_spec.rb +10 -10
- data/spec/acceptance/rest/channel_spec.rb +35 -19
- data/spec/acceptance/rest/channels_spec.rb +8 -8
- data/spec/acceptance/rest/message_spec.rb +224 -0
- data/spec/acceptance/rest/presence_spec.rb +159 -23
- data/spec/acceptance/rest/stats_spec.rb +5 -5
- data/spec/acceptance/rest/time_spec.rb +4 -4
- data/spec/integration/rest/auth.rb +1 -1
- data/spec/resources/crypto-data-128.json +56 -0
- data/spec/resources/crypto-data-256.json +56 -0
- data/spec/rspec_config.rb +39 -0
- data/spec/spec_helper.rb +4 -42
- data/spec/support/api_helper.rb +1 -1
- data/spec/support/event_machine_helper.rb +0 -5
- data/spec/support/protocol_msgbus_helper.rb +3 -3
- data/spec/support/test_app.rb +3 -3
- data/spec/unit/logger_spec.rb +135 -0
- data/spec/unit/models/message_encoders/base64_spec.rb +181 -0
- data/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
- data/spec/unit/models/message_encoders/json_spec.rb +135 -0
- data/spec/unit/models/message_encoders/utf8_spec.rb +100 -0
- data/spec/unit/models/message_spec.rb +16 -1
- data/spec/unit/models/paginated_resource_spec.rb +46 -0
- data/spec/unit/models/presence_message_spec.rb +18 -5
- data/spec/unit/models/token_spec.rb +1 -1
- data/spec/unit/modules/event_emitter_spec.rb +24 -10
- data/spec/unit/realtime/channel_spec.rb +3 -3
- data/spec/unit/realtime/channels_spec.rb +1 -1
- data/spec/unit/realtime/client_spec.rb +44 -2
- data/spec/unit/realtime/connection_spec.rb +2 -2
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +4 -4
- data/spec/unit/realtime/presence_spec.rb +1 -1
- data/spec/unit/realtime/realtime_spec.rb +3 -3
- data/spec/unit/realtime/websocket_transport_spec.rb +24 -0
- data/spec/unit/rest/channels_spec.rb +1 -1
- data/spec/unit/rest/client_spec.rb +45 -10
- data/spec/unit/util/crypto_spec.rb +82 -0
- data/spec/unit/{modules → util}/pub_sub_spec.rb +13 -1
- metadata +43 -12
- data/spec/acceptance/crypto.rb +0 -63
@@ -8,6 +8,7 @@ module Ably::Realtime
|
|
8
8
|
def initialize(client, connection)
|
9
9
|
@client = client
|
10
10
|
@connection = connection
|
11
|
+
|
11
12
|
subscribe_to_incoming_protocol_messages
|
12
13
|
end
|
13
14
|
|
@@ -21,7 +22,7 @@ module Ably::Realtime
|
|
21
22
|
def get_channel(channel_name)
|
22
23
|
channels.fetch(channel_name) do
|
23
24
|
logger.warn "Received channel message for non-existent channel"
|
24
|
-
Ably::Models::NilChannel.new
|
25
|
+
Ably::Realtime::Models::NilChannel.new
|
25
26
|
end
|
26
27
|
end
|
27
28
|
|
@@ -64,7 +65,9 @@ module Ably::Realtime
|
|
64
65
|
when ACTION.Error
|
65
66
|
logger.error "Error received: #{protocol_message.error}"
|
66
67
|
if protocol_message.channel && !protocol_message.has_message_serial?
|
67
|
-
|
68
|
+
dispatch_channel_error protocol_message
|
69
|
+
else
|
70
|
+
connection.transition_state_machine :failed, protocol_message.error
|
68
71
|
end
|
69
72
|
|
70
73
|
when ACTION.Attach
|
@@ -90,6 +93,14 @@ module Ably::Realtime
|
|
90
93
|
end
|
91
94
|
end
|
92
95
|
|
96
|
+
def dispatch_channel_error(protocol_message)
|
97
|
+
if !protocol_message.has_message_serial?
|
98
|
+
get_channel(protocol_message.channel).change_state Ably::Realtime::Channel::STATE.Failed, protocol_message.error
|
99
|
+
else
|
100
|
+
logger.fatal "Cannot process ProtocolMessage as not yet implemented: #{protocol_message}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
93
104
|
def update_connection_id(protocol_message)
|
94
105
|
if protocol_message.connection_id && (protocol_message.connection_id != connection.id)
|
95
106
|
logger.debug "New connection ID set to #{protocol_message.connection_id}"
|
@@ -136,7 +147,7 @@ module Ably::Realtime
|
|
136
147
|
end
|
137
148
|
|
138
149
|
def subscribe_to_incoming_protocol_messages
|
139
|
-
connection.__incoming_protocol_msgbus__.subscribe(:
|
150
|
+
connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |*args|
|
140
151
|
dispatch_protocol_message *args
|
141
152
|
end
|
142
153
|
end
|
@@ -11,6 +11,7 @@ module Ably::Realtime
|
|
11
11
|
def initialize(client, connection)
|
12
12
|
@client = client
|
13
13
|
@connection = connection
|
14
|
+
|
14
15
|
subscribe_to_outgoing_protocol_message_queue
|
15
16
|
setup_event_handlers
|
16
17
|
end
|
@@ -34,19 +35,27 @@ module Ably::Realtime
|
|
34
35
|
connection.__pending_message_queue__
|
35
36
|
end
|
36
37
|
|
38
|
+
def current_transport_outgoing_message_bus
|
39
|
+
connection.transport.__outgoing_protocol_msgbus__
|
40
|
+
end
|
41
|
+
|
37
42
|
def deliver_queued_protocol_messages
|
38
43
|
condition = -> { can_send_messages? && messages_in_outgoing_queue? }
|
39
44
|
|
40
45
|
non_blocking_loop_while(condition) do
|
41
46
|
protocol_message = outgoing_queue.shift
|
42
|
-
|
43
|
-
|
44
|
-
|
47
|
+
current_transport_outgoing_message_bus.publish :protocol_message, protocol_message
|
48
|
+
|
49
|
+
if protocol_message.ack_required?
|
50
|
+
pending_queue << protocol_message
|
51
|
+
else
|
52
|
+
protocol_message.succeed protocol_message
|
53
|
+
end
|
45
54
|
end
|
46
55
|
end
|
47
56
|
|
48
57
|
def subscribe_to_outgoing_protocol_message_queue
|
49
|
-
connection.__outgoing_protocol_msgbus__.subscribe(:
|
58
|
+
connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |*args|
|
50
59
|
deliver_queued_protocol_messages
|
51
60
|
end
|
52
61
|
end
|
@@ -23,6 +23,12 @@ module Ably
|
|
23
23
|
# Connection::STATE.Closed
|
24
24
|
# Connection::STATE.Failed
|
25
25
|
#
|
26
|
+
# @example
|
27
|
+
# client = Ably::Realtime::Client.new('key.id:secret')
|
28
|
+
# client.connection.on(:connected) do
|
29
|
+
# puts "Connected with connection ID: #{client.connection.id}"
|
30
|
+
# end
|
31
|
+
#
|
26
32
|
# @!attribute [r] state
|
27
33
|
# @return {Ably::Realtime::Connection::STATE} connection state
|
28
34
|
# @!attribute [r] id
|
@@ -31,6 +37,7 @@ module Ably
|
|
31
37
|
# @return {Ably::Models::ErrorInfo} error information associated with a connection failure
|
32
38
|
class Connection
|
33
39
|
include Ably::Modules::EventEmitter
|
40
|
+
include Ably::Modules::Conversions
|
34
41
|
extend Ably::Modules::Enum
|
35
42
|
|
36
43
|
# Valid Connection states
|
@@ -52,6 +59,11 @@ module Ably
|
|
52
59
|
# @return {Ably::Realtime::Connection::WebsocketTransport}
|
53
60
|
attr_reader :transport
|
54
61
|
|
62
|
+
# @api private
|
63
|
+
# The connection manager responsible for creating, maintaining and closing the connection and underlying transport
|
64
|
+
# @return {Ably::Realtime::Connection::ConnectionManager}
|
65
|
+
attr_reader :manager
|
66
|
+
|
55
67
|
# @api private
|
56
68
|
# An internal queue used to manage unsent outgoing messages. You should never interface with this array directly
|
57
69
|
# @return [Array]
|
@@ -62,11 +74,6 @@ module Ably
|
|
62
74
|
# @return [Array]
|
63
75
|
attr_reader :__pending_message_queue__
|
64
76
|
|
65
|
-
# @api private
|
66
|
-
# Timers used to manage connection state, for internal use by the client library
|
67
|
-
# @return [Hash]
|
68
|
-
attr_reader :timers
|
69
|
-
|
70
77
|
# @api public
|
71
78
|
def initialize(client)
|
72
79
|
@client = client
|
@@ -75,9 +82,6 @@ module Ably
|
|
75
82
|
@__outgoing_message_queue__ = []
|
76
83
|
@__pending_message_queue__ = []
|
77
84
|
|
78
|
-
@timers = Hash.new { |hash, key| hash[key] = [] }
|
79
|
-
@timers[:initializer] << EventMachine::Timer.new(0.001) { connect }
|
80
|
-
|
81
85
|
Client::IncomingMessageDispatcher.new client, self
|
82
86
|
Client::OutgoingMessageDispatcher.new client, self
|
83
87
|
|
@@ -86,6 +90,7 @@ module Ably
|
|
86
90
|
end
|
87
91
|
|
88
92
|
@state_machine = ConnectionStateMachine.new(self)
|
93
|
+
@manager = ConnectionManager.new(self)
|
89
94
|
@state = STATE(state_machine.current_state)
|
90
95
|
end
|
91
96
|
|
@@ -95,13 +100,13 @@ module Ably
|
|
95
100
|
#
|
96
101
|
# @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Closed state
|
97
102
|
#
|
98
|
-
# @return
|
103
|
+
# @return [void]
|
99
104
|
def close(&block)
|
100
105
|
if closed?
|
101
106
|
block.call self
|
102
107
|
else
|
103
108
|
EventMachine.next_tick do
|
104
|
-
|
109
|
+
transition_state_machine(:closed)
|
105
110
|
end
|
106
111
|
once(STATE.Closed) { block.call self } if block_given?
|
107
112
|
end
|
@@ -112,46 +117,107 @@ module Ably
|
|
112
117
|
#
|
113
118
|
# @yield [Ably::Realtime::Connection] block is called as soon as this connection is in the Connected state
|
114
119
|
#
|
115
|
-
# @return
|
120
|
+
# @return [void]
|
116
121
|
def connect(&block)
|
117
122
|
if connected?
|
118
123
|
block.call self
|
119
124
|
else
|
120
|
-
|
125
|
+
transition_state_machine(:connecting) unless connecting?
|
121
126
|
once(STATE.Connected) { block.call self } if block_given?
|
122
127
|
end
|
123
128
|
end
|
124
129
|
|
130
|
+
# Sends a ping to Ably and yields the provided block when a heartbeat ping request is echoed from the server.
|
131
|
+
# This can be useful for measuring true roundtrip client to Ably server latency for a simple message, or checking that an underlying transport is responding currently.
|
132
|
+
# The elapsed milliseconds is passed as an argument to the block and represents the time taken to echo a ping heartbeat once the connection is in the `:connected` state.
|
133
|
+
#
|
134
|
+
# @yield [Integer] if a block is passed to this method, then this block will be called once the ping heartbeat is received with the time elapsed in milliseconds
|
135
|
+
#
|
136
|
+
# @example
|
137
|
+
# client = Ably::Rest::Client.new(api_key: 'key.id:secret')
|
138
|
+
# client.connection.ping do |ms_elapsed|
|
139
|
+
# puts "Ping took #{ms_elapsed}ms"
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
def ping(&block)
|
143
|
+
raise RuntimeError, 'Cannot send a ping when connection is in a closed or failed state' if closed? || failed?
|
144
|
+
|
145
|
+
started = nil
|
146
|
+
|
147
|
+
wait_for_ping = Proc.new do |protocol_message|
|
148
|
+
if protocol_message.action == Ably::Models::ProtocolMessage::ACTION.Heartbeat
|
149
|
+
__incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
|
150
|
+
time_passed = (Time.now.to_f * 1000 - started.to_f * 1000).to_i
|
151
|
+
block.call time_passed if block_given?
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
once(STATE.Connected) do
|
156
|
+
started = Time.now
|
157
|
+
send_protocol_message action: Ably::Models::ProtocolMessage::ACTION.Heartbeat.to_i
|
158
|
+
__incoming_protocol_msgbus__.subscribe :protocol_message, &wait_for_ping
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
125
162
|
# Reconfigure the current connection ID
|
126
|
-
# @return
|
163
|
+
# @return [void]
|
127
164
|
# @api private
|
128
165
|
def update_connection_id(connection_id)
|
129
166
|
@id = connection_id
|
130
167
|
end
|
131
168
|
|
132
|
-
#
|
133
|
-
#
|
169
|
+
# Call #transition_to on {Ably::Realtime::Connection::ConnectionStateMachine}
|
170
|
+
#
|
171
|
+
# @return [Boolean] true if new_state can be transitioned to by state machine
|
134
172
|
# @api private
|
135
|
-
def transition_state_machine(new_state)
|
136
|
-
state_machine.transition_to(new_state)
|
173
|
+
def transition_state_machine(new_state, emit_object = nil)
|
174
|
+
state_machine.transition_to(new_state, emit_object)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Call #transition_to! on {Ably::Realtime::Connection::ConnectionStateMachine}.
|
178
|
+
# An exception wil be raised if new_state cannot be transitioned to by state machine
|
179
|
+
#
|
180
|
+
# @return [void]
|
181
|
+
# @api private
|
182
|
+
def transition_state_machine!(new_state, emit_object = nil)
|
183
|
+
state_machine.transition_to!(new_state, emit_object)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Provides an internal method for the {Ably::Realtime::Connection} state to match the {Ably::Realtime::Connection::ConnectionStateMachine}'s state
|
187
|
+
# @api private
|
188
|
+
def synchronize_state_with_statemachine(*args)
|
189
|
+
log_state_machine_state_change
|
190
|
+
change_state state_machine.current_state, state_machine.last_transition.metadata
|
137
191
|
end
|
138
192
|
|
139
193
|
# @!attribute [r] __outgoing_protocol_msgbus__
|
140
|
-
# @return [Ably::Util::PubSub] Client library internal outgoing message bus
|
194
|
+
# @return [Ably::Util::PubSub] Client library internal outgoing protocol message bus
|
141
195
|
# @api private
|
142
196
|
def __outgoing_protocol_msgbus__
|
143
197
|
@__outgoing_protocol_msgbus__ ||= create_pub_sub_message_bus
|
144
198
|
end
|
145
199
|
|
146
200
|
# @!attribute [r] __incoming_protocol_msgbus__
|
147
|
-
# @return [Ably::Util::PubSub] Client library internal incoming message bus
|
201
|
+
# @return [Ably::Util::PubSub] Client library internal incoming protocol message bus
|
148
202
|
# @api private
|
149
203
|
def __incoming_protocol_msgbus__
|
150
204
|
@__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus
|
151
205
|
end
|
152
206
|
|
207
|
+
# @!attribute [r] host
|
208
|
+
# @return [String] The default host name used for this connection
|
209
|
+
def host
|
210
|
+
client.endpoint.host
|
211
|
+
end
|
212
|
+
|
213
|
+
# @!attribute [r] port
|
214
|
+
# @return [Integer] The default port used for this connection
|
215
|
+
def port
|
216
|
+
client.use_tls? ? 443 : 80
|
217
|
+
end
|
218
|
+
|
153
219
|
# @!attribute [r] logger
|
154
|
-
# @return [Logger] The Logger
|
220
|
+
# @return [Logger] The {Ably::Logger} for this client.
|
155
221
|
# Configure the log_level with the `:log_level` option, refer to {Ably::Realtime::Client#initialize}
|
156
222
|
def logger
|
157
223
|
client.logger
|
@@ -161,67 +227,99 @@ module Ably
|
|
161
227
|
# ready to be sent
|
162
228
|
#
|
163
229
|
# @param [Ably::Models::ProtocolMessage] protocol_message
|
164
|
-
# @return
|
230
|
+
# @return [void]
|
165
231
|
# @api private
|
166
232
|
def send_protocol_message(protocol_message)
|
167
233
|
add_message_serial_if_ack_required_to(protocol_message) do
|
168
|
-
Models::ProtocolMessage.new(protocol_message).tap do |protocol_message|
|
234
|
+
Ably::Models::ProtocolMessage.new(protocol_message).tap do |protocol_message|
|
169
235
|
add_message_to_outgoing_queue protocol_message
|
170
236
|
notify_message_dispatcher_of_new_message protocol_message
|
171
|
-
logger.debug("Prot msg queued =>: #{protocol_message.action} #{protocol_message}")
|
237
|
+
logger.debug("Connection: Prot msg queued =>: #{protocol_message.action} #{protocol_message}")
|
172
238
|
end
|
173
239
|
end
|
174
240
|
end
|
175
241
|
|
242
|
+
# @api private
|
176
243
|
def add_message_to_outgoing_queue(protocol_message)
|
177
244
|
__outgoing_message_queue__ << protocol_message
|
178
245
|
end
|
179
246
|
|
247
|
+
# @api private
|
180
248
|
def notify_message_dispatcher_of_new_message(protocol_message)
|
181
|
-
__outgoing_protocol_msgbus__.publish :
|
249
|
+
__outgoing_protocol_msgbus__.publish :protocol_message, protocol_message
|
182
250
|
end
|
183
251
|
|
184
|
-
#
|
185
|
-
# @
|
252
|
+
# @!attribute [r] previous_state
|
253
|
+
# @return [Ably::Realtime::Connection::STATE,nil] The previous state for this connection
|
186
254
|
# @api private
|
187
|
-
def
|
188
|
-
if
|
189
|
-
|
255
|
+
def previous_state
|
256
|
+
if state_machine.previous_state
|
257
|
+
STATE(state_machine.previous_state)
|
190
258
|
end
|
259
|
+
end
|
191
260
|
|
192
|
-
|
193
|
-
|
261
|
+
# @!attribute [r] state_history
|
262
|
+
# @return [Array<Hash>] All previous states including the current state in date ascending order with Hash properties :state, :metadata, :transitioned_at
|
263
|
+
# @api private
|
264
|
+
def state_history
|
265
|
+
state_machine.history.map do |transition|
|
266
|
+
{
|
267
|
+
state: STATE(transition.to_state),
|
268
|
+
metadata: transition.metadata,
|
269
|
+
transitioned_at: transition.created_at
|
270
|
+
}
|
194
271
|
end
|
195
272
|
end
|
196
273
|
|
197
|
-
# Reconnect the {Ably::Realtime::Connection::WebsocketTransport} following a disconnection
|
198
274
|
# @api private
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
275
|
+
def create_websocket_transport(&block)
|
276
|
+
operation = proc do
|
277
|
+
URI(client.endpoint).tap do |endpoint|
|
278
|
+
endpoint.query = URI.encode_www_form(client.auth.auth_params.merge(
|
279
|
+
timestamp: as_since_epoch(Time.now),
|
280
|
+
format: client.protocol,
|
281
|
+
echo: client.echo_messages
|
282
|
+
))
|
283
|
+
end.to_s
|
284
|
+
end
|
205
285
|
|
206
|
-
|
207
|
-
|
286
|
+
callback = proc do |url|
|
287
|
+
begin
|
288
|
+
@transport = EventMachine.connect(host, port, WebsocketTransport, self, url) do |websocket_transport|
|
289
|
+
yield websocket_transport if block_given?
|
290
|
+
end
|
291
|
+
rescue EventMachine::ConnectionError => error
|
292
|
+
manager.connection_failed error
|
293
|
+
end
|
294
|
+
end
|
208
295
|
|
209
|
-
|
210
|
-
|
296
|
+
# client.auth.auth_params is a blocking call, so defer this into a thread
|
297
|
+
EventMachine.defer operation, callback
|
211
298
|
end
|
212
299
|
|
213
|
-
|
214
|
-
|
300
|
+
# @api private
|
301
|
+
def release_websocket_transport
|
302
|
+
@transport = nil
|
215
303
|
end
|
216
304
|
|
305
|
+
# As we are using a state machine, do not allow change_state to be used
|
306
|
+
# #transition_state_machine must be used instead
|
307
|
+
private :change_state
|
308
|
+
|
309
|
+
private
|
310
|
+
attr_reader :serial, :state_machine
|
311
|
+
|
217
312
|
def create_pub_sub_message_bus
|
218
313
|
Ably::Util::PubSub.new(
|
219
|
-
coerce_into: Proc.new
|
314
|
+
coerce_into: Proc.new do |event|
|
315
|
+
raise KeyError, "Expected :protocol_message, :#{event} is disallowed" unless event == :protocol_message
|
316
|
+
:protocol_message
|
317
|
+
end
|
220
318
|
)
|
221
319
|
end
|
222
320
|
|
223
321
|
def add_message_serial_if_ack_required_to(protocol_message)
|
224
|
-
if Models::ProtocolMessage.ack_required?(protocol_message[:action])
|
322
|
+
if Ably::Models::ProtocolMessage.ack_required?(protocol_message[:action])
|
225
323
|
add_message_serial_to(protocol_message) { yield }
|
226
324
|
else
|
227
325
|
yield
|
@@ -236,6 +334,14 @@ module Ably
|
|
236
334
|
@serial -= 1
|
237
335
|
raise e
|
238
336
|
end
|
337
|
+
|
338
|
+
def log_state_machine_state_change
|
339
|
+
if state_machine.previous_state
|
340
|
+
logger.debug "ConnectionStateMachine: Transitioned from #{state_machine.previous_state} => #{state_machine.current_state}"
|
341
|
+
else
|
342
|
+
logger.debug "ConnectionStateMachine: Transitioned to #{state_machine.current_state}"
|
343
|
+
end
|
344
|
+
end
|
239
345
|
end
|
240
346
|
end
|
241
347
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module Ably::Realtime
|
2
|
+
class Connection
|
3
|
+
# ConnectionManager is responsible for all actions relating to underlying connection and transports,
|
4
|
+
# such as opening, closing, attempting reconnects etc.
|
5
|
+
#
|
6
|
+
# This is a private class and should never be used directly by developers as the API is likely to change in future.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class ConnectionManager
|
10
|
+
CONNECTION_FAILED = { retry_after: 0.5, max_retries: 2, code: 80000 }.freeze
|
11
|
+
|
12
|
+
def initialize(connection)
|
13
|
+
@connection = connection
|
14
|
+
|
15
|
+
@timers = Hash.new { |hash, key| hash[key] = [] }
|
16
|
+
@timers[:initializer] << EventMachine::Timer.new(0.01) { connection.connect }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates and sets up a new {Ably::Realtime::Connection::WebsocketTransport} available on attribute #transport
|
20
|
+
#
|
21
|
+
# @yield [Ably::Realtime::Connection::WebsocketTransport] block is called with new websocket transport
|
22
|
+
# @api private
|
23
|
+
def setup_transport(&block)
|
24
|
+
if transport && !transport.ready_for_release?
|
25
|
+
raise RuntimeError, 'Existing WebsocketTransport is connected, and must be closed first'
|
26
|
+
end
|
27
|
+
|
28
|
+
logger.debug "ConnectionManager: Opening connection to #{connection.host}:#{connection.port}"
|
29
|
+
|
30
|
+
connection.create_websocket_transport do |websocket_transport|
|
31
|
+
subscribe_to_transport_events websocket_transport
|
32
|
+
yield websocket_transport if block_given?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Called by the transport when a connection attempt fails
|
37
|
+
#
|
38
|
+
# @api private
|
39
|
+
def connection_failed(error)
|
40
|
+
logger.info "ConnectionManager: Connection to #{connection.host}:#{connection.port} failed; #{error.message}"
|
41
|
+
connection.transition_state_machine :disconnected, Ably::Models::ErrorInfo.new(message: "Connection failed; #{error.message}", code: 80000)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Ensures the underlying transport has been disconnected and all event emitter callbacks removed
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
def destroy_transport
|
48
|
+
if transport
|
49
|
+
unsubscribe_from_transport_events transport
|
50
|
+
transport.close_connection
|
51
|
+
connection.release_websocket_transport
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Reconnect the {Ably::Realtime::Connection::WebsocketTransport} if possible, otherwise set up a new transport
|
56
|
+
#
|
57
|
+
# @api private
|
58
|
+
def reconnect_transport
|
59
|
+
if !transport || transport.disconnected?
|
60
|
+
setup_transport
|
61
|
+
else
|
62
|
+
transport.reconnect connection.host, connection.port
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Send a Close {Ably::Models::ProtocolMessage} to the server and release the transport
|
67
|
+
#
|
68
|
+
# @api private
|
69
|
+
def close_connection
|
70
|
+
protocol_message = connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
|
71
|
+
|
72
|
+
unsubscribe_from_transport_events transport
|
73
|
+
|
74
|
+
protocol_message.callback do
|
75
|
+
destroy_transport
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Remove all timers set up as part of the initialize process.
|
80
|
+
# Typically called by StateMachine when connection is closed and can no longer process the timers
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
def cancel_initialized_timers
|
84
|
+
clear_timers :initializer
|
85
|
+
end
|
86
|
+
|
87
|
+
# Remove all timers related to connection attempt retries following a disconnect or suspended connection state.
|
88
|
+
# Typically called by StateMachine when connection is opened to ensure no further connection attempts are made
|
89
|
+
#
|
90
|
+
# @api private
|
91
|
+
def cancel_connection_retry_timers
|
92
|
+
clear_timers :connection_retry_timers
|
93
|
+
end
|
94
|
+
|
95
|
+
# When a connection is disconnected try and reconnect or set the connection state to :failed
|
96
|
+
#
|
97
|
+
# @api private
|
98
|
+
def respond_to_transport_disconnected(current_transition)
|
99
|
+
error_code = current_transition && current_transition.metadata && current_transition.metadata.code
|
100
|
+
|
101
|
+
if connection.previous_state == :connecting && error_code == CONNECTION_FAILED[:code]
|
102
|
+
return if retry_connection_failed
|
103
|
+
end
|
104
|
+
|
105
|
+
# Fallback if no other criteria met
|
106
|
+
connection.transition_state_machine :failed, current_transition.metadata
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
attr_reader :connection
|
111
|
+
|
112
|
+
# Timers used to manage connection state, for internal use by the client library
|
113
|
+
# @return [Hash]
|
114
|
+
attr_reader :timers
|
115
|
+
|
116
|
+
def transport
|
117
|
+
connection.transport
|
118
|
+
end
|
119
|
+
|
120
|
+
def client
|
121
|
+
connection.client
|
122
|
+
end
|
123
|
+
|
124
|
+
def clear_timers(key)
|
125
|
+
timers.fetch(key, []).each(&:cancel)
|
126
|
+
end
|
127
|
+
|
128
|
+
def retry_connection_failed
|
129
|
+
if retries_for_state(:disconnected, ignore_states: [:connecting]).count < CONNECTION_FAILED[:max_retries]
|
130
|
+
logger.debug "ConnectionManager: Pausing for #{CONNECTION_FAILED[:retry_after]}s before attempting to reconnect"
|
131
|
+
@timers[:connection_retry_timers] << EventMachine::Timer.new(CONNECTION_FAILED[:retry_after]) do
|
132
|
+
connection.connect
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def retries_for_state(state, ignore_states: [])
|
138
|
+
allowed_states = Array(state) + Array(ignore_states)
|
139
|
+
|
140
|
+
connection.state_history.reverse.take_while do |transition|
|
141
|
+
allowed_states.include?(transition[:state].to_sym)
|
142
|
+
end.select do |transition|
|
143
|
+
transition[:state] == state
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def subscribe_to_transport_events(transport)
|
148
|
+
transport.__incoming_protocol_msgbus__.on(:protocol_message) do |protocol_message|
|
149
|
+
connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
|
150
|
+
end
|
151
|
+
|
152
|
+
transport.on(:disconnected) do
|
153
|
+
connection.transition_state_machine :disconnected
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def unsubscribe_from_transport_events(transport)
|
158
|
+
transport.__incoming_protocol_msgbus__.unsubscribe
|
159
|
+
transport.off
|
160
|
+
logger.debug "ConnectionManager: Unsubscribed from all events from current transport"
|
161
|
+
end
|
162
|
+
|
163
|
+
def logger
|
164
|
+
connection.logger
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|