ably 1.0.7 → 1.1.4.rc
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 +58 -4
- data/LICENSE +1 -3
- data/README.md +9 -5
- data/Rakefile +32 -0
- data/SPEC.md +920 -565
- data/ably.gemspec +16 -11
- data/lib/ably/auth.rb +28 -2
- 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 +161 -35
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
- 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 +22 -21
- 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 +4 -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 +94 -31
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
@@ -3,6 +3,9 @@ require 'json'
|
|
3
3
|
require 'logger'
|
4
4
|
require 'uri'
|
5
5
|
|
6
|
+
require 'typhoeus'
|
7
|
+
require 'typhoeus/adapters/faraday'
|
8
|
+
|
6
9
|
require 'ably/rest/middleware/exceptions'
|
7
10
|
|
8
11
|
module Ably
|
@@ -30,6 +33,15 @@ module Ably
|
|
30
33
|
max_retry_count: 3
|
31
34
|
}.freeze
|
32
35
|
|
36
|
+
FALLBACK_RETRY_TIMEOUT = 10 * 60
|
37
|
+
|
38
|
+
# Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError
|
39
|
+
FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError)
|
40
|
+
[Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError]
|
41
|
+
else
|
42
|
+
Faraday::ClientError
|
43
|
+
end
|
44
|
+
|
33
45
|
def_delegators :auth, :client_id, :auth_options
|
34
46
|
|
35
47
|
# Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
|
@@ -83,10 +95,23 @@ module Ably
|
|
83
95
|
# if empty or nil then fallback host functionality is disabled
|
84
96
|
attr_reader :fallback_hosts
|
85
97
|
|
86
|
-
#
|
98
|
+
# Whether the {Client} has to add a random identifier to the path of a request
|
87
99
|
# @return [Boolean]
|
88
100
|
attr_reader :add_request_ids
|
89
101
|
|
102
|
+
# Retries are logged by default to warn and error. When true, retries are logged at info level
|
103
|
+
# @return [Boolean]
|
104
|
+
# @api private
|
105
|
+
attr_reader :log_retries_as_info
|
106
|
+
|
107
|
+
# True when idempotent publishing is enabled for all messages published via REST.
|
108
|
+
# When this feature is enabled, the client library will add a unique ID to every published message (without an ID)
|
109
|
+
# ensuring any failed published attempts (due to failures such as HTTP requests failing mid-flight) that are
|
110
|
+
# automatically retried will not result in duplicate messages being published to the Ably platform.
|
111
|
+
# Note: This is a beta unsupported feature!
|
112
|
+
# @return [Boolean]
|
113
|
+
attr_reader :idempotent_rest_publishing
|
114
|
+
|
90
115
|
# Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
|
91
116
|
#
|
92
117
|
# @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,7 +142,10 @@ module Ably
|
|
117
142
|
#
|
118
143
|
# @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
144
|
# @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
|
145
|
+
# @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
|
146
|
+
#
|
120
147
|
# @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.
|
148
|
+
# @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST
|
121
149
|
#
|
122
150
|
# @return [Ably::Rest::Client]
|
123
151
|
#
|
@@ -140,18 +168,21 @@ module Ably
|
|
140
168
|
end
|
141
169
|
end
|
142
170
|
|
143
|
-
@realtime_client
|
144
|
-
@tls
|
145
|
-
@environment
|
146
|
-
@environment
|
147
|
-
@protocol
|
148
|
-
@debug_http
|
149
|
-
@log_level
|
150
|
-
@custom_logger
|
151
|
-
@custom_host
|
152
|
-
@custom_port
|
153
|
-
@custom_tls_port
|
154
|
-
@add_request_ids
|
171
|
+
@realtime_client = options.delete(:realtime_client)
|
172
|
+
@tls = options.delete(:tls) == false ? false : true
|
173
|
+
@environment = options.delete(:environment) # nil is production
|
174
|
+
@environment = nil if [:production, 'production'].include?(@environment)
|
175
|
+
@protocol = options.delete(:protocol) || :msgpack
|
176
|
+
@debug_http = options.delete(:debug_http)
|
177
|
+
@log_level = options.delete(:log_level) || ::Logger::WARN
|
178
|
+
@custom_logger = options.delete(:logger)
|
179
|
+
@custom_host = options.delete(:rest_host)
|
180
|
+
@custom_port = options.delete(:port)
|
181
|
+
@custom_tls_port = options.delete(:tls_port)
|
182
|
+
@add_request_ids = options.delete(:add_request_ids)
|
183
|
+
@log_retries_as_info = options.delete(:log_retries_as_info)
|
184
|
+
@idempotent_rest_publishing = options.delete(:idempotent_rest_publishing) || Ably.major_minor_version_numeric > 1.1
|
185
|
+
|
155
186
|
|
156
187
|
if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
|
157
188
|
raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
|
@@ -167,9 +198,15 @@ module Ably
|
|
167
198
|
Ably::FALLBACK_HOSTS
|
168
199
|
end
|
169
200
|
|
201
|
+
options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT
|
202
|
+
|
203
|
+
# Take option keys prefixed with `http_`, remove the http_ and
|
204
|
+
# check if the option exists in HTTP_DEFAULTS. If so, update http_defaults
|
170
205
|
@http_defaults = HTTP_DEFAULTS.dup
|
171
206
|
options.each do |key, val|
|
172
207
|
if http_key = key[/^http_(.+)/, 1]
|
208
|
+
# Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout)
|
209
|
+
val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001)
|
173
210
|
@http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym)
|
174
211
|
end
|
175
212
|
end
|
@@ -191,8 +228,12 @@ module Ably
|
|
191
228
|
raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
|
192
229
|
|
193
230
|
token_params = options.delete(:default_token_params) || {}
|
194
|
-
@options
|
195
|
-
|
231
|
+
@options = options
|
232
|
+
init_auth_options = options.select do |key, _|
|
233
|
+
Auth::AUTH_OPTIONS_KEYS.include?(key.to_s)
|
234
|
+
end
|
235
|
+
|
236
|
+
@auth = Auth.new(self, token_params, init_auth_options)
|
196
237
|
@channels = Ably::Rest::Channels.new(self)
|
197
238
|
@encoders = []
|
198
239
|
|
@@ -274,6 +315,24 @@ module Ably
|
|
274
315
|
raw_request(:post, path, params, options)
|
275
316
|
end
|
276
317
|
|
318
|
+
# Perform an HTTP PUT request to the API using configured authentication
|
319
|
+
#
|
320
|
+
# @return [Faraday::Response]
|
321
|
+
#
|
322
|
+
# @api private
|
323
|
+
def put(path, params, options = {})
|
324
|
+
raw_request(:put, path, params, options)
|
325
|
+
end
|
326
|
+
|
327
|
+
# Perform an HTTP DELETE request to the API using configured authentication
|
328
|
+
#
|
329
|
+
# @return [Faraday::Response]
|
330
|
+
#
|
331
|
+
# @api private
|
332
|
+
def delete(path, params, options = {})
|
333
|
+
raw_request(:delete, path, params, options)
|
334
|
+
end
|
335
|
+
|
277
336
|
# Perform an HTTP request to the Ably API
|
278
337
|
# This is a convenience for customers who wish to use bleeding edge REST API functionality
|
279
338
|
# that is either not documented or is not included in the API for our client libraries.
|
@@ -293,14 +352,14 @@ module Ably
|
|
293
352
|
|
294
353
|
response = case method.to_sym
|
295
354
|
when :get
|
296
|
-
|
355
|
+
reauthorize_on_authorization_failure do
|
297
356
|
send_request(method, path, params, headers: headers)
|
298
357
|
end
|
299
358
|
when :post
|
300
359
|
path_with_params = Addressable::URI.new
|
301
360
|
path_with_params.query_values = params || {}
|
302
361
|
query = path_with_params.query
|
303
|
-
|
362
|
+
reauthorize_on_authorization_failure do
|
304
363
|
send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
|
305
364
|
end
|
306
365
|
end
|
@@ -322,6 +381,20 @@ module Ably
|
|
322
381
|
Models::HttpPaginatedResponse.new(response, path, self)
|
323
382
|
end
|
324
383
|
|
384
|
+
# The local device detilas
|
385
|
+
# @return [Ably::Models::LocalDevice]
|
386
|
+
#
|
387
|
+
# @note This is unsupported in the Ruby library
|
388
|
+
def device
|
389
|
+
raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable'
|
390
|
+
end
|
391
|
+
|
392
|
+
# Push notification object for publishing and managing push notifications
|
393
|
+
# @return [Ably::Rest::Push]
|
394
|
+
def push
|
395
|
+
@push ||= Push.new(self)
|
396
|
+
end
|
397
|
+
|
325
398
|
# @!attribute [r] endpoint
|
326
399
|
# @return [URI::Generic] Default Ably REST endpoint used for all requests
|
327
400
|
def endpoint
|
@@ -390,7 +463,6 @@ module Ably
|
|
390
463
|
def fallback_connection
|
391
464
|
unless defined?(@fallback_connections) && @fallback_connections
|
392
465
|
@fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
|
393
|
-
@fallback_connections << Faraday.new(endpoint.to_s, connection_options) # Try the original host last if all fallbacks have been used
|
394
466
|
end
|
395
467
|
@fallback_index ||= 0
|
396
468
|
|
@@ -411,23 +483,58 @@ module Ably
|
|
411
483
|
|
412
484
|
# Allowable duration for an external auth request
|
413
485
|
# For REST client this defaults to request_timeout
|
414
|
-
# For Realtime clients this defaults to realtime_request_timeout
|
486
|
+
# For Realtime clients this defaults to 250ms less than the realtime_request_timeout
|
487
|
+
# ensuring an auth failure will be triggered before the realtime request timeout fires
|
488
|
+
# which would lead to a misleading error message (connection timeout as opposed to auth request timeout)
|
415
489
|
# @api private
|
416
490
|
def auth_request_timeout
|
417
491
|
if @realtime_client
|
418
|
-
@realtime_client.connection.defaults.fetch(:realtime_request_timeout)
|
492
|
+
@realtime_client.connection.defaults.fetch(:realtime_request_timeout) - 0.25
|
419
493
|
else
|
420
494
|
http_defaults.fetch(:request_timeout)
|
421
495
|
end
|
422
496
|
end
|
423
497
|
|
498
|
+
# If the primary host endpoint fails, and a subsequent fallback host succeeds, the fallback
|
499
|
+
# host that succeeded is used for +ClientOption+ +fallback_retry_timeout+ seconds to avoid
|
500
|
+
# retries to known failing hosts for a short period of time.
|
501
|
+
# See https://github.com/ably/docs/pull/554, spec id #RSC15f
|
502
|
+
#
|
503
|
+
# @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
|
504
|
+
def using_preferred_fallback_host?
|
505
|
+
if preferred_fallback_connection && (preferred_fallback_connection.fetch(:expires_at) > Time.now)
|
506
|
+
preferred_fallback_connection.fetch(:connection_object).host
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
424
510
|
private
|
511
|
+
|
512
|
+
attr_reader :preferred_fallback_connection
|
513
|
+
|
514
|
+
# See #using_preferred_fallback_host? for context
|
515
|
+
def set_preferred_fallback_connection(connection)
|
516
|
+
@preferred_fallback_connection = if connection == @connection
|
517
|
+
# If the succeeded connection is in fact the primary connection (tried after a failed fallback)
|
518
|
+
# then clear the preferred fallback connection
|
519
|
+
nil
|
520
|
+
else
|
521
|
+
{
|
522
|
+
expires_at: Time.now + options.fetch(:fallback_retry_timeout),
|
523
|
+
connection_object: connection,
|
524
|
+
}
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
def get_preferred_fallback_connection_object
|
529
|
+
preferred_fallback_connection.fetch(:connection_object) if using_preferred_fallback_host?
|
530
|
+
end
|
531
|
+
|
425
532
|
def raw_request(method, path, params = {}, options = {})
|
426
533
|
options = options.clone
|
427
534
|
if options.delete(:disable_automatic_reauthorize) == true
|
428
535
|
send_request(method, path, params, options)
|
429
536
|
else
|
430
|
-
|
537
|
+
reauthorize_on_authorization_failure do
|
431
538
|
send_request(method, path, params, options)
|
432
539
|
end
|
433
540
|
end
|
@@ -443,15 +550,30 @@ module Ably
|
|
443
550
|
retry_sequence_id = nil
|
444
551
|
request_id = SecureRandom.urlsafe_base64(10) if add_request_ids
|
445
552
|
|
553
|
+
preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object
|
554
|
+
|
446
555
|
begin
|
447
|
-
use_fallback = can_fallback_to_alternate_ably_host? && retry_count > 0
|
556
|
+
use_fallback = can_fallback_to_alternate_ably_host? && (retry_count > 0)
|
557
|
+
|
558
|
+
conn = if preferred_fallback_connection_for_first_request
|
559
|
+
case retry_count
|
560
|
+
when 0
|
561
|
+
preferred_fallback_connection_for_first_request
|
562
|
+
when 1
|
563
|
+
# Ensure the root host is used first if the preferred fallback fails, see #RSC15f
|
564
|
+
connection(use_fallback: false)
|
565
|
+
end
|
566
|
+
end || connection(use_fallback: use_fallback) # default to normal connection selection process if not preferred connection set
|
448
567
|
|
449
|
-
|
568
|
+
conn.send(method, path, params) do |request|
|
450
569
|
if add_request_ids
|
451
570
|
request.params[:request_id] = request_id
|
452
571
|
request.options.context = {} if request.options.context.nil?
|
453
572
|
request.options.context[:request_id] = request_id
|
454
573
|
end
|
574
|
+
if options[:qs_params]
|
575
|
+
request.params.merge!(options[:qs_params])
|
576
|
+
end
|
455
577
|
unless options[:send_auth_header] == false
|
456
578
|
request.headers[:authorization] = auth.auth_header
|
457
579
|
if options[:headers]
|
@@ -462,41 +584,45 @@ module Ably
|
|
462
584
|
end
|
463
585
|
end.tap do
|
464
586
|
if retry_count > 0
|
465
|
-
|
587
|
+
retry_log_severity = log_retries_as_info ? :info : :warn
|
588
|
+
logger.public_send(retry_log_severity) do
|
466
589
|
"Ably::Rest::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
|
467
590
|
" #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
|
468
591
|
end
|
592
|
+
set_preferred_fallback_connection conn
|
469
593
|
end
|
470
594
|
end
|
471
595
|
|
472
|
-
rescue Faraday::TimeoutError,
|
596
|
+
rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error
|
473
597
|
retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
|
474
598
|
time_passed = Time.now - requested_at
|
475
599
|
|
476
|
-
if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
|
600
|
+
if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration)
|
477
601
|
retry_count += 1
|
478
|
-
|
602
|
+
retry_log_severity = log_retries_as_info ? :info : :warn
|
603
|
+
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}" }
|
479
604
|
retry
|
480
605
|
end
|
481
606
|
|
482
|
-
|
607
|
+
retry_log_severity = log_retries_as_info ? :info : :error
|
608
|
+
logger.public_send(retry_log_severity) do
|
483
609
|
"Ably::Rest::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
|
484
610
|
" #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
|
485
611
|
end
|
486
612
|
|
487
613
|
case error
|
488
614
|
when Faraday::TimeoutError
|
489
|
-
raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil,
|
490
|
-
when
|
615
|
+
raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id })
|
616
|
+
when *FARADAY_CLIENT_OR_SERVER_ERRORS
|
491
617
|
# request_id is also available in the request context
|
492
|
-
raise Ably::Exceptions::ConnectionError.new(error.message, nil,
|
618
|
+
raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id })
|
493
619
|
else
|
494
620
|
raise error
|
495
621
|
end
|
496
622
|
end
|
497
623
|
end
|
498
624
|
|
499
|
-
def
|
625
|
+
def reauthorize_on_authorization_failure
|
500
626
|
yield
|
501
627
|
rescue Ably::Exceptions::TokenExpired => e
|
502
628
|
if auth.token_renewable?
|
@@ -545,7 +671,7 @@ module Ably
|
|
545
671
|
}
|
546
672
|
end
|
547
673
|
|
548
|
-
# Return a Faraday middleware stack to initiate the Faraday::
|
674
|
+
# Return a Faraday middleware stack to initiate the Faraday::RackBuilder with
|
549
675
|
#
|
550
676
|
# @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
|
551
677
|
def middleware
|
@@ -557,8 +683,8 @@ module Ably
|
|
557
683
|
|
558
684
|
setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
|
559
685
|
|
560
|
-
# Set Faraday's HTTP adapter
|
561
|
-
builder.adapter :
|
686
|
+
# Set Faraday's HTTP adapter with support for HTTP/2
|
687
|
+
builder.adapter :typhoeus, http_version: :httpv2_0
|
562
688
|
end
|
563
689
|
end
|
564
690
|
|