ably-rest 1.0.5 → 1.1.3

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 (118) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +6 -3
  3. data/CHANGELOG.md +1 -1
  4. data/LICENSE +1 -1
  5. data/README.md +26 -7
  6. data/SPEC.md +2003 -1605
  7. data/ably-rest.gemspec +4 -2
  8. data/lib/submodules/ably-ruby/.editorconfig +14 -0
  9. data/lib/submodules/ably-ruby/.travis.yml +10 -8
  10. data/lib/submodules/ably-ruby/CHANGELOG.md +97 -1
  11. data/lib/submodules/ably-ruby/LICENSE +1 -3
  12. data/lib/submodules/ably-ruby/README.md +12 -7
  13. data/lib/submodules/ably-ruby/Rakefile +32 -0
  14. data/lib/submodules/ably-ruby/SPEC.md +1277 -835
  15. data/lib/submodules/ably-ruby/ably.gemspec +17 -11
  16. data/lib/submodules/ably-ruby/lib/ably/auth.rb +34 -8
  17. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +10 -4
  18. data/lib/submodules/ably-ruby/lib/ably/logger.rb +8 -2
  19. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
  20. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
  21. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
  24. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +12 -12
  25. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +6 -4
  26. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +6 -4
  27. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
  28. data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
  29. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +2 -2
  30. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +2 -2
  31. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +2 -2
  32. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +2 -2
  33. data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
  34. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
  35. data/lib/submodules/ably-ruby/lib/ably/modules/safe_deferrable.rb +1 -1
  36. data/lib/submodules/ably-ruby/lib/ably/modules/safe_yield.rb +1 -1
  37. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +5 -5
  38. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -2
  39. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +2 -2
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +27 -105
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +4 -8
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  44. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
  45. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
  46. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +91 -3
  47. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +9 -4
  48. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  49. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +45 -26
  50. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +25 -9
  51. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +2 -2
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +7 -7
  53. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +9 -9
  54. data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
  55. data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
  56. data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  57. data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
  58. data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
  59. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +54 -18
  60. data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
  61. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +171 -41
  62. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  63. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  64. data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
  65. data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
  66. data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  67. data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
  68. data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
  69. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +253 -49
  70. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +33 -21
  71. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +180 -62
  72. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +155 -2
  73. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +293 -13
  74. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +142 -39
  75. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +38 -36
  76. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +12 -3
  77. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +207 -173
  78. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +736 -0
  79. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
  80. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +62 -51
  81. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
  82. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +79 -4
  83. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +6 -0
  84. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +318 -74
  85. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +158 -6
  86. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +952 -0
  87. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
  88. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
  90. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +1 -9
  91. data/lib/submodules/ably-ruby/spec/spec_helper.rb +3 -1
  92. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +9 -5
  93. data/lib/submodules/ably-ruby/spec/support/event_emitter_helper.rb +31 -0
  94. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +1 -1
  95. data/lib/submodules/ably-ruby/spec/support/test_app.rb +2 -2
  96. data/lib/submodules/ably-ruby/spec/support/test_logger_helper.rb +42 -0
  97. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +11 -12
  98. data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
  99. data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
  100. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
  101. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
  102. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  103. data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  104. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -2
  105. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +1 -1
  106. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +3 -3
  107. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +10 -10
  108. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +1 -1
  109. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +13 -1
  110. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +2 -2
  111. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +1 -1
  112. data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
  113. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +30 -1
  114. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
  115. data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
  116. data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +3 -3
  117. data/spec/spec_helper.rb +1 -0
  118. metadata +51 -10
@@ -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
  #
@@ -22,22 +32,22 @@ module Ably
22
32
  # @option channel_options [Hash,Ably::Models::CipherParams] :cipher A hash of options or a {Ably::Models::CipherParams} to configure the encryption. *:key* is required, all other options are optional. See {Ably::Util::Crypto#initialize} for a list of +:cipher+ options
23
33
  #
24
34
  def initialize(client, name, channel_options = {})
25
- ensure_utf_8 :name, name
35
+ name = (ensure_utf_8 :name, name)
26
36
 
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
- 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
@@ -439,25 +542,33 @@ module Ably
439
542
  max_retry_duration = http_defaults.fetch(:max_retry_duration)
440
543
  requested_at = Time.now
441
544
  retry_count = 0
442
- request_id = nil
443
- if add_request_ids
444
- params = if params.nil?
445
- {}
446
- else
447
- params.dup
448
- end
449
- request_id = SecureRandom.urlsafe_base64(10)
450
- params[:request_id] = request_id
451
- end
545
+ retry_sequence_id = nil
546
+ request_id = SecureRandom.urlsafe_base64(10) if add_request_ids
547
+
548
+ preferred_fallback_connection_for_first_request = get_preferred_fallback_connection_object
452
549
 
453
550
  begin
454
- 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
455
562
 
456
- connection(use_fallback: use_fallback).send(method, path, params) do |request|
563
+ conn.send(method, path, params) do |request|
457
564
  if add_request_ids
565
+ request.params[:request_id] = request_id
458
566
  request.options.context = {} if request.options.context.nil?
459
567
  request.options.context[:request_id] = request_id
460
568
  end
569
+ if options[:qs_params]
570
+ request.params.merge!(options[:qs_params])
571
+ end
461
572
  unless options[:send_auth_header] == false
462
573
  request.headers[:authorization] = auth.auth_header
463
574
  if options[:headers]
@@ -466,28 +577,47 @@ module Ably
466
577
  end
467
578
  end
468
579
  end
580
+ end.tap do
581
+ if retry_count > 0
582
+ retry_log_severity = log_retries_as_info ? :info : :warn
583
+ logger.public_send(retry_log_severity) do
584
+ "Ably::Rest::Client - Request SUCCEEDED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
585
+ " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
586
+ end
587
+ set_preferred_fallback_connection conn
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
592
+ retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
472
593
  time_passed = Time.now - requested_at
473
- if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
594
+
595
+ if can_fallback_to_alternate_ably_host? && (retry_count < max_retry_count) && (time_passed <= max_retry_duration)
474
596
  retry_count += 1
475
- logger.warn { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed: #{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}" }
476
599
  retry
477
600
  end
601
+
602
+ retry_log_severity = log_retries_as_info ? :info : :error
603
+ logger.public_send(retry_log_severity) do
604
+ "Ably::Rest::Client - Request FAILED after #{retry_count} #{retry_count > 1 ? 'retries' : 'retry' } for" \
605
+ " #{method} #{path} #{params} (seq ##{retry_sequence_id}, time elapsed #{(Time.now.to_f - requested_at.to_f).round(2)}s)"
606
+ end
607
+
478
608
  case error
479
609
  when Faraday::TimeoutError
480
- raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, 80014, error, { request_id: request_id })
481
- 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
482
612
  # request_id is also available in the request context
483
- 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 })
484
614
  else
485
615
  raise error
486
616
  end
487
617
  end
488
618
  end
489
619
 
490
- def reauthorize_on_authorisation_failure
620
+ def reauthorize_on_authorization_failure
491
621
  yield
492
622
  rescue Ably::Exceptions::TokenExpired => e
493
623
  if auth.token_renewable?
@@ -549,7 +679,7 @@ module Ably
549
679
  setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
550
680
 
551
681
  # Set Faraday's HTTP adapter
552
- builder.adapter Faraday.default_adapter
682
+ builder.adapter :excon
553
683
  end
554
684
  end
555
685