ably-rest 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. checksums.yaml +13 -5
  2. data/.gitmodules +1 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -3
  5. data/SPEC.md +495 -419
  6. data/ably-rest.gemspec +19 -5
  7. data/lib/ably-rest.rb +9 -1
  8. data/lib/submodules/ably-ruby/.gitignore +6 -0
  9. data/lib/submodules/ably-ruby/.rspec +1 -0
  10. data/lib/submodules/ably-ruby/.ruby-version.old +1 -0
  11. data/lib/submodules/ably-ruby/.travis.yml +10 -0
  12. data/lib/submodules/ably-ruby/Gemfile +4 -0
  13. data/lib/submodules/ably-ruby/LICENSE.txt +22 -0
  14. data/lib/submodules/ably-ruby/README.md +122 -0
  15. data/lib/submodules/ably-ruby/Rakefile +34 -0
  16. data/lib/submodules/ably-ruby/SPEC.md +1794 -0
  17. data/lib/submodules/ably-ruby/ably.gemspec +36 -0
  18. data/lib/submodules/ably-ruby/lib/ably.rb +12 -0
  19. data/lib/submodules/ably-ruby/lib/ably/auth.rb +438 -0
  20. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +69 -0
  21. data/lib/submodules/ably-ruby/lib/ably/logger.rb +102 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +37 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +223 -0
  24. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +132 -0
  25. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +108 -0
  26. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base64.rb +40 -0
  27. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/cipher.rb +83 -0
  28. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/json.rb +34 -0
  29. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/utf8.rb +26 -0
  30. data/lib/submodules/ably-ruby/lib/ably/models/nil_logger.rb +20 -0
  31. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +173 -0
  32. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +147 -0
  33. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +210 -0
  34. data/lib/submodules/ably-ruby/lib/ably/models/stat.rb +161 -0
  35. data/lib/submodules/ably-ruby/lib/ably/models/token.rb +74 -0
  36. data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +15 -0
  37. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +62 -0
  38. data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +69 -0
  39. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +100 -0
  40. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +69 -0
  41. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +202 -0
  42. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +128 -0
  43. data/lib/submodules/ably-ruby/lib/ably/modules/event_machine_helpers.rb +26 -0
  44. data/lib/submodules/ably-ruby/lib/ably/modules/http_helpers.rb +41 -0
  45. data/lib/submodules/ably-ruby/lib/ably/modules/message_pack.rb +14 -0
  46. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +41 -0
  47. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +153 -0
  48. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +57 -0
  49. data/lib/submodules/ably-ruby/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  50. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +74 -0
  51. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +64 -0
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +298 -0
  53. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +92 -0
  54. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +69 -0
  55. data/lib/submodules/ably-ruby/lib/ably/realtime/channels.rb +50 -0
  56. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +184 -0
  57. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +184 -0
  58. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +70 -0
  59. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +445 -0
  60. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +368 -0
  61. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +91 -0
  62. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +188 -0
  63. data/lib/submodules/ably-ruby/lib/ably/realtime/models/nil_channel.rb +30 -0
  64. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +564 -0
  65. data/lib/submodules/ably-ruby/lib/ably/rest.rb +43 -0
  66. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +104 -0
  67. data/lib/submodules/ably-ruby/lib/ably/rest/channels.rb +44 -0
  68. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +396 -0
  69. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/encoder.rb +49 -0
  70. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +41 -0
  71. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  72. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
  73. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +58 -0
  74. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_json.rb +27 -0
  75. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +27 -0
  76. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +92 -0
  77. data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +105 -0
  78. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +43 -0
  79. data/lib/submodules/ably-ruby/lib/ably/version.rb +3 -0
  80. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +154 -0
  81. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +558 -0
  82. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +119 -0
  83. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +575 -0
  84. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +785 -0
  85. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +457 -0
  86. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +55 -0
  87. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1001 -0
  88. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +23 -0
  89. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +27 -0
  90. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +564 -0
  91. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +165 -0
  92. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +134 -0
  93. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +41 -0
  94. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +273 -0
  95. data/lib/submodules/ably-ruby/spec/acceptance/rest/encoders_spec.rb +185 -0
  96. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +247 -0
  97. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +292 -0
  98. data/lib/submodules/ably-ruby/spec/acceptance/rest/stats_spec.rb +172 -0
  99. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +15 -0
  100. data/lib/submodules/ably-ruby/spec/resources/crypto-data-128.json +56 -0
  101. data/lib/submodules/ably-ruby/spec/resources/crypto-data-256.json +56 -0
  102. data/lib/submodules/ably-ruby/spec/rspec_config.rb +57 -0
  103. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +212 -0
  104. data/lib/submodules/ably-ruby/spec/shared/model_behaviour.rb +86 -0
  105. data/lib/submodules/ably-ruby/spec/shared/protocol_msgbus_behaviour.rb +36 -0
  106. data/lib/submodules/ably-ruby/spec/spec_helper.rb +20 -0
  107. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +60 -0
  108. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +104 -0
  109. data/lib/submodules/ably-ruby/spec/support/markdown_spec_formatter.rb +118 -0
  110. data/lib/submodules/ably-ruby/spec/support/private_api_formatter.rb +36 -0
  111. data/lib/submodules/ably-ruby/spec/support/protocol_helper.rb +32 -0
  112. data/lib/submodules/ably-ruby/spec/support/random_helper.rb +15 -0
  113. data/lib/submodules/ably-ruby/spec/support/rest_testapp_before_retry.rb +15 -0
  114. data/lib/submodules/ably-ruby/spec/support/test_app.rb +113 -0
  115. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +68 -0
  116. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +146 -0
  117. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +18 -0
  118. data/lib/submodules/ably-ruby/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +349 -0
  119. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  120. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  121. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/json_spec.rb +135 -0
  122. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/utf8_spec.rb +56 -0
  123. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +389 -0
  124. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +288 -0
  125. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +386 -0
  126. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +315 -0
  127. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +113 -0
  128. data/lib/submodules/ably-ruby/spec/unit/models/token_spec.rb +86 -0
  129. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +124 -0
  130. data/lib/submodules/ably-ruby/spec/unit/modules/conversions_spec.rb +72 -0
  131. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +272 -0
  132. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +184 -0
  133. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +283 -0
  134. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +206 -0
  135. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +81 -0
  136. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +30 -0
  137. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +33 -0
  138. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
  139. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +111 -0
  140. data/lib/submodules/ably-ruby/spec/unit/realtime/realtime_spec.rb +9 -0
  141. data/lib/submodules/ably-ruby/spec/unit/realtime/websocket_transport_spec.rb +25 -0
  142. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +109 -0
  143. data/lib/submodules/ably-ruby/spec/unit/rest/channels_spec.rb +79 -0
  144. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +53 -0
  145. data/lib/submodules/ably-ruby/spec/unit/rest/rest_spec.rb +10 -0
  146. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +87 -0
  147. data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +86 -0
  148. metadata +182 -27
