Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,58 @@ Ably::PubSub::Realtime::Connection::STATE.Connected

`Ably::Rest` and `Ably::Realtime` raise `NameError` in 2.0.

### Connection options: `endpoint`

The `:endpoint` option is now the only way to choose where a client connects. The
`:environment`, `:rest_host`, `:realtime_host` (and its alias `:ws_host`) and
`:fallback_hosts_use_default` options have been removed and are ignored, so a client that
still passes them connects to production (`main.realtime.ably.net`).

| 1.x | 2.0 |
| --- | --- |
| no host options | no change; traffic moves to `main.realtime.ably.net` |
| `environment: 'sandbox'` | `endpoint: 'nonprod:sandbox'` |
| `environment: 'acme'` (dedicated cluster) | `endpoint: 'acme'` |
| `rest_host:` / `realtime_host: 'localhost'` | `endpoint: 'localhost'` |
| `rest_host` and `realtime_host` set to the same custom host | `endpoint: '<that host>'` |
| `rest_host` and `realtime_host` set to different hosts | not supported; use one host that serves both, or contact Ably |
| `fallback_hosts_use_default: true` | delete it |
| custom `fallback_hosts` | unchanged |

```ruby
# 1.x
client = Ably::Rest::Client.new(key: ENV['ABLY_API_KEY'], environment: 'sandbox')

# 2.0
client = Ably::PubSub::Server.create_http_client(key: ENV['ABLY_API_KEY'], endpoint: 'nonprod:sandbox')
```

REST requests and the realtime connection now use the same primary domain:

| `endpoint` | Primary domain | Default fallback hosts |
| --- | --- | --- |
| unset | `main.realtime.ably.net` | `main.[a-e].fallback.ably-realtime.com` |
| `'acme'` | `acme.realtime.ably.net` | `acme.[a-e].fallback.ably-realtime.com` |
| `'nonprod:sandbox'` | `sandbox.realtime.ably-nonprod.net` | `sandbox.[a-e].fallback.ably-realtime-nonprod.com` |
| a hostname, such as `'localhost'`, `'127.0.0.1'` or `'ably.example.com'` | the hostname | none |

A custom `port` or `tls_port` no longer disables the default fallback hosts; pass
`fallback_hosts: []` to disable them.

Firewall allowlists and proxies must allow `*.realtime.ably.net` and
`*.fallback.ably-realtime.com` in place of `rest.ably.io`, `realtime.ably.io` and
`*.ably-realtime.com`.

Related client attributes:

| 1.x | 2.0 |
| --- | --- |
| `client.endpoint` (a `URI`) | `client.uri`; `client.endpoint` now returns the `:endpoint` option |
| `client.environment` | `client.endpoint` |
| `client.custom_host`, `realtime_client.custom_realtime_host` | `client.primary_domain` |
| `Ably::PubSub::Http::Client::DOMAIN`, `Ably::PubSub::Realtime::Client::DOMAIN` | `client.primary_domain` |
| `Ably::CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES` | removed |

### Releasing realtime channels

In 1.x, `Channels#release` detached a realtime channel before removing it. In 2.0, a realtime channel can only be released
Expand Down
53 changes: 52 additions & 1 deletion core/lib/ably/internal.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Ably
# Construction entry points for Ably's own per-side packages: +ably-pubsub-server+, and
# any future device-side package.
# any future device-side package, plus endpoint resolution shared by the clients.
#
# Applications must not use this module. It carries no compatibility guarantee and may
# change in any release, including a patch. Use the factory functions of the package you
Expand All @@ -12,6 +12,12 @@ module Ably
# platform and a directly constructed client declares none. These entry points are how a
# per-side package constructs the client it has stamped its side onto.
module Internal
# Prefix of an +:endpoint+ value that selects a non-production routing policy, such as +nonprod:sandbox+
NONPROD_ENDPOINT_PREFIX = 'nonprod:'.freeze
NONPROD_ROOT_DOMAIN = 'ably-nonprod.net'.freeze
NONPROD_FALLBACK_DOMAIN = 'ably-realtime-nonprod.com'.freeze
private_constant :NONPROD_ENDPOINT_PREFIX, :NONPROD_ROOT_DOMAIN, :NONPROD_FALLBACK_DOMAIN

class << self
# Constructs an {Ably::PubSub::Http::Client}, bypassing the direct-construction refusal.
#
Expand Down Expand Up @@ -52,13 +58,58 @@ def direct_construction_error(subject, factory)
MSG
end

