ably 1.0.7 → 1.1.4.rc
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/.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
|
|