Skip to content

Commit 842bd77

Browse files
committed
Use endpoint as default connection option (ADR-119)
This implements ADR-119[1], which specifies the client connection options to update requests to the endpoints implemented as part of ADR-042[2]. The endpoint may be one of the following: * a routing policy name (such as `main`) * a nonprod routing policy name (such as `nonprod:sandbox`) * a FQDN such as `foo.example.com` The endpoint option is not valid with any of environment, restHost or realtimeHost, but we still intend to support the legacy options. If the client has been configured to use any of these legacy options, then they should continue to work in the same way, using the same primary and fallback hostnames. If the client has not been explicitly configured, then the hostnames will change to the new `ably.net` domain when the package is upgraded. [1] https://ably.atlassian.net/wiki/spaces/ENG/pages/3428810778/ADR-119+ClientOptions+for+new+DNS+structure [2] https://ably.atlassian.net/wiki/spaces/ENG/pages/1791754276/ADR-042+DNS+Restructure
1 parent 85a15a3 commit 842bd77

6 files changed

+513
-177
lines changed

ably/export_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ func GetEnvFallbackHosts(env string) []string {
1616
return getEnvFallbackHosts(env)
1717
}
1818

19+
func GetEndpointFallbackHosts(endpoint string) []string {
20+
return getEndpointFallbackHosts(endpoint)
21+
}
22+
23+
func (opts *clientOptions) GetEndpoint() string {
24+
return opts.getEndpoint()
25+
}
26+
1927
func (opts *clientOptions) GetRestHost() string {
2028
return opts.getRestHost()
2129
}
@@ -192,6 +200,10 @@ func ApplyOptionsWithDefaults(o ...ClientOption) *clientOptions {
192200
return applyOptionsWithDefaults(o...)
193201
}
194202

203+
func EndpointFqdn(endpoint string) bool {
204+
return endpointFqdn(endpoint)
205+
}
206+
195207
type ConnStateChanges = connStateChanges
196208

197209
type ChannelStateChanges = channelStateChanges

ably/options.go

