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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +10 -8
  4. data/CHANGELOG.md +58 -4
  5. data/LICENSE +1 -3
  6. data/README.md +9 -5
  7. data/Rakefile +32 -0
  8. data/SPEC.md +920 -565
  9. data/ably.gemspec +16 -11
  10. data/lib/ably/auth.rb +28 -2
  11. data/lib/ably/exceptions.rb +10 -4
  12. data/lib/ably/logger.rb +7 -1
  13. data/lib/ably/models/channel_state_change.rb +1 -1
  14. data/lib/ably/models/connection_state_change.rb +1 -1
  15. data/lib/ably/models/device_details.rb +87 -0
  16. data/lib/ably/models/device_push_details.rb +86 -0
  17. data/lib/ably/models/error_info.rb +23 -2
  18. data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  19. data/lib/ably/models/protocol_message.rb +32 -2
  20. data/lib/ably/models/push_channel_subscription.rb +89 -0
  21. data/lib/ably/modules/conversions.rb +1 -1
  22. data/lib/ably/modules/encodeable.rb +1 -1
  23. data/lib/ably/modules/exception_codes.rb +128 -0
  24. data/lib/ably/modules/model_common.rb +15 -2
  25. data/lib/ably/modules/state_machine.rb +2 -2
  26. data/lib/ably/realtime.rb +1 -0
  27. data/lib/ably/realtime/auth.rb +1 -1
  28. data/lib/ably/realtime/channel.rb +24 -102
  29. data/lib/ably/realtime/channel/channel_manager.rb +2 -6
  30. data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  31. data/lib/ably/realtime/channel/publisher.rb +74 -0
  32. data/lib/ably/realtime/channel/push_channel.rb +62 -0
  33. data/lib/ably/realtime/client.rb +91 -3
  34. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  35. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  36. data/lib/ably/realtime/connection.rb +34 -20
  37. data/lib/ably/realtime/connection/connection_manager.rb +25 -9
  38. data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  39. data/lib/ably/realtime/presence.rb +4 -4
  40. data/lib/ably/realtime/presence/members_map.rb +3 -3
  41. data/lib/ably/realtime/push.rb +40 -0
  42. data/lib/ably/realtime/push/admin.rb +61 -0
  43. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  44. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  45. data/lib/ably/rest.rb +1 -0
  46. data/lib/ably/rest/channel.rb +53 -17
  47. data/lib/ably/rest/channel/push_channel.rb +62 -0
  48. data/lib/ably/rest/client.rb +161 -35
  49. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  50. data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  51. data/lib/ably/rest/presence.rb +1 -0
  52. data/lib/ably/rest/push.rb +42 -0
  53. data/lib/ably/rest/push/admin.rb +54 -0
  54. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  55. data/lib/ably/rest/push/device_registrations.rb +103 -0
  56. data/lib/ably/version.rb +7 -2
  57. data/spec/acceptance/realtime/auth_spec.rb +22 -21
  58. data/spec/acceptance/realtime/channel_history_spec.rb +26 -20
  59. data/spec/acceptance/realtime/channel_spec.rb +177 -59
  60. data/spec/acceptance/realtime/client_spec.rb +153 -0
  61. data/spec/acceptance/realtime/connection_failures_spec.rb +72 -6
  62. data/spec/acceptance/realtime/connection_spec.rb +129 -18
  63. data/spec/acceptance/realtime/message_spec.rb +36 -34
  64. data/spec/acceptance/realtime/presence_spec.rb +201 -167
  65. data/spec/acceptance/realtime/push_admin_spec.rb +736 -0
  66. data/spec/acceptance/realtime/push_spec.rb +27 -0
  67. data/spec/acceptance/rest/auth_spec.rb +4 -3
  68. data/spec/acceptance/rest/base_spec.rb +2 -2
  69. data/spec/acceptance/rest/channel_spec.rb +79 -4
  70. data/spec/acceptance/rest/channels_spec.rb +6 -0
  71. data/spec/acceptance/rest/client_spec.rb +129 -10
  72. data/spec/acceptance/rest/message_spec.rb +158 -6
  73. data/spec/acceptance/rest/push_admin_spec.rb +952 -0
  74. data/spec/acceptance/rest/push_spec.rb +25 -0
  75. data/spec/acceptance/rest/time_spec.rb +1 -1
  76. data/spec/run_parallel_tests +33 -0
  77. data/spec/spec_helper.rb +1 -1
  78. data/spec/support/debug_failure_helper.rb +9 -5
  79. data/spec/support/test_app.rb +2 -2
  80. data/spec/unit/logger_spec.rb +10 -3
  81. data/spec/unit/models/device_details_spec.rb +102 -0
  82. data/spec/unit/models/device_push_details_spec.rb +101 -0
  83. data/spec/unit/models/error_info_spec.rb +51 -3
  84. data/spec/unit/models/message_spec.rb +17 -2
  85. data/spec/unit/models/presence_message_spec.rb +1 -1
  86. data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  87. data/spec/unit/modules/enum_spec.rb +1 -1
  88. data/spec/unit/realtime/client_spec.rb +13 -1
  89. data/spec/unit/realtime/connection_spec.rb +1 -1
  90. data/spec/unit/realtime/push_channel_spec.rb +36 -0
  91. data/spec/unit/rest/channel_spec.rb +8 -1
  92. data/spec/unit/rest/client_spec.rb +30 -0
  93. data/spec/unit/rest/push_channel_spec.rb +36 -0
  94. metadata +94 -31
@@ -1,6 +1,7 @@
1
1
  require 'ably/rest/channel'
2
2
  require 'ably/rest/channels'
3
3
  require 'ably/rest/client'
4
+ require 'ably/rest/push'
4
5
  require 'ably/rest/presence'
5
6
 
6
7
  require 'ably/models/message_encoders/base'
@@ -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
- attr_reader :client, :name, :options
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 name [String, Array<Ably::Models::Message|Hash>, 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
35
- # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
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
- def publish(name, data = nil, attributes = {})
58
- messages = if name.kind_of?(Enumerable)
59
- name
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
- name = ensure_utf_8(:name, name, allow_nil: true)
62
- ensure_supported_payload data
63
- [{ name: name, data: data }.merge(attributes)]
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
- response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload)
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
@@ -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
- # Whethere the {Client} has to add a random identifier to the path of a request
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 = options.delete(:realtime_client)
144
- @tls = options.delete(:tls) == false ? false : true
145
- @environment = options.delete(:environment) # nil is production
146
- @environment = nil if [:production, 'production'].include?(@environment)
147
- @protocol = options.delete(:protocol) || :msgpack
148
- @debug_http = options.delete(:debug_http)
149
- @log_level = options.delete(:log_level) || ::Logger::WARN
150
- @custom_logger = options.delete(:logger)
151
- @custom_host = options.delete(:rest_host)
152
- @custom_port = options.delete(:port)
153
- @custom_tls_port = options.delete(:tls_port)
154
- @add_request_ids = options.delete(: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 = options
195
- @auth = Auth.new(self, token_params, options)
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
- reauthorize_on_authorisation_failure do
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
- reauthorize_on_authorisation_failure do
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
- reauthorize_on_authorisation_failure do
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
- connection(use_fallback: use_fallback).send(method, path, params) do |request|
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
- logger.warn do
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, Faraday::ClientError, Ably::Exceptions::ServerError => error
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
- logger.warn { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
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
- logger.error do
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, 80014, error, { request_id: request_id })
490
- when Faraday::ClientError
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, 80000, error, { request_id: request_id })
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 reauthorize_on_authorisation_failure
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::Connection with
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 :excon
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