ably 1.0.6 → 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +14 -0
- data/.travis.yml +10 -8
- data/CHANGELOG.md +75 -3
- data/LICENSE +1 -3
- data/README.md +12 -7
- data/Rakefile +32 -0
- data/SPEC.md +1277 -835
- data/ably.gemspec +14 -9
- data/lib/ably/auth.rb +30 -4
- data/lib/ably/exceptions.rb +10 -4
- data/lib/ably/logger.rb +7 -1
- data/lib/ably/models/channel_state_change.rb +1 -1
- data/lib/ably/models/connection_state_change.rb +1 -1
- data/lib/ably/models/device_details.rb +87 -0
- data/lib/ably/models/device_push_details.rb +86 -0
- data/lib/ably/models/error_info.rb +23 -2
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
- data/lib/ably/models/protocol_message.rb +32 -2
- data/lib/ably/models/push_channel_subscription.rb +89 -0
- data/lib/ably/modules/conversions.rb +1 -1
- data/lib/ably/modules/encodeable.rb +1 -1
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/model_common.rb +15 -2
- data/lib/ably/modules/state_machine.rb +2 -2
- data/lib/ably/realtime.rb +1 -0
- data/lib/ably/realtime/auth.rb +1 -1
- data/lib/ably/realtime/channel.rb +24 -102
- data/lib/ably/realtime/channel/channel_manager.rb +2 -6
- data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
- data/lib/ably/realtime/channel/publisher.rb +74 -0
- data/lib/ably/realtime/channel/push_channel.rb +62 -0
- data/lib/ably/realtime/client.rb +91 -3
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +34 -20
- data/lib/ably/realtime/connection/connection_manager.rb +25 -9
- data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
- data/lib/ably/realtime/presence.rb +4 -4
- data/lib/ably/realtime/presence/members_map.rb +3 -3
- data/lib/ably/realtime/push.rb +40 -0
- data/lib/ably/realtime/push/admin.rb +61 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/rest.rb +1 -0
- data/lib/ably/rest/channel.rb +53 -17
- data/lib/ably/rest/channel/push_channel.rb +62 -0
- data/lib/ably/rest/client.rb +154 -32
- data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
- data/lib/ably/rest/presence.rb +1 -0
- data/lib/ably/rest/push.rb +42 -0
- data/lib/ably/rest/push/admin.rb +54 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +103 -0
- data/lib/ably/version.rb +7 -2
- data/spec/acceptance/realtime/auth_spec.rb +245 -17
- data/spec/acceptance/realtime/channel_history_spec.rb +26 -20
- data/spec/acceptance/realtime/channel_spec.rb +177 -59
- data/spec/acceptance/realtime/client_spec.rb +153 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +72 -6
- data/spec/acceptance/realtime/connection_spec.rb +129 -18
- data/spec/acceptance/realtime/message_spec.rb +36 -34
- data/spec/acceptance/realtime/presence_spec.rb +201 -167
- data/spec/acceptance/realtime/push_admin_spec.rb +736 -0
- data/spec/acceptance/realtime/push_spec.rb +27 -0
- data/spec/acceptance/rest/auth_spec.rb +41 -3
- data/spec/acceptance/rest/base_spec.rb +2 -2
- data/spec/acceptance/rest/channel_spec.rb +79 -4
- data/spec/acceptance/rest/channels_spec.rb +6 -0
- data/spec/acceptance/rest/client_spec.rb +129 -10
- data/spec/acceptance/rest/message_spec.rb +158 -6
- data/spec/acceptance/rest/push_admin_spec.rb +952 -0
- data/spec/acceptance/rest/push_spec.rb +25 -0
- data/spec/acceptance/rest/time_spec.rb +1 -1
- data/spec/run_parallel_tests +33 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/debug_failure_helper.rb +9 -5
- data/spec/support/test_app.rb +2 -2
- data/spec/unit/logger_spec.rb +10 -3
- data/spec/unit/models/device_details_spec.rb +102 -0
- data/spec/unit/models/device_push_details_spec.rb +101 -0
- data/spec/unit/models/error_info_spec.rb +51 -3
- data/spec/unit/models/message_spec.rb +17 -2
- data/spec/unit/models/presence_message_spec.rb +1 -1
- data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
- data/spec/unit/modules/enum_spec.rb +1 -1
- data/spec/unit/realtime/client_spec.rb +13 -1
- data/spec/unit/realtime/connection_spec.rb +1 -1
- data/spec/unit/realtime/push_channel_spec.rb +36 -0
- data/spec/unit/rest/channel_spec.rb +8 -1
- data/spec/unit/rest/client_spec.rb +30 -0
- data/spec/unit/rest/push_channel_spec.rb +36 -0
- metadata +95 -26
data/lib/ably/rest.rb
CHANGED
data/lib/ably/rest/channel.rb
CHANGED
@@ -3,8 +3,6 @@ module Ably
|
|
3
3
|
# The Ably Realtime service organises the traffic within any application into named channels.
|
4
4
|
# Channels are the "unit" of message distribution; clients attach to channels to subscribe to messages, and every message broadcast by the service is associated with a unique channel.
|
5
5
|
#
|
6
|
-
# @!attribute [r] client
|
7
|
-
# @return {Ably::Realtime::Client} Ably client associated with this channel
|
8
6
|
# @!attribute [r] name
|
9
7
|
# @return {String} channel name
|
10
8
|
# @!attribute [r] options
|
@@ -12,7 +10,19 @@ module Ably
|
|
12
10
|
class Channel
|
13
11
|
include Ably::Modules::Conversions
|
14
12
|
|
15
|
-
|
13
|
+
# Ably client associated with this channel
|
14
|
+
# @return [Ably::Realtime::Client]
|
15
|
+
# @api private
|
16
|
+
attr_reader :client
|
17
|
+
|
18
|
+
attr_reader :name, :options
|
19
|
+
|
20
|
+
# Push channel used for push notification (client-side)
|
21
|
+
# @return [Ably::Rest::Channel::PushChannel]
|
22
|
+
# @api private
|
23
|
+
attr_reader :push
|
24
|
+
|
25
|
+
IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH = 9 # See spec RSL1k1
|
16
26
|
|
17
27
|
# Initialize a new Channel object
|
18
28
|
#
|
@@ -27,17 +37,17 @@ module Ably
|
|
27
37
|
update_options channel_options
|
28
38
|
@client = client
|
29
39
|
@name = name
|
40
|
+
@push = PushChannel.new(self)
|
30
41
|
end
|
31
42
|
|
32
|
-
# Publish one or more messages to the channel.
|
33
|
-
#
|
34
|
-
# @param
|
35
|
-
# @param
|
36
|
-
# @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
|
43
|
+
# Publish one or more messages to the channel. Three overloaded forms
|
44
|
+
# @param name [String, Array<Ably::Models::Message|Hash>, Ably::Models::Message, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs, or a single Ably::Model::Message object
|
45
|
+
# @param data [String, ByteArray, Hash, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument, in which case an optional hash of query parameters
|
46
|
+
# @param attributes [Hash, nil] Optional additional message attributes such as :extras, :id, :client_id or :connection_id, applied when name attribute is nil or a string (Deprecated, will be removed in 2.0 in favour of constructing a Message object)
|
37
47
|
# @return [Boolean] true if the message was published, otherwise false
|
38
48
|
#
|
39
49
|
# @example
|
40
|
-
# # Publish a single message
|
50
|
+
# # Publish a single message with (name, data) form
|
41
51
|
# channel.publish 'click', { x: 1, y: 2 }
|
42
52
|
#
|
43
53
|
# # Publish an array of message Hashes
|
@@ -54,16 +64,28 @@ module Ably
|
|
54
64
|
# ]
|
55
65
|
# channel.publish messages
|
56
66
|
#
|
57
|
-
|
58
|
-
|
59
|
-
|
67
|
+
# # Publish a single Ably::Models::Message object, with a query params
|
68
|
+
# # specifying quickAck: true
|
69
|
+
# message = Ably::Models::Message(name: 'click', { x: 1, y: 2 })
|
70
|
+
# channel.publish message, quickAck: 'true'
|
71
|
+
#
|
72
|
+
def publish(first, second = nil, third = {})
|
73
|
+
messages, qs_params = if first.kind_of?(Enumerable)
|
74
|
+
# ([Message], qs_params) form
|
75
|
+
[first, second]
|
76
|
+
elsif first.kind_of?(Ably::Models::Message)
|
77
|
+
# (Message, qs_params) form
|
78
|
+
[[first], second]
|
60
79
|
else
|
61
|
-
|
62
|
-
|
63
|
-
|
80
|
+
# (name, data, attributes) form
|
81
|
+
first = ensure_utf_8(:name, first, allow_nil: true)
|
82
|
+
ensure_supported_payload second
|
83
|
+
# RSL1h - attributes as an extra method parameter is extra-spec but need to
|
84
|
+
# keep it for backcompat until version 2
|
85
|
+
[[{ name: first, data: second }.merge(third)], nil]
|
64
86
|
end
|
65
87
|
|
66
|
-
payload = messages.map do |message|
|
88
|
+
payload = messages.each_with_index.map do |message, index|
|
67
89
|
Ably::Models::Message(message.dup).tap do |msg|
|
68
90
|
msg.encode client.encoders, options
|
69
91
|
|
@@ -75,9 +97,21 @@ module Ably
|
|
75
97
|
raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{msg.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'")
|
76
98
|
end
|
77
99
|
end.as_json
|
100
|
+
end.tap do |payload|
|
101
|
+
if client.idempotent_rest_publishing
|
102
|
+
# We cannot mutate for idempotent publishing if one or more messages already has an ID
|
103
|
+
if payload.all? { |msg| !msg['id'] }
|
104
|
+
# Mutate the JSON to support idempotent publishing where a Message.id does not exist
|
105
|
+
idempotent_publish_id = SecureRandom.base64(IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH)
|
106
|
+
payload.each_with_index do |msg, idx|
|
107
|
+
msg['id'] = "#{idempotent_publish_id}:#{idx}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
78
111
|
end
|
79
112
|
|
80
|
-
|
113
|
+
options = qs_params ? { qs_params: qs_params } : {}
|
114
|
+
response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload, options)
|
81
115
|
|
82
116
|
[201, 204].include?(response.status)
|
83
117
|
end
|
@@ -141,3 +175,5 @@ module Ably
|
|
141
175
|
end
|
142
176
|
end
|
143
177
|
end
|
178
|
+
|
179
|
+
require 'ably/rest/channel/push_channel'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Ably::Rest
|
2
|
+
class Channel
|
3
|
+
# A push channel used for push notifications
|
4
|
+
# Each PushChannel maps to exactly one Rest Channel
|
5
|
+
#
|
6
|
+
# @!attribute [r] channel
|
7
|
+
# @return [Ably::Rest::Channel] Underlying channel object
|
8
|
+
#
|
9
|
+
class PushChannel
|
10
|
+
attr_reader :channel
|
11
|
+
|
12
|
+
def initialize(channel)
|
13
|
+
raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::Rest::Channel)
|
14
|
+
@channel = channel
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"<PushChannel: name=#{channel.name}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Subscribe local device for push notifications on this channel
|
22
|
+
#
|
23
|
+
# @note This is unsupported in the Ruby library
|
24
|
+
def subscribe_device(*args)
|
25
|
+
raise_unsupported
|
26
|
+
end
|
27
|
+
|
28
|
+
# Subscribe all devices registered to this client's authenticated client_id for push notifications on this channel
|
29
|
+
#
|
30
|
+
# @note This is unsupported in the Ruby library
|
31
|
+
def subscribe_client_id(*args)
|
32
|
+
raise_unsupported
|
33
|
+
end
|
34
|
+
|
35
|
+
# Unsubscribe local device for push notifications on this channel
|
36
|
+
#
|
37
|
+
# @note This is unsupported in the Ruby library
|
38
|
+
def unsubscribe_device(*args)
|
39
|
+
raise_unsupported
|
40
|
+
end
|
41
|
+
|
42
|
+
# Unsubscribe all devices registered to this client's authenticated client_id for push notifications on this channel
|
43
|
+
#
|
44
|
+
# @note This is unsupported in the Ruby library
|
45
|
+
def unsubscribe_client_id(*args)
|
46
|
+
raise_unsupported
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get list of subscriptions on this channel for this device or authenticate client_id
|
50
|
+
#
|
51
|
+
# @note This is unsupported in the Ruby library
|
52
|
+
def get_subscriptions(*args)
|
53
|
+
raise_unsupported
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def raise_unsupported
|
58
|
+
raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/ably/rest/client.rb
CHANGED
@@ -30,6 +30,15 @@ module Ably
|
|
30
30
|
max_retry_count: 3
|
31
31
|
}.freeze
|
32
32
|
|
33
|
+
FALLBACK_RETRY_TIMEOUT = 10 * 60
|
34
|
+
|
35
|
+
# Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError
|
36
|
+
FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError)
|
37
|
+
[Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError]
|
38
|
+
else
|
39
|
+
Faraday::ClientError
|
40
|
+
end
|
41
|
+
|
33
42
|
def_delegators :auth, :client_id, :auth_options
|
34
43
|
|
35
44
|
# Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
|
@@ -83,10 +92,23 @@ module Ably
|
|
83
92
|
# if empty or nil then fallback host functionality is disabled
|
84
93
|
attr_reader :fallback_hosts
|
85
94
|
|
86
|
-
#
|
95
|
+
# Whether the {Client} has to add a random identifier to the path of a request
|
87
96
|
# @return [Boolean]
|
88
97
|
attr_reader :add_request_ids
|
89
98
|
|
99
|
+
# Retries are logged by default to warn and error. When true, retries are logged at info level
|
100
|
+
# @return [Boolean]
|
101
|
+
# @api private
|
102
|
+
attr_reader :log_retries_as_info
|
103
|
+
|
104
|
+
# True when idempotent publishing is enabled for all messages published via REST.
|
105
|
+
# When this feature is enabled, the client library will add a unique ID to every published message (without an ID)
|
106
|
+
# ensuring any failed published attempts (due to failures such as HTTP requests failing mid-flight) that are
|
107
|
+
# automatically retried will not result in duplicate messages being published to the Ably platform.
|
108
|
+
# Note: This is a beta unsupported feature!
|
109
|
+
# @return [Boolean]
|
110
|
+
attr_reader :idempotent_rest_publishing
|
111
|
+
|
90
112
|
# Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
|
91
113
|
#
|
92
114
|
# @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key or Token ID
|
@@ -117,6 +139,10 @@ module Ably
|
|
117
139
|
#
|
118
140
|
# @option options [Boolean] :fallback_hosts_use_default (false) When true, forces the user of fallback hosts even if a non-default production endpoint is being used
|
119
141
|
# @option options [Array<String>] :fallback_hosts When an array of fallback hosts are provided, these fallback hosts are always used if a request fails to the primary endpoint. If an empty array is provided, the fallback host functionality is disabled
|
142
|
+
# @option options [Integer] :fallback_retry_timeout (600 seconds) amount of time in seconds a REST client will continue to use a working fallback host when the primary fallback host has previously failed
|
143
|
+
#
|
144
|
+
# @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request.
|
145
|
+
# @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST
|
120
146
|
#
|
121
147
|
# @return [Ably::Rest::Client]
|
122
148
|
#
|
@@ -139,18 +165,21 @@ module Ably
|
|
139
165
|
end
|
140
166
|
end
|
141
167
|
|
142
|
-
@realtime_client
|
143
|
-
@tls
|
144
|
-
@environment
|
145
|
-
@environment
|
146
|
-
@protocol
|
147
|
-
@debug_http
|
148
|
-
@log_level
|
149
|
-
@custom_logger
|
150
|
-
@custom_host
|
151
|
-
@custom_port
|
152
|
-
@custom_tls_port
|
153
|
-
@add_request_ids
|
168
|
+
@realtime_client = options.delete(:realtime_client)
|
169
|
+
@tls = options.delete(:tls) == false ? false : true
|
170
|
+
@environment = options.delete(:environment) # nil is production
|
171
|
+
@environment = nil if [:production, 'production'].include?(@environment)
|
172
|
+
@protocol = options.delete(:protocol) || :msgpack
|
173
|
+
@debug_http = options.delete(:debug_http)
|
174
|
+
@log_level = options.delete(:log_level) || ::Logger::WARN
|
175
|
+
@custom_logger = options.delete(:logger)
|
176
|
+
@custom_host = options.delete(:rest_host)
|
177
|
+
@custom_port = options.delete(:port)
|
178
|
+
@custom_tls_port = options.delete(:tls_port)
|
179
|
+
@add_request_ids = options.delete(:add_request_ids)
|
180
|
+
@log_retries_as_info = options.delete(:log_retries_as_info)
|
181
|
+
@idempotent_rest_publishing = options.delete(:idempotent_rest_publishing) || Ably.major_minor_version_numeric > 1.1
|
182
|
+
|
154
183
|
|
155
184
|
if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
|
156
185
|
raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
|
@@ -166,6 +195,10 @@ module Ably
|
|
166
195
|
Ably::FALLBACK_HOSTS
|
167
196
|
end
|
168
197
|
|
198
|
+
options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT
|
199
|
+
|
200
|
+
# Take option keys prefixed with `http_`, remove the http_ and
|
201
|
+
# check if the option exists in HTTP_DEFAULTS. If so, update http_defaults
|
169
202
|
@http_defaults = HTTP_DEFAULTS.dup
|
170
203
|
options.each do |key, val|
|
171
204
|
if http_key = key[/^http_(.+)/, 1]
|
@@ -190,8 +223,12 @@ module Ably
|
|
190
223
|
raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
|
191
224
|
|
192
225
|
token_params = options.delete(:default_token_params) || {}
|
193
|
-
@options
|
194
|
-
|
226
|
+
@options = options
|
227
|
+
init_auth_options = options.select do |key, _|
|
228
|
+
Auth::AUTH_OPTIONS_KEYS.include?(key.to_s)
|
229
|
+
end
|
230
|
+
|
231
|
+
@auth = Auth.new(self, token_params, init_auth_options)
|
195
232
|
@channels = Ably::Rest::Channels.new(self)
|
196
233
|
@encoders = []
|
197
234
|
|
@@ -273,6 +310,24 @@ module Ably
|
|
273
310
|
raw_request(:post, path, params, options)
|
274
311
|
end
|
275
312
|
|
313
|
+
# Perform an HTTP PUT request to the API using configured authentication
|
314
|
+
#
|
315
|
+
# @return [Faraday::Response]
|
316
|
+
#
|
317
|
+
# @api private
|
318
|
+
def put(path, params, options = {})
|
319
|
+
raw_request(:put, path, params, options)
|
320
|
+
end
|
321
|
+
|
322
|
+
# Perform an HTTP DELETE request to the API using configured authentication
|
323
|
+
#
|
324
|
+
# @return [Faraday::Response]
|
325
|
+
#
|
326
|
+
# @api private
|
327
|
+
def delete(path, params, options = {})
|
328
|
+
raw_request(:delete, path, params, options)
|
329
|
+
end
|
330
|
+
|
276
331
|
# Perform an HTTP request to the Ably API
|
277
332
|
# This is a convenience for customers who wish to use bleeding edge REST API functionality
|
278
333
|
# that is either not documented or is not included in the API for our client libraries.
|
@@ -292,14 +347,14 @@ module Ably
|
|
292
347
|
|
293
348
|
response = case method.to_sym
|
294
349
|
when :get
|
295
|
-
|
350
|
+
reauthorize_on_authorization_failure do
|
296
351
|
send_request(method, path, params, headers: headers)
|
297
352
|
end
|
298
353
|
when :post
|
299
354
|
path_with_params = Addressable::URI.new
|
300
355
|
path_with_params.query_values = params || {}
|
301
356
|
query = path_with_params.query
|
302
|
-
|
357
|
+
reauthorize_on_authorization_failure do
|
303
358
|
send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
|
304
359
|
end
|
305
360
|
end
|
@@ -321,6 +376,20 @@ module Ably
|
|
321
376
|
Models::HttpPaginatedResponse.new(response, path, self)
|
322
377
|
end
|
323
378
|
|
379
|
+
# The local device detilas
|
380
|
+
# @return [Ably::Models::LocalDevice]
|
381
|
+
#
|
382
|
+
# @note This is unsupported in the Ruby library
|
383
|
+
def device
|
384
|
+
raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable'
|
385
|
+
end
|
386
|
+
|
387
|
+
# Push notification object for publishing and managing push notifications
|
388
|
+
# @return [Ably::Rest::Push]
|
389
|
+
def push
|
390
|
+
@push ||= Push.new(self)
|
391
|
+
end
|
392
|
+
|
324
393
|
# @!attribute [r] endpoint
|
325
394
|
# @return [URI::Generic] Default Ably REST endpoint used for all requests
|
326
395
|
def endpoint
|
@@ -389,7 +458,6 @@ module Ably
|
|
389
458
|
def fallback_connection
|
390
459
|
unless defined?(@fallback_connections) && @fallback_connections
|
391
460
|
@fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
|
392
|
-
@fallback_connections << Faraday.new(endpoint.to_s, connection_options) # Try the original host last if all fallbacks have been used
|
393
461
|
end
|
394
462
|
@fallback_index ||= 0
|
395
463
|
|
@@ -410,23 +478,58 @@ module Ably
|
|
410
478
|
|
411
479
|
# Allowable duration for an external auth request
|
412
480
|
# For REST client this defaults to request_timeout
|
413
|
-
# For Realtime clients this defaults to realtime_request_timeout
|
481
|
+
# For Realtime clients this defaults to 250ms less than the realtime_request_timeout
|
482
|
+
# ensuring an auth failure will be triggered before the realtime request timeout fires
|
483
|
+
# which would lead to a misleading error message (connection timeout as opposed to auth request timeout)
|
414
484
|
# @api private
|
415
485
|
def auth_request_timeout
|
416
486
|
if @realtime_client
|
417
|
-
@realtime_client.connection.defaults.fetch(:realtime_request_timeout)
|
487
|
+
@realtime_client.connection.defaults.fetch(:realtime_request_timeout) - 0.25
|
418
488
|
else
|
419
489
|
http_defaults.fetch(:request_timeout)
|
420
490
|
end
|
421
491
|
end
|
422
492
|
|
493
|
+
# If the primary host endpoint fails, and a subsequent fallback host succeeds, the fallback
|
494
|
+
# host that succeeded is used for +ClientOption+ +fallback_retry_timeout+ seconds to avoid
|
495
|
+
# retries to known failing hosts for a short period of time.
|
496
|
+
# See https://github.com/ably/docs/pull/554, spec id #RSC15f
|
497
|
+
#
|
498
|
+
# @return [nil, String] Returns nil (falsey) if the primary host is being used, or the currently used host if a fallback host is currently preferred
|
499
|
+
def using_preferred_fallback_host?
|
500
|
+
if preferred_fallback_connection && (preferred_fallback_connection.fetch(:expires_at) > Time.now)
|
501
|
+
preferred_fallback_connection.fetch(:connection_object).host
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
423
505
|
private
|
506
|
+
|
507
|
+
attr_reader :preferred_fallback_connection
|
508
|
+
|
509
|
+
# See #using_preferred_fallback_host? for context
|
510
|
+
def set_preferred_fallback_connection(connection)
|
511
|
+
@preferred_fallback_connection = if connection == @connection
|
512
|
+
# If the succeeded connection is in fact the primary connection (tried after a failed fallback)
|
513
|
+
# then clear the preferred fallback connection
|
514
|
+
nil
|
515
|
+
else
|
516
|
+
{
|
517
|
+
expires_at: Time.now + options.fetch(:fallback_retry_timeout),
|
518
|
+
connection_object: connection,
|
519
|
+
}
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def get_preferred_fallback_connection_object
|
524
|
+
preferred_fallback_connection.fetch(:connection_object) if using_preferred_fallback_host?
|
525
|
+
end
|
526
|
+
|
424
527
|
def raw_request(method, path, params = {}, options = {})
|
425
528
|
options = options.clone
|
426
529
|
if options.delete(:disable_automatic_reauthorize) == true
|
427
530
|
send_request(method, path, params, options)
|
428
531
|
else
|
429
|
-
|
532
|
+
reauthorize_on_authorization_failure do
|
430
533
|
send_request(method, path, params, options)
|
431
534
|
end
|
432
535
|
end
|
@@ -442,15 +545,30 @@ module Ably
|
|
442
545
|
retry_sequence_id = nil
|
443
546
|
request_id = SecureRandom.urlsafe_base64(10) if add_request_ids
|
444
547
|
|
548
|
+
preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object
|
549
|
+
|
445
550
|
begin
|
446
|
-
use_fallback = can_fallback_to_alternate_ably_host? && retry_count > 0
|
551
|
+
use_fallback = can_fallback_to_alternate_ably_host? && (retry_count > 0)
|
552
|
+
|
553
|
+
conn = if preferred_fallback_connection_for_first_request
|
554
|
+
case retry_count
|
555
|
+
when 0
|
556
|
+
preferred_fallback_connection_for_first_request
|
557
|
+
when 1
|
558
|
+
# Ensure the root host is used first if the preferred fallback fails, see #RSC15f
|
559
|
+
connection(use_fallback: false)
|
560
|
+
end
|
561
|
+
end || connection(use_fallback: use_fallback) # default to normal connection selection process if not preferred connection set
|
447
562
|
|
448
|
-
|
563
|
+
conn.send(method, path, params) do |request|
|
449
564
|
if add_request_ids
|
450
565
|
request.params[:request_id] = request_id
|
451
566
|
request.options.context = {} if request.options.context.nil?
|
452
567
|
request.options.context[:request_id] = request_id
|
453
568
|
end
|
569
|
+
if options[:qs_params]
|
570
|
+
request.params.merge!(options[:qs_params])
|
571
|
+
end
|
454
572
|
unless options[:send_auth_header] == false
|
455
573
|
request.headers[:authorization] = auth.auth_header
|
456
574
|
if options[:headers]
|
@@ -461,41 +579,45 @@ module Ably
|
|
461
579
|
end
|
462
580
|
end.tap do
|
463
581
|
if retry_count > 0
|
464
|
-
|
582
|
+
retry_log_severity = log_retries_as_info ? :info : :warn
|
583
|
+
logger.public_send(retry_log_severity) do
|
465
584
|
"Ably::Rest::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
|
466
585
|
" #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
|
467
586
|
end
|
587
|
+
set_preferred_fallback_connection conn
|
468
588
|
end
|
469
589
|
end
|
470
590
|
|
471
|
-
rescue Faraday::TimeoutError,
|
591
|
+
rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error
|
472
592
|
retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
|
473
593
|
time_passed = Time.now - requested_at
|
474
594
|
|
475
|
-
if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
|
595
|
+
if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration)
|
476
596
|
retry_count += 1
|
477
|
-
|
597
|
+
retry_log_severity = log_retries_as_info ? :info : :warn
|
598
|
+
logger.public_send(retry_log_severity) { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
|
478
599
|
retry
|
479
600
|
end
|
480
601
|
|
481
|
-
|
602
|
+
retry_log_severity = log_retries_as_info ? :info : :error
|
603
|
+
logger.public_send(retry_log_severity) do
|
482
604
|
"Ably::Rest::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
|
483
605
|
" #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
|
484
606
|
end
|
485
607
|
|
486
608
|
case error
|
487
609
|
when Faraday::TimeoutError
|
488
|
-
raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil,
|
489
|
-
when
|
610
|
+
raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id })
|
611
|
+
when *FARADAY_CLIENT_OR_SERVER_ERRORS
|
490
612
|
# request_id is also available in the request context
|
491
|
-
raise Ably::Exceptions::ConnectionError.new(error.message, nil,
|
613
|
+
raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id })
|
492
614
|
else
|
493
615
|
raise error
|
494
616
|
end
|
495
617
|
end
|
496
618
|
end
|
497
619
|
|
498
|
-
def
|
620
|
+
def reauthorize_on_authorization_failure
|
499
621
|
yield
|
500
622
|
rescue Ably::Exceptions::TokenExpired => e
|
501
623
|
if auth.token_renewable?
|