+101-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/http/httptrace"
1313
"net/url"
1414
"os"
15+
"regexp"
1516
"strconv"
1617
"strings"
1718
"time"
@@ -23,6 +24,9 @@ const (
2324
protocolJSON = "application/json"
2425
protocolMsgPack = "application/x-msgpack"
2526

27+
// endpoint is the default routing policy used to connect to Ably
28+
endpoint = "main"
29+
2630
// restHost is the primary ably host.
2731
restHost = "rest.ably.io"
2832
// realtimeHost is the primary ably host.
@@ -37,6 +41,7 @@ const (
3741
)
3842

3943
var defaultOptions = clientOptions{
44+
Endpoint: endpoint,
4045
RESTHost: restHost,
4146
FallbackHosts: defaultFallbackHosts(),
4247
HTTPMaxRetryCount: 3,
@@ -59,13 +64,24 @@ var defaultOptions = clientOptions{
5964
}
6065

6166
func defaultFallbackHosts() []string {
62-
return []string{
63-
"a.ably-realtime.com",
64-
"b.ably-realtime.com",
65-
"c.ably-realtime.com",
66-
"d.ably-realtime.com",
67-
"e.ably-realtime.com",
67+
return endpointFallbacks("main", "ably-realtime.com")
68+
}
69+
70+
func getEndpointFallbackHosts(endpoint string) []string {
71+
if match := regexp.MustCompile(`^nonprod:(.*)$`).FindStringSubmatch(endpoint); match != nil {
72+
namespace := match[1]
73+
return endpointFallbacks(namespace, "ably-realtime-nonprod.com")
6874
}
75+
76+
return endpointFallbacks(endpoint, "ably-realtime.com")
77+
}
78+
79+
func endpointFallbacks(namespace, root string) []string {
80+
var fallbacks []string
81+
for _, id := range []string{"a", "b", "c", "d", "e"} {
82+
fallbacks = append(fallbacks, fmt.Sprintf("%s.%s.fallback.%s", namespace, id, root))
83+
}
84+
return fallbacks
6985
}
7086

7187
func getEnvFallbackHosts(env string) []string {
@@ -244,8 +260,17 @@ type clientOptions struct {
244260
// authOptions Embedded an [ably.authOptions] object (TO3j).
245261
authOptions
246262

247-
// RESTHost enables a non-default Ably host to be specified. For development environments only.
248-
// The default value is rest.ably.io (RSC12, TO3k2).
263+
// Endpoint specifies the domain used to connect to Ably. It has the following properties:
264+
// If the endpoint option is a production routing policy name of the form [name] then the primary domain is [name].realtime.ably.net (REC1b4).
265+
// If the endpoint option is a non-production routing policy name of the form nonprod:[name] then the primary domain is [name].realtime.ably-nonprod.net (REC1b3).
266+
// If the endpoint option is a domain name, determined by it containing at least one period (.) then the primary domain is the value of the endpoint option (REC1b2).
267+
// If any one of the deprecated options environment, restHost, realtimeHost are also specified then the options as a set are invalid (REC1b1).
268+
Endpoint string
269+
270+
// Deprecated: this property is deprecated and will be removed in a future version.
271+
// RESTHost enables a non-default Ably host to be specified. For
272+
// development environments only. The default value is rest.ably.io (RSC12,
273+
// TO3k2).
249274
RESTHost string
250275

251276
// Deprecated: this property is deprecated and will be removed in a future version.
@@ -257,10 +282,12 @@ type clientOptions struct {
257282
// please specify them here (RSC15b, RSC15a, TO3k6).
258283
FallbackHosts []string
259284

285+
// Deprecated: this property is deprecated and will be removed in a future version.
260286
// RealtimeHost enables a non-default Ably host to be specified for realtime connections.
261287
// For development environments only. The default value is realtime.ably.io (RTC1d, TO3k3).
262288
RealtimeHost string
263289

290+
// Deprecated: this property is deprecated and will be removed in a future version.
264291
// Environment enables a custom environment to be used with the Ably service.
265292
// Optional: prefixes both hostname with the environment string (RSC15b, TO3k1).
266293
Environment string
@@ -415,6 +442,13 @@ type clientOptions struct {
415442
}
416443

417444
func (opts *clientOptions) validate() error {
445+
if !empty(opts.Endpoint) && (!empty(opts.Environment) || !empty(opts.RealtimeHost) || !empty(opts.RESTHost)) {
446+
err := errors.New("invalid client option: cannot use endpoint with any of environment, realtimeHost or restHost")
447+
logger := opts.LogHandler
448+
logger.Printf(LogError, "Invalid client options : %v", err.Error())
449+
return err
450+
}
451+
418452
_, err := opts.getFallbackHosts()
419453
if err != nil {
420454
logger := opts.LogHandler
@@ -450,7 +484,39 @@ func (opts *clientOptions) activePort() (port int, isDefault bool) {
450484
return
451485
}
452486

487+
func (opts *clientOptions) usingLegacyOpts() bool {
488+
return empty(opts.Endpoint) && (!empty(opts.Environment) || !empty(opts.RESTHost) || !empty(opts.RealtimeHost))
489+
}
490+
491+
// endpointFqdn handles an endpoint that uses a hostname, which may be an IPv4
492+
// address, IPv6 address or localhost
493+
func endpointFqdn(endpoint string) bool {
494+
return strings.Contains(endpoint, ".") || strings.Contains(endpoint, "::") || endpoint == "localhost"
495+
}
496+
497+
func (opts *clientOptions) getEndpoint() string {
498+
endpoint := opts.Endpoint
499+
if empty(endpoint) {
500+
endpoint = defaultOptions.Endpoint
501+
}
502+
503+
if endpointFqdn(opts.Endpoint) {
504+
return endpoint
505+
}
506+
507+
if match := regexp.MustCompile(`^nonprod:(.*)$`).FindStringSubmatch(endpoint); match != nil {
508+
namespace := match[1]
509+
return fmt.Sprintf("%s.realtime.ably-nonprod.net", namespace)
510+
}
511+
512+
return fmt.Sprintf("%s.realtime.ably.net", endpoint)
513+
}
514+
453515
func (opts *clientOptions) getRestHost() string {
516+
if !opts.usingLegacyOpts() {
517+
return opts.getEndpoint()
518+
}
519+
454520
if !empty(opts.RESTHost) {
455521
return opts.RESTHost
456522
}
@@ -461,6 +527,10 @@ func (opts *clientOptions) getRestHost() string {
461527
}
462528

463529
func (opts *clientOptions) getRealtimeHost() string {
530+
if !opts.usingLegacyOpts() {
531+
return opts.getEndpoint()
532+
}
533+
464534
if !empty(opts.RealtimeHost) {
465535
return opts.RealtimeHost
466536
}
@@ -508,6 +578,7 @@ func (opts *clientOptions) realtimeURL(realtimeHost string) (realtimeUrl string)
508578
func (opts *clientOptions) getFallbackHosts() ([]string, error) {
509579
logger := opts.LogHandler
510580
_, isDefaultPort := opts.activePort()
581+
511582
if opts.FallbackHostsUseDefault {
512583
if opts.FallbackHosts != nil {
513584
return nil, errors.New("fallbackHosts and fallbackHostsUseDefault cannot both be set")
@@ -521,12 +592,21 @@ func (opts *clientOptions) getFallbackHosts() ([]string, error) {
521592
logger.Printf(LogWarning, "Deprecated fallbackHostsUseDefault : using default fallbackhosts")
522593
return defaultOptions.FallbackHosts, nil
523594
}
595+
596+
if opts.FallbackHosts == nil && !empty(opts.Endpoint) {
597+
if endpointFqdn(opts.Endpoint) {
598+
return opts.FallbackHosts, nil
599+
}
600+
return getEndpointFallbackHosts(opts.Endpoint), nil
601+
}
602+
524603
if opts.FallbackHosts == nil && empty(opts.RESTHost) && empty(opts.RealtimeHost) && isDefaultPort {
525604
if opts.isProductionEnvironment() {
526605
return defaultOptions.FallbackHosts, nil
527606
}
528607
return getEnvFallbackHosts(opts.Environment), nil
529608
}
609+
530610
return opts.FallbackHosts, nil
531611
}
532612

@@ -1070,6 +1150,18 @@ func WithEchoMessages(echo bool) ClientOption {
10701150
}
10711151
}
10721152

1153+
// WithEndpoint is used for setting Endpoint using [ably.ClientOption].
1154+
// Endpoint specifies the domain used to connect to Ably. It has the following properties:
1155+
// If the endpoint option is a production routing policy name of the form [name] then the primary domain is [name].realtime.ably.net (REC1b4).
1156+
// If the endpoint option is a non-production routing policy name of the form nonprod:[name] then the primary domain is [name].realtime.ably-nonprod.net (REC1b3).
1157+
// If the endpoint option is a domain name, determined by it containing at least one period (.) then the primary domain is the value of the endpoint option (REC1b2).
1158+
// If any one of the deprecated options environment, restHost, realtimeHost are also specified then the options as a set are invalid (REC1b1).
1159+
func WithEndpoint(env string) ClientOption {
1160+
return func(os *clientOptions) {
1161+
os.Endpoint = env
1162+
}
1163+
}
1164+
10731165
// WithEnvironment is used for setting Environment using [ably.ClientOption].
10741166
// Environment enables a custom environment to be used with the Ably service.
10751167
// Optional: prefixes both hostname with the environment string (RSC15b, TO3k1).
@@ -1331,6 +1423,7 @@ func WithInsecureAllowBasicAuthWithoutTLS() ClientOption {
13311423
func applyOptionsWithDefaults(opts ...ClientOption) *clientOptions {
13321424
to := defaultOptions
13331425
// No need to set hosts by default
1426+
to.Endpoint = ""
13341427
to.RESTHost = ""
13351428
to.RealtimeHost = ""
13361429
to.FallbackHosts = nil

0 commit comments

Comments
 (0)