@@ -0,0 +1,368 @@
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
+ # Connection state changes are performed by this class and executed from {ConnectionStateMachine}
6
+ #
7
+ # This is a private class and should never be used directly by developers as the API is likely to change in future.
8
+ #
9
+ # @api private
10
+ class ConnectionManager
11
+ # Configuration for automatic recovery of failed connection attempts
12
+ CONNECT_RETRY_CONFIG = {
13
+ disconnected: { retry_every: 15, max_time_in_state: 120 },
14
+ suspended: { retry_every: 120, max_time_in_state: Float::INFINITY }
15
+ }.freeze
16
+
17
+ # Time to wait following a connection state request before it's considered a failure
18
+ TIMEOUTS = {
19
+ open: 15,
20
+ close: 10
21
+ }
22
+
23
+ # Error codes from the server that can potentially be resolved
24
+ RESOLVABLE_ERROR_CODES = {
25
+ token_expired: 40140
26
+ }
27
+
28
+ def initialize(connection)
29
+ @connection = connection
30
+ @timers = Hash.new { |hash, key| hash[key] = [] }
31
+
32
+ connection.on(:closed) do
33
+ connection.reset_resume_info
34
+ end
35
+
36
+ connection.once(:connecting) do
37
+ close_connection_when_reactor_is_stopped
38
+ end
39
+
40
+ EventMachine.next_tick do
41
+ # Connect once Connection object is initialised
42
+ connection.connect if client.connect_automatically
43
+ end
44
+ end
45
+
46
+ # Creates and sets up a new {Ably::Realtime::Connection::WebsocketTransport} available on attribute #transport
47
+ #
48
+ # @yield [Ably::Realtime::Connection::WebsocketTransport] block is called with new websocket transport
49
+ # @api private
50
+ def setup_transport
51
+ if transport && !transport.ready_for_release?
52
+ raise RuntimeError, 'Existing WebsocketTransport is connected, and must be closed first'
53
+ end
54
+
55
+ unless client.auth.authentication_security_requirements_met?
56
+ connection.transition_state_machine :failed, Ably::Exceptions::InsecureRequestError.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
57
+ return
58
+ end
59
+
60
+ logger.debug 'ConnectionManager: Opening a websocket transport connection'
61
+
62
+ connection.create_websocket_transport do |websocket_transport|
63
+ subscribe_to_transport_events websocket_transport
64
+ yield websocket_transport if block_given?
65
+ end
66
+
67
+ logger.debug "ConnectionManager: Setting up automatic connection timeout timer for #{TIMEOUTS.fetch(:open)}s"
68
+ create_timeout_timer_whilst_in_state(:connect, TIMEOUTS.fetch(:open)) do
69
+ connection_opening_failed Ably::Exceptions::ConnectionTimeoutError.new("Connection to Ably timed out after #{TIMEOUTS.fetch(:open)}s", nil, 80014)
70
+ end
71
+ end
72
+
73
+ # Called by the transport when a connection attempt fails
74
+ #
75
+ # @api private
76
+ def connection_opening_failed(error)
77
+ logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
78
+ connection.transition_state_machine next_retry_state, Ably::Exceptions::ConnectionError.new("Connection failed; #{error.message}", nil, 80000)
79
+ end
80
+
81
+ # Called whenever a new connection message is received with an error
82
+ #
83
+ # @api private
84
+ def connected_with_error(error)
85
+ logger.warn "ConnectionManager: Connected with error; #{error.message}"
86
+ connection.trigger :error, error
87
+ end
88
+
89
+ # Ensures the underlying transport has been disconnected and all event emitter callbacks removed
90
+ #
91
+ # @api private
92
+ def destroy_transport
93
+ if transport
94
+ unsubscribe_from_transport_events transport
95
+ transport.close_connection
96
+ connection.release_websocket_transport
97
+ end
98
+ end
99
+
100
+ # Reconnect the {Ably::Realtime::Connection::WebsocketTransport} if possible, otherwise set up a new transport
101
+ #
102
+ # @api private
103
+ def reconnect_transport
104
+ if !transport || transport.disconnected?
105
+ setup_transport
106
+ else
107
+ transport.reconnect connection.current_host, connection.port
108
+ end
109
+ end
110
+
111
+ # Send a Close {Ably::Models::ProtocolMessage} to the server and release the transport
112
+ #
113
+ # @api private
114
+ def close_connection
115
+ connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
116
+
117
+ create_timeout_timer_whilst_in_state(:close, TIMEOUTS.fetch(:close)) do
118
+ force_close_connection if connection.closing?
119
+ end
120
+ end
121
+
122
+ # Close the underlying transport immediately and set the connection state to closed
123
+ #
124
+ # @api private
125
+ def force_close_connection
126
+ destroy_transport
127
+ connection.transition_state_machine :closed
128
+ end
129
+
130
+ # Connection has failed
131
+ #
132
+ # @api private
133
+ def fail(error)
134
+ connection.logger.fatal "ConnectionManager: Connection failed - #{error}"
135
+ connection.manager.destroy_transport
136
+ connection.once(:failed) { connection.trigger :error, error }
137
+ end
138
+
139
+ # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
140
+ #
141
+ # @api private
142
+ def respond_to_transport_disconnected_when_connecting(current_transition)
143
+ return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
144
+ return unless retry_connection? # do not always reattempt connection or change state as client may be re-authorising
145
+
146
+ unless connection_retry_from_suspended_state?
147
+ return if connection_retry_for(:disconnected, ignore_states: [:connecting])
148
+ end
149
+
150
+ return if connection_retry_for(:suspended, ignore_states: [:connecting])
151
+
152
+ # Fallback if no other criteria met
153
+ connection.transition_state_machine :failed, current_transition.metadata
154
+ end
155
+
156
+ # When a connection is disconnected after connecting, attempt reconnect and/or set state to :suspended or :failed
157
+ #
158
+ # @api private
159
+ def respond_to_transport_disconnected_whilst_connected(current_transition)
160
+ logger.warn "ConnectionManager: Connection to #{connection.transport.url} was disconnected unexpectedly"
161
+
162
+ if current_transition.metadata.kind_of?(Ably::Models::ErrorInfo)
163
+ connection.trigger :error, current_transition.metadata
164
+ logger.error "ConnectionManager: Error received when disconnected within ProtocolMessage - #{current_transition.metadata}"
165
+ end
166
+
167
+ destroy_transport
168
+ respond_to_transport_disconnected_when_connecting current_transition
169
+ end
170
+
171
+ # {Ably::Models::ProtocolMessage ProtocolMessage Error} received from server.
172
+ # Some error states can be resolved by the client library.
173
+ #
174
+ # @api private
175
+ def error_received_from_server(error)
176
+ case error.code
177
+ when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
178
+ connection.transition_state_machine :disconnected
179
+ connection.once_or_if(:disconnected) do
180
+ renew_token_and_reconnect error
181
+ end
182
+ else
183
+ logger.error "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state"
184
+ connection.transition_state_machine :failed, error
185
+ end
186
+ end
187
+
188
+ # Number of consecutive attempts for provided state
189
+ # @return [Integer]
190
+ # @api private
191
+ def retry_count_for_state(state)
192
+ retries_for_state(state, ignore_states: [:connecting]).count
193
+ end
194
+
195
+ private
196
+ attr_reader :connection
197
+
198
+ # Timers used to manage connection state, for internal use by the client library
199
+ # @return [Hash]
200
+ attr_reader :timers
201
+
202
+ def transport
203
+ connection.transport
204
+ end
205
+
206
+ def client
207
+ connection.client
208
+ end
209
+
210
+ # Create a timer that will execute in timeout_in seconds.
211
+ # If the connection state changes however, cancel the timer
212
+ def create_timeout_timer_whilst_in_state(timer_id, timeout_in)
213
+ raise ArgumentError, 'Block required' unless block_given?
214
+
215
+ timers[timer_id] << EventMachine::Timer.new(timeout_in) do
216
+ yield
217
+ end
218
+ connection.once_state_changed { clear_timers timer_id }
219
+ end
220
+
221
+ def clear_timers(key)
222
+ timers.fetch(key, []).each(&:cancel)
223
+ end
224
+
225
+ def next_retry_state
226
+ if connection_retry_from_suspended_state? || time_passed_since_disconnected > CONNECT_RETRY_CONFIG.fetch(:disconnected).fetch(:max_time_in_state)
227
+ :suspended
228
+ else
229
+ :disconnected
230
+ end
231
+ end
232
+
233
+ def connection_retry_from_suspended_state?
234
+ !retries_for_state(:suspended, ignore_states: [:connecting]).empty?
235
+ end
236
+
237
+ def time_passed_since_disconnected
238
+ time_spent_attempting_state(:disconnected, ignore_states: [:connecting])
239
+ end
240
+
241
+ # Reattempt a connection with a delay based on the CONNECT_RETRY_CONFIG for `from_state`
242
+ #
243
+ # @return [Boolean] True if a connection attempt has been set up, false if no further connection attempts can be made for this state
244
+ #
245
+ def connection_retry_for(from_state, options = {})
246
+ retry_params = CONNECT_RETRY_CONFIG.fetch(from_state)
247
+
248
+ if time_spent_attempting_state(from_state, options) <= retry_params.fetch(:max_time_in_state)
249
+ logger.debug "ConnectionManager: Pausing for #{retry_params.fetch(:retry_every)}s before attempting to reconnect"
250
+ create_timeout_timer_whilst_in_state(:reconnect, retry_params.fetch(:retry_every)) do
251
+ connection.connect if connection.state == from_state
252
+ end
253
+ true
254
+ end
255
+ end
256
+
257
+ # Returns a float representing the amount of time passed since the first consecutive attempt of this state
258
+ #
259
+ # @param (see #retries_for_state)
260
+ # @return [Float] time passed in seconds
261
+ #
262
+ def time_spent_attempting_state(state, options)
263
+ states = retries_for_state(state, options)
264
+ if states.empty?
265
+ 0
266
+ else
267
+ Time.now.to_f - states.last[:transitioned_at].to_f
268
+ end.to_f
269
+ end
270
+
271
+ # Checks the state change history for the current connection and returns all matching consecutive states.
272
+ # This is useful to determine the number of retries of a particular state on a connection.
273
+ #
274
+ # @param state [Symbol]
275
+ # @param options [Hash]
276
+ # @option options [Array<Symbol>] :ignore_states states that should be ignored when determining consecutive historical retries for `state`.
277
+ # For example, when working out :connecting attempts, :disconnect state changes should be ignored as they are a side effect of a failed :connecting
278
+ #
279
+ # @return [Array<Hash>] Array of consecutive state attempts matching `state` in order of transitioned_at desc
280
+ #
281
+ def retries_for_state(state, options)
282
+ ignore_states = options.fetch(:ignore_states, [])
283
+ allowed_states = Array(state) + Array(ignore_states)
284
+
285
+ connection.state_history.reverse.take_while do |transition|
286
+ allowed_states.include?(transition[:state].to_sym)
287
+ end.select do |transition|
288
+ transition[:state] == state
289
+ end
290
+ end
291
+
292
+ def subscribe_to_transport_events(transport)
293
+ transport.__incoming_protocol_msgbus__.on(:protocol_message) do |protocol_message|
294
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
295
+ end
296
+
297
+ transport.on(:disconnected) do
298
+ if connection.closing?
299
+ connection.transition_state_machine :closed
300
+ elsif !connection.closed? && !connection.disconnected?
301
+ connection.transition_state_machine :disconnected
302
+ end
303
+ end
304
+ end
305
+
306
+ def renew_token_and_reconnect(error)
307
+ if client.auth.token_renewable?
308
+ if @renewing_token
309
+ connection.transition_state_machine :failed, error
310
+ return
311
+ end
312
+
313
+ @renewing_token = true
314
+ logger.warn "ConnectionManager: Token has expired and is renewable, renewing token now"
315
+
316
+ operation = proc do
317
+ begin
318
+ client.auth.authorise
319
+ rescue StandardError => auth_error
320
+ connection.transition_state_machine :failed, auth_error
321
+ nil
322
+ end
323
+ end
324
+
325
+ callback = proc do |token|
326
+ state_changed_callback = proc do
327
+ @renewing_token = false
328
+ connection.off &state_changed_callback
329
+ end
330
+
331
+ connection.once :connected, :closed, :failed, &state_changed_callback
332
+
333
+ if token && !token.expired?
334
+ reconnect_transport
335
+ else
336
+ connection.transition_state_machine :failed, error unless connection.failed?
337
+ end
338
+ end
339
+
340
+ EventMachine.defer operation, callback
341
+ else
342
+ logger.warn "ConnectionManager: Token has expired and is not renewable"
343
+ connection.transition_state_machine :failed, error
344
+ end
345
+ end
346
+
347
+ def unsubscribe_from_transport_events(transport)
348
+ transport.__incoming_protocol_msgbus__.unsubscribe
349
+ transport.off
350
+ logger.debug "ConnectionManager: Unsubscribed from all events from current transport"
351
+ end
352
+
353
+ def close_connection_when_reactor_is_stopped
354
+ EventMachine.add_shutdown_hook do
355
+ connection.close unless connection.closed? || connection.failed?
356
+ end
357
+ end
358
+
359
+ def retry_connection?
360
+ !@renewing_token
361
+ end
362
+
363
+ def logger
364
+ connection.logger
365
+ end
366
+ end
367
+ end
368
+ end
@@ -0,0 +1,91 @@
1
+ require 'ably/modules/state_machine'
2
+
3
+ module Ably::Realtime
4
+ class Connection
5
+ # Internal class to manage connection state, recovery and state transitions for {Ably::Realtime::Connection}
6
+ class ConnectionStateMachine
7
+ include Ably::Modules::StateMachine
8
+
9
+ # States supported by this StateMachine match #{Connection::STATE}s
10
+ # :initialized
11
+ # :connecting
12
+ # :connected
13
+ # :disconnected
14
+ # :suspended
15
+ # :closing
16
+ # :closed
17
+ # :failed
18
+ Connection::STATE.each_with_index do |state_enum, index|
19
+ state state_enum.to_sym, initial: index == 0
20
+ end
21
+
22
+ transition :from => :initialized, :to => [:connecting, :closing]
23
+ transition :from => :connecting, :to => [:connected, :failed, :closing, :disconnected, :suspended]
24
+ transition :from => :connected, :to => [:disconnected, :suspended, :closing, :failed]
25
+ transition :from => :disconnected, :to => [:connecting, :closing, :suspended, :failed]
26
+ transition :from => :suspended, :to => [:connecting, :closing, :failed]
27
+ transition :from => :closing, :to => [:closed]
28
+ transition :from => :closed, :to => [:connecting]
29
+ transition :from => :failed, :to => [:connecting]
30
+
31
+ after_transition do |connection, transition|
32
+ connection.synchronize_state_with_statemachine
33
+ end
34
+
35
+ after_transition(to: [:connecting], from: [:initialized, :closed, :failed]) do |connection|
36
+ connection.manager.setup_transport
37
+ end
38
+
39
+ after_transition(to: [:connecting], from: [:disconnected, :suspended]) do |connection|
40
+ connection.manager.reconnect_transport
41
+ end
42
+
43
+ after_transition(to: [:connected]) do |connection, current_transition|
44
+ connection.manager.connected_with_error current_transition.metadata if current_transition.metadata
45
+ end
46
+
47
+ after_transition(to: [:disconnected, :suspended], from: [:connecting]) do |connection, current_transition|
48
+ connection.manager.respond_to_transport_disconnected_when_connecting current_transition
49
+ end
50
+
51
+ after_transition(to: [:disconnected], from: [:connected]) do |connection, current_transition|
52
+ connection.manager.respond_to_transport_disconnected_whilst_connected current_transition
53
+ end
54
+
55
+ after_transition(to: [:disconnected, :suspended]) do |connection|
56
+ connection.manager.destroy_transport # never reuse a transport if the connection has failed
57
+ end
58
+
59
+ before_transition(to: [:failed]) do |connection, current_transition|
60
+ connection.manager.fail current_transition.metadata
61
+ end
62
+
63
+ after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
64
+ connection.manager.force_close_connection
65
+ end
66
+
67
+ after_transition(to: [:closing], from: [:connecting, :connected]) do |connection|
68
+ connection.manager.close_connection
69
+ end
70
+
71
+ before_transition(to: [:closed], from: [:closing]) do |connection|
72
+ connection.manager.destroy_transport
73
+ end
74
+
75
+ # Transitions responsible for updating connection#error_reason
76
+ before_transition(to: [:connected, :closed, :disconnected, :suspended, :failed]) do |connection, current_transition|
77
+ reason = current_transition.metadata if is_error_type?(current_transition.metadata)
78
+ connection.set_failed_connection_error_reason reason
79
+ end
80
+
81
+ private
82
+ def connection
83
+ object
84
+ end
85
+
86
+ def logger
87
+ connection.logger
88
+ end
89
+ end
90
+ end
91
+ end