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.
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