# Returns the primary domain for the given +:endpoint+ client option value
#
# @spec REC1b2, REC1b3, REC1b4
# @param endpoint [String] a routing policy name such as +main+, a +nonprod:[id]+ routing policy, or a hostname
# @return [String]
# @api private
def primary_domain(endpoint)
return endpoint if hostname_endpoint?(endpoint)

if nonprod_endpoint?(endpoint)
"#{nonprod_endpoint_id(endpoint)}.realtime.#{NONPROD_ROOT_DOMAIN}"
else
"#{endpoint}.realtime.#{Ably::PROD_ROOT_DOMAIN}"
end
end

# Returns the default fallback domains for the given +:endpoint+ client option value
#
# @spec REC2c1, REC2c2, REC2c3, REC2c4
# @param endpoint [String] a routing policy name such as +main+, a +nonprod:[id]+ routing policy, or a hostname
# @return [Array<String>] empty when +endpoint+ is a hostname
# @api private
def fallback_domains(endpoint)
return [] if hostname_endpoint?(endpoint)

if nonprod_endpoint?(endpoint)
Ably::FALLBACK_IDS.map { |id| "#{nonprod_endpoint_id(endpoint)}.#{id}.fallback.#{NONPROD_FALLBACK_DOMAIN}" }
else
Ably::FALLBACK_IDS.map { |id| "#{endpoint}.#{id}.fallback.#{Ably::PROD_FALLBACK_DOMAIN}" }
end
end

private

# +allocate+ skips the class's overridden +new+, so +initialize+ has to be invoked
# explicitly. Every client instance in the library is built through here.
def construct(klass, options)
klass.allocate.tap { |client| client.send(:initialize, options) }
end

# @spec REC1b2
def hostname_endpoint?(endpoint)
endpoint.include?('.') || endpoint.include?('::') || endpoint == 'localhost'
end

def nonprod_endpoint?(endpoint)
endpoint.start_with?(NONPROD_ENDPOINT_PREFIX)
end

def nonprod_endpoint_id(endpoint)
endpoint.delete_prefix(NONPROD_ENDPOINT_PREFIX)
end
end
end
end
23 changes: 12 additions & 11 deletions core/lib/ably/modules/ably.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
#
# @see file:README.md README
module Ably
# Fallback hosts to use when a connection to rest/realtime.ably.io is not possible due to
# network failures either at the client, between the client and Ably, within an Ably data center, or at the IO domain registrar
# Default routing policy used when the +:endpoint+ client option is not provided
# @spec REC1a
ENDPOINT = 'main'.freeze

PROD_ROOT_DOMAIN = 'ably.net'.freeze
PROD_FALLBACK_DOMAIN = 'ably-realtime.com'.freeze

# Fallback hosts are used when a connection to the primary domain is not possible due to
# network failures either at the client, between the client and Ably, within an Ably data center, or at the domain registrar
# see https://ably.com/docs/client-lib-development-guide/features/#RSC15a
#
FALLBACK_DOMAIN = 'ably-realtime.com'.freeze
FALLBACK_IDS = %w(a b c d e).freeze

# Default production fallbacks a.ably-realtime.com ... e.ably-realtime.com
FALLBACK_HOSTS = FALLBACK_IDS.map { |host| "#{host}.#{FALLBACK_DOMAIN}".freeze }.freeze

# Custom environment default fallbacks {ENV}-a-fallback.ably-realtime.com ... {ENV}-a-fallback.ably-realtime.com
CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES = FALLBACK_IDS.map do |host|
"-#{host}-fallback.#{FALLBACK_DOMAIN}".freeze
end.freeze
# Default production fallbacks main.a.fallback.ably-realtime.com ... main.e.fallback.ably-realtime.com
# @spec REC2c1
FALLBACK_HOSTS = FALLBACK_IDS.map { |id| "#{ENDPOINT}.#{id}.fallback.#{PROD_FALLBACK_DOMAIN}".freeze }.freeze

