ably 1.0.6 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +10 -8
  4. data/CHANGELOG.md +75 -3
  5. data/LICENSE +1 -3
  6. data/README.md +12 -7
  7. data/Rakefile +32 -0
  8. data/SPEC.md +1277 -835
  9. data/ably.gemspec +14 -9
  10. data/lib/ably/auth.rb +30 -4
  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 +154 -32
  49. data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  50. data/lib/ably/rest/presence.rb +1 -0
  51. data/lib/ably/rest/push.rb +42 -0
  52. data/lib/ably/rest/push/admin.rb +54 -0
  53. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  54. data/lib/ably/rest/push/device_registrations.rb +103 -0
  55. data/lib/ably/version.rb +7 -2
  56. data/spec/acceptance/realtime/auth_spec.rb +245 -17
  57. data/spec/acceptance/realtime/channel_history_spec.rb +26 -20
  58. data/spec/acceptance/realtime/channel_spec.rb +177 -59
  59. data/spec/acceptance/realtime/client_spec.rb +153 -0
  60. data/spec/acceptance/realtime/connection_failures_spec.rb +72 -6
  61. data/spec/acceptance/realtime/connection_spec.rb +129 -18
  62. data/spec/acceptance/realtime/message_spec.rb +36 -34
  63. data/spec/acceptance/realtime/presence_spec.rb +201 -167
  64. data/spec/acceptance/realtime/push_admin_spec.rb +736 -0
  65. data/spec/acceptance/realtime/push_spec.rb +27 -0
  66. data/spec/acceptance/rest/auth_spec.rb +41 -3
  67. data/spec/acceptance/rest/base_spec.rb +2 -2
  68. data/spec/acceptance/rest/channel_spec.rb +79 -4
  69. data/spec/acceptance/rest/channels_spec.rb +6 -0
  70. data/spec/acceptance/rest/client_spec.rb +129 -10
  71. data/spec/acceptance/rest/message_spec.rb +158 -6
  72. data/spec/acceptance/rest/push_admin_spec.rb +952 -0
  73. data/spec/acceptance/rest/push_spec.rb +25 -0
  74. data/spec/acceptance/rest/time_spec.rb +1 -1
  75. data/spec/run_parallel_tests +33 -0
  76. data/spec/spec_helper.rb +1 -1
  77. data/spec/support/debug_failure_helper.rb +9 -5
  78. data/spec/support/test_app.rb +2 -2
  79. data/spec/unit/logger_spec.rb +10 -3
  80. data/spec/unit/models/device_details_spec.rb +102 -0
  81. data/spec/unit/models/device_push_details_spec.rb +101 -0
  82. data/spec/unit/models/error_info_spec.rb +51 -3
  83. data/spec/unit/models/message_spec.rb +17 -2
  84. data/spec/unit/models/presence_message_spec.rb +1 -1
  85. data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  86. data/spec/unit/modules/enum_spec.rb +1 -1
  87. data/spec/unit/realtime/client_spec.rb +13 -1
  88. data/spec/unit/realtime/connection_spec.rb +1 -1
  89. data/spec/unit/realtime/push_channel_spec.rb +36 -0
  90. data/spec/unit/rest/channel_spec.rb +8 -1
  91. data/spec/unit/rest/client_spec.rb +30 -0
  92. data/spec/unit/rest/push_channel_spec.rb +36 -0
  93. metadata +95 -26
@@ -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
@@ -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
- # Whethere the {Client} has to add a random identifier to the path of a request
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 = options.delete(:realtime_client)
143
- @tls = options.delete(:tls) == false ? false : true
144
- @environment = options.delete(:environment) # nil is production
145
- @environment = nil if [:production, 'production'].include?(@environment)
146
- @protocol = options.delete(:protocol) || :msgpack
147
- @debug_http = options.delete(:debug_http)
148
- @log_level = options.delete(:log_level) || ::Logger::WARN
149
- @custom_logger = options.delete(:logger)
150
- @custom_host = options.delete(:rest_host)
151
- @custom_port = options.delete(:port)
152
- @custom_tls_port = options.delete(:tls_port)
153
- @add_request_ids = options.delete(: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 = options
194
- @auth = Auth.new(self, token_params, options)
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
- reauthorize_on_authorisation_failure do
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
- reauthorize_on_authorisation_failure do
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
- reauthorize_on_authorisation_failure do
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
- connection(use_fallback: use_fallback).send(method, path, params) do |request|
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
- logger.warn do
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, Faraday::ClientError, Ably::Exceptions::ServerError => error
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
- logger.warn { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed (seq ##{retry_sequence_id}): #{error}" }
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
- logger.error do
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, 80014, error, { request_id: request_id })
489
- when Faraday::ClientError
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, 80000, error, { request_id: request_id })
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 reauthorize_on_authorisation_failure
620
+ def reauthorize_on_authorization_failure
499
621
  yield
500
622
  rescue Ably::Exceptions::TokenExpired => e
501
623
  if auth.token_renewable?