INTERNET_CHECK = {
url: '//internet-up.ably-realtime.com/is-the-internet-up.txt',
Expand Down
60 changes: 23 additions & 37 deletions core/lib/ably/pubsub/http/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ class Client
extend Forwardable
using Ably::Util::AblyExtensions

# Default Ably domain for REST
DOMAIN = 'rest.ably.io'

MAX_MESSAGE_SIZE = 65536 # See spec TO3l8
MAX_FRAME_SIZE = 524288 # See spec TO3l8

Expand All @@ -44,9 +41,15 @@ class Client

def_delegators :auth, :client_id, :auth_options

# Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
# The +:endpoint+ client option: a routing policy name such as +main+, a +nonprod:[id]+ routing policy, or a hostname
# @spec REC1
# @return [String]
attr_reader :endpoint

# The domain that REST requests and realtime connections are sent to, derived from {#endpoint}
# @spec REC1b
# @return [String]
attr_reader :environment
attr_reader :primary_domain

# The protocol configured for this client, either binary `:msgpack` or text based `:json`
# @return [Symbol]
Expand All @@ -70,10 +73,6 @@ class Client
# @return [Logger::Severity]
attr_reader :log_level

# The custom host that is being used if it was provided with the option +:rest_host+ when the {Client} was created
# @return [String,Nil]
attr_reader :custom_host

# The custom port for non-TLS requests if it was provided with the option +:port+ when the {Client} was created
# @return [Integer,Nil]
attr_reader :custom_port
Expand Down Expand Up @@ -147,7 +146,7 @@ def self.new(*args, **kwargs, &block)
# @option options [String] :token Token string or {Models::TokenDetails} used to authenticate requests
# @option options [String] :token_details {Models::TokenDetails} used to authenticate requests
# @option options [Boolean] :use_token_auth Will force Basic Auth if set to false, and Token auth if set to true
# @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
# @option options [String] :endpoint ('main') A routing policy name such as 'main', a non-production routing policy such as 'nonprod:sandbox', or a hostname such as 'localhost'
# @option options [Symbol] :protocol (:msgpack) Protocol used to communicate with Ably, :json and :msgpack currently supported
# @option options [Boolean] :use_binary_protocol (true) When true will use the MessagePack binary protocol, when false it will use JSON encoding. This option will overide :protocol option
# @option options [Logger::Severity,Symbol] :log_level (Logger::WARN) Log level for the standard Logger that outputs to STDOUT. Can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none
Expand All @@ -167,8 +166,8 @@ def self.new(*args, **kwargs, &block)
# @option options [Integer] :http_max_retry_count (3) maximum number of fallback host retries for HTTP requests that fail due to network issues or server problems
# @option options [Integer] :http_max_retry_duration (15 seconds) maximum elapsed time in which fallback host retries for HTTP requests will be attempted i.e. if the first default host attempt takes 5s, and then the subsequent fallback retry attempt takes 7s, no further fallback host attempts will be made as the total elapsed time of 12s exceeds the default 10s limit
#
# @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
# @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
# @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 domain. If an empty array is provided, the fallback host functionality is disabled.
# When not provided, fallback hosts are derived from +:endpoint+, and none are used if +:endpoint+ is a hostname
# @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
#
# @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.
Expand Down Expand Up @@ -209,14 +208,14 @@ def initialize(options)
@agent = "#{@agent} #{version ? "#{identifier}/#{version}" : identifier}"
end
@realtime_client = options.delete(:realtime_client)

@endpoint = (options.delete(:endpoint) || Ably::ENDPOINT).to_s
@primary_domain = Ably::Internal.primary_domain(endpoint)
@tls = options.delete_with_default(:tls, true)
@environment = options.delete(:environment) # nil is production
@environment = nil if [:production, 'production'].include?(@environment)
@protocol = options.delete(:protocol) || :msgpack
@debug_http = options.delete(:debug_http)
@log_level = options.delete(:log_level) || ::Logger::WARN
@custom_logger = options.delete(:logger)
@custom_host = options.delete(:rest_host)
@custom_port = options.delete(:port)
@custom_tls_port = options.delete(:tls_port)
@add_request_ids = options.delete(:add_request_ids)
Expand All @@ -225,21 +224,8 @@ def initialize(options)
@max_frame_size = options.delete(:max_frame_size) || MAX_FRAME_SIZE
@idempotent_rest_publishing = options.delete_with_default(:idempotent_rest_publishing, true)

if options[:fallback_hosts_use_default] && options[:fallback_hosts]
raise ArgumentError, "fallback_hosts_use_default cannot be set to try when fallback_hosts is also provided"
end
@fallback_hosts = case
when options.delete(:fallback_hosts_use_default)
Ably::FALLBACK_HOSTS
when options_fallback_hosts = options.delete(:fallback_hosts)
options_fallback_hosts
when custom_host || options[:realtime_host] || custom_port || custom_tls_port
[]
when environment
CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES.map { |host| "#{environment}#{host}" }
else
Ably::FALLBACK_HOSTS
end
# REC2a2, REC2c
@fallback_hosts = options.delete(:fallback_hosts) || Ably::Internal.fallback_domains(endpoint)

options[:fallback_retry_timeout] ||= FALLBACK_RETRY_TIMEOUT

Expand Down Expand Up @@ -448,10 +434,10 @@ def push
@push ||= Push.new(self)
end

# @!attribute [r] endpoint
# @return [URI::Generic] Default Ably REST endpoint used for all requests
def endpoint
endpoint_for_host(custom_host || [@environment, DOMAIN].compact.join('-'))
# @!attribute [r] uri
# @return [URI::Generic] Ably REST URI for the {#primary_domain}, used for all requests unless a fallback host is in use
def uri
uri_for_host(primary_domain)
end

# @!attribute [r] logger
Expand Down Expand Up @@ -502,7 +488,7 @@ def connection(options = {})
if options[:use_fallback]
fallback_connection
else
@connection ||= Faraday.new(endpoint.to_s, connection_options)
@connection ||= Faraday.new(uri.to_s, connection_options)
end
end

Expand All @@ -515,7 +501,7 @@ def connection(options = {})
# @api private
def fallback_connection
unless defined?(@fallback_connections) && @fallback_connections
@fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
@fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(uri_for_host(host).to_s, connection_options) }
end
@fallback_index ||= 0

Expand Down Expand Up @@ -675,7 +661,7 @@ def reauthorize_on_authorization_failure
end
end

def endpoint_for_host(host)
def uri_for_host(host)
port = if use_tls?
custom_tls_port
else
Expand Down
23 changes: 8 additions & 15 deletions core/lib/ably/pubsub/realtime/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ class Client
extend Forwardable
using Ably::Util::AblyExtensions

DOMAIN = 'realtime.ably.io'

# A {Aby::Realtime::Channels} object.
#
# @spec RTC3, RTS1
Expand Down Expand Up @@ -55,10 +53,6 @@ class Client
# @return [Boolean]
attr_reader :queue_messages

# The custom realtime websocket host that is being used if it was provided with the option `:ws_host` when the {Client} was created
# @return [String,Nil]
attr_reader :custom_realtime_host

# When true, as soon as the client library is instantiated it will connect to Ably. If this attribute is false, a connection must be opened explicitly
# @return [Boolean]
attr_reader :auto_connect
Expand All @@ -74,7 +68,7 @@ class Client
def_delegators :auth, :client_id, :auth_options
def_delegators :@rest_client, :encoders
def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary?
def_delegators :@rest_client, :environment, :custom_host, :custom_port, :custom_tls_port
def_delegators :@rest_client, :endpoint, :primary_domain, :custom_port, :custom_tls_port
def_delegators :@rest_client, :log_level
def_delegators :@rest_client, :options

Expand Down Expand Up @@ -139,7 +133,6 @@ def initialize(options)
@rest_client = Ably::Internal.create_http_client(options.merge(realtime_client: self))
@echo_messages = rest_client.options.fetch_with_default(:echo_messages, true)
@queue_messages = rest_client.options.fetch_with_default(:queue_messages, true)
@custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host]
@auto_connect = rest_client.options.fetch_with_default(:auto_connect, true)
@recover = rest_client.options.fetch_with_default(:recover, '')

Expand Down Expand Up @@ -304,10 +297,10 @@ def publish(channel_name, name, data = nil, attributes = {}, &success_block)
end
end

# @!attribute [r] endpoint
# @return [URI::Generic] Default Ably Realtime endpoint used for all requests
def endpoint
endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-'))
# @!attribute [r] uri
# @return [URI::Generic] Ably WebSocket URI for the {#primary_domain}, shared with REST requests (RTN2)
def uri
uri_for_host(primary_domain)
end

# (see Ably::PubSub::Http::Client#register_encoder)
Expand Down Expand Up @@ -337,8 +330,8 @@ def disable_automatic_connection_recovery
# @api private
def fallback_endpoint
unless defined?(@fallback_endpoints) && @fallback_endpoints
@fallback_endpoints = fallback_hosts.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) }
@fallback_endpoints << endpoint # Try the original host last if all fallbacks have been used
@fallback_endpoints = fallback_hosts.shuffle.map { |fallback_host| uri_for_host(fallback_host) }
@fallback_endpoints << uri # Try the original host last if all fallbacks have been used
end

fallback_endpoint_index = connection.manager.retry_count_for_state(:disconnected) + connection.manager.retry_count_for_state(:suspended) - 1
Expand All @@ -356,7 +349,7 @@ def device
end

private
def endpoint_for_host(host)
def uri_for_host(host)
port = if use_tls?
custom_tls_port
else
Expand Down
Loading
Loading