Skip to content

Commit ce58fa3

Browse files
authored
Merge pull request #679 from ably/feature/endpoint-option
[ClientOption] Add support for endpoint according to new routing policy
2 parents 85a15a3 + ffd50e8 commit ce58fa3

14 files changed

+591
-277
lines changed

ably/ably_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ func NewRecorder(httpClient *http.Client) *HostRecorder {
356356

357357
func (hr *HostRecorder) Options(host string, opts ...ably.ClientOption) []ably.ClientOption {
358358
return append(opts,
359-
ably.WithRealtimeHost(host),
359+
ably.WithEndpoint(host),
360360
ably.WithAutoConnect(false),
361361
ably.WithDial(hr.dialWS),
362362
ably.WithHTTPClient(hr.httpClient),

ably/auth_integration_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) {
389389
rec, optn := ablytest.NewHttpRecorder()
390390
rest, err := ably.NewREST(
391391
ably.WithToken(jwt),
392-
ably.WithEnvironment(app.Environment),
392+
ably.WithEndpoint(app.Endpoint),
393393
optn[0],
394394
)
395395
assert.NoError(t, err, "rest()=%v", err)
@@ -414,7 +414,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) {
414414
rest, err := ably.NewREST(
415415
ably.WithAuthURL(ablytest.CREATE_JWT_URL),
416416
ably.WithAuthParams(app.GetJwtAuthParams(30*time.Second, false)),
417-
ably.WithEnvironment(app.Environment),
417+
ably.WithEndpoint(app.Endpoint),
418418
optn[0],
419419
)
420420
assert.NoError(t, err, "rest()=%v", err)
@@ -457,7 +457,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) {
457457

458458
rec, optn := ablytest.NewHttpRecorder()
459459
rest, err := ably.NewREST(
460-
ably.WithEnvironment(app.Environment),
460+
ably.WithEndpoint(app.Endpoint),
461461
authCallback,
462462
optn[0],
463463
)
@@ -485,7 +485,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) {
485485
rest, err := ably.NewREST(
486486
ably.WithAuthURL(ablytest.CREATE_JWT_URL),
487487
ably.WithAuthParams(app.GetJwtAuthParams(30*time.Second, true)),
488-
ably.WithEnvironment(app.Environment),
488+
ably.WithEndpoint(app.Endpoint),
489489
optn[0],
490490
)
491491
assert.NoError(t, err, "rest()=%v", err)

ably/error_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func TestIssue127ErrorResponse(t *testing.T) {
5454
ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"),
5555
ably.WithTLS(false),
5656
ably.WithUseTokenAuth(true),
57-
ably.WithRESTHost(endpointURL.Hostname()),
57+
ably.WithEndpoint(endpointURL.Hostname()),
5858
}
5959
port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
6060
opts = append(opts, ably.WithPort(int(port)))
@@ -134,7 +134,7 @@ func TestIssue_154(t *testing.T) {
134134
ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"),
135135
ably.WithTLS(false),
136136
ably.WithUseTokenAuth(true),
137-
ably.WithRESTHost(endpointURL.Hostname()),
137+
ably.WithEndpoint(endpointURL.Hostname()),
138138
}
139139
port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
140140
opts = append(opts, ably.WithPort(int(port)))

ably/export_test.go

+15-3
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ func NewClientOptions(os ...ClientOption) *clientOptions {
1212
return applyOptionsWithDefaults(os...)
1313
}
1414

15-
func GetEnvFallbackHosts(env string) []string {
16-
return getEnvFallbackHosts(env)
15+
func GetEndpointFallbackHosts(endpoint string) []string {
16+
return getEndpointFallbackHosts(endpoint)
1717
}
1818

1919
func (opts *clientOptions) GetRestHost() string {
@@ -24,6 +24,14 @@ func (opts *clientOptions) GetRealtimeHost() string {
2424
return opts.getRealtimeHost()
2525
}
2626

27+
func (opts *clientOptions) Validate() error {
28+
return opts.validate()
29+
}
30+
31+
func (opts *clientOptions) GetHostnameFromEndpoint() string {
32+
return opts.getHostnameFromEndpoint()
33+
}
34+
2735
func (opts *clientOptions) ActivePort() (int, bool) {
2836
return opts.activePort()
2937
}
@@ -192,14 +200,18 @@ func ApplyOptionsWithDefaults(o ...ClientOption) *clientOptions {
192200
return applyOptionsWithDefaults(o...)
193201
}
194202

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

197209
type ChannelStateChanges = channelStateChanges
198210

199211
const ConnectionStateTTLErrFmt = connectionStateTTLErrFmt
200212

201213
func DefaultFallbackHosts() []string {
202-
return defaultFallbackHosts()
214+
return defaultOptions.FallbackHosts
203215
}
204216

205217
// PendingItems returns the number of messages waiting for Ack/Nack

ably/http_paginated_response_integration_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestHTTPPaginatedFallback(t *testing.T) {
2121
assert.NoError(t, err)
2222
defer app.Close()
2323
opts := app.Options(ably.WithUseBinaryProtocol(false),
24-
ably.WithRESTHost("ably.invalid"),
24+
ably.WithEndpoint("ably.invalid"),
2525
ably.WithFallbackHosts(nil))
2626
client, err := ably.NewREST(opts...)
2727
assert.NoError(t, err)

ably/options.go

+109-32
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ const (
2323
protocolJSON = "application/json"
2424
protocolMsgPack = "application/x-msgpack"
2525

26-
// restHost is the primary ably host.
27-
restHost = "rest.ably.io"
28-
// realtimeHost is the primary ably host.
29-
realtimeHost = "realtime.ably.io"
26+
// defaultEndpoint is the default routing policy used to connect to Ably
27+
defaultEndpoint = "main"
28+
defaultPrimaryHost = "main.realtime.ably.net" // REC1a
29+
3030
Port = 80
3131
TLSPort = 443
3232
maxMessageSize = 65536 // 64kb, default value TO3l8
@@ -37,11 +37,12 @@ const (
3737
)
3838

3939
var defaultOptions = clientOptions{
40-
RESTHost: restHost,
41-
FallbackHosts: defaultFallbackHosts(),
40+
Endpoint: defaultEndpoint,
41+
RESTHost: defaultPrimaryHost,
42+
FallbackHosts: getEndpointFallbackHosts(defaultEndpoint), // REC2c1
4243
HTTPMaxRetryCount: 3,
4344
HTTPRequestTimeout: 10 * time.Second,
44-
RealtimeHost: realtimeHost,
45+
RealtimeHost: defaultPrimaryHost,
4546
TimeoutDisconnect: 30 * time.Second,
4647
ConnectionStateTTL: 120 * time.Second,
4748
RealtimeRequestTimeout: 10 * time.Second, // DF1b
@@ -58,23 +59,31 @@ var defaultOptions = clientOptions{
5859
LogLevel: LogWarning, // RSC2
5960
}
6061

61-
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",
62+
func getPrimaryHost(root string) string {
63+
// REC1b3
64+
if strings.HasPrefix(root, "nonprod:") {
65+
root := strings.TrimPrefix(root, "nonprod:")
66+
return fmt.Sprintf("%s.realtime.ably-nonprod.net", root)
67+
}
68+
return fmt.Sprintf("%s.realtime.ably.net", root)
69+
}
70+
71+
func getEndpointFallbackHosts(endpoint string) []string {
72+
if strings.HasPrefix(endpoint, "nonprod:") { // REC2c3
73+
root := strings.TrimPrefix(endpoint, "nonprod:")
74+
return endpointFallbacks(root, "ably-realtime-nonprod.com")
6875
}
76+
return endpointFallbacks(endpoint, "ably-realtime.com") // REC2c4
6977
}
7078

71-
func getEnvFallbackHosts(env string) []string {
79+
// endpointFallbacks generates a list of fallback hosts based on the given namespace and root.
80+
func endpointFallbacks(root, domain string) []string {
7281
return []string{
73-
fmt.Sprintf("%s-%s", env, "a-fallback.ably-realtime.com"),
74-
fmt.Sprintf("%s-%s", env, "b-fallback.ably-realtime.com"),
75-
fmt.Sprintf("%s-%s", env, "c-fallback.ably-realtime.com"),
76-
fmt.Sprintf("%s-%s", env, "d-fallback.ably-realtime.com"),
77-
fmt.Sprintf("%s-%s", env, "e-fallback.ably-realtime.com"),
82+
fmt.Sprintf("%s.a.fallback.%s", root, domain),
83+
fmt.Sprintf("%s.b.fallback.%s", root, domain),
84+
fmt.Sprintf("%s.c.fallback.%s", root, domain),
85+
fmt.Sprintf("%s.d.fallback.%s", root, domain),
86+
fmt.Sprintf("%s.e.fallback.%s", root, domain),
7887
}
7988
}
8089

@@ -244,8 +253,11 @@ type clientOptions struct {
244253
// authOptions Embedded an [ably.authOptions] object (TO3j).
245254
authOptions
246255

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).
256+
// Endpoint specifies either a routing policy name or fully qualified domain name to connect to Ably.
257+
Endpoint string
258+
259+
// Deprecated: this property is deprecated and will be removed in a future version.
260+
// If the restHost option is specified the primary domain is the value of the restHost option REC1d1).
249261
RESTHost string
250262

251263
// Deprecated: this property is deprecated and will be removed in a future version.
@@ -257,12 +269,14 @@ type clientOptions struct {
257269
// please specify them here (RSC15b, RSC15a, TO3k6).
258270
FallbackHosts []string
259271

260-
// RealtimeHost enables a non-default Ably host to be specified for realtime connections.
261-
// For development environments only. The default value is realtime.ably.io (RTC1d, TO3k3).
272+
// Deprecated: this property is deprecated and will be removed in a future version.
273+
// If the realtimeHost option is specified the primary domain is the value of the realtimeHost option (REC1d2).
262274
RealtimeHost string
263275

264-
// Environment enables a custom environment to be used with the Ably service.
265-
// Optional: prefixes both hostname with the environment string (RSC15b, TO3k1).
276+
// Deprecated: this property is deprecated and will be removed in a future version.
277+
// If the deprecated environment option is specified then it defines a production routing policy name [name] (REC1c):
278+
// If any one of the deprecated options restHost, realtimeHost are also specified then the options as a set are invalid (REC1c1).
279+
// Otherwise, the primary domain is [name].realtime.ably.net (REC1c2).
266280
Environment string
267281

268282
// Port is used for non-TLS connections and requests
@@ -415,6 +429,14 @@ type clientOptions struct {
415429
}
416430

417431
func (opts *clientOptions) validate() error {
432+
// REC1b1
433+
if !empty(opts.Endpoint) && (!empty(opts.Environment) || !empty(opts.RealtimeHost) || !empty(opts.RESTHost) || opts.FallbackHostsUseDefault) {
434+
err := errors.New("invalid client option: cannot use endpoint with any of deprecated options environment, realtimeHost, restHost or FallbackHostsUseDefault")
435+
logger := opts.LogHandler
436+
logger.Printf(LogError, "Invalid client options : %v", err.Error())
437+
return err
438+
}
439+
418440
_, err := opts.getFallbackHosts()
419441
if err != nil {
420442
logger := opts.LogHandler
@@ -451,16 +473,22 @@ func (opts *clientOptions) activePort() (port int, isDefault bool) {
451473
}
452474

453475
func (opts *clientOptions) getRestHost() string {
476+
if !empty(opts.Endpoint) {
477+
return opts.getHostnameFromEndpoint()
478+
}
454479
if !empty(opts.RESTHost) {
455480
return opts.RESTHost
456481
}
457482
if !opts.isProductionEnvironment() {
458-
return opts.Environment + "-" + defaultOptions.RESTHost
483+
return getPrimaryHost(opts.Environment)
459484
}
460485
return defaultOptions.RESTHost
461486
}
462487

463488
func (opts *clientOptions) getRealtimeHost() string {
489+
if !empty(opts.Endpoint) {
490+
return opts.getHostnameFromEndpoint()
491+
}
464492
if !empty(opts.RealtimeHost) {
465493
return opts.RealtimeHost
466494
}
@@ -470,11 +498,29 @@ func (opts *clientOptions) getRealtimeHost() string {
470498
return opts.RESTHost
471499
}
472500
if !opts.isProductionEnvironment() {
473-
return opts.Environment + "-" + defaultOptions.RealtimeHost
501+
return getPrimaryHost(opts.Environment)
474502
}
475503
return defaultOptions.RealtimeHost
476504
}
477505

506+
// isEndpointFQDN returns true if the given endpoint is a hostname, which may
507+
// be an IPv4 address, IPv6 address or localhost
508+
func isEndpointFQDN(endpoint string) bool {
509+
return strings.Contains(endpoint, ".") || strings.Contains(endpoint, "::") || endpoint == "localhost"
510+
}
511+
512+
// REC1b
513+
func (opts *clientOptions) getHostnameFromEndpoint() string {
514+
endpoint := opts.Endpoint
515+
if empty(endpoint) {
516+
return defaultPrimaryHost
517+
}
518+
if isEndpointFQDN(endpoint) { // REC1b2
519+
return endpoint
520+
}
521+
return getPrimaryHost(endpoint) // REC1b4
522+
}
523+
478524
func empty(s string) bool {
479525
return len(strings.TrimSpace(s)) == 0
480526
}
@@ -506,6 +552,16 @@ func (opts *clientOptions) realtimeURL(realtimeHost string) (realtimeUrl string)
506552
}
507553

508554
func (opts *clientOptions) getFallbackHosts() ([]string, error) {
555+
if !empty(opts.Endpoint) {
556+
if opts.FallbackHosts == nil {
557+
if isEndpointFQDN(opts.Endpoint) { // REC2c2
558+
return opts.FallbackHosts, nil
559+
}
560+
return getEndpointFallbackHosts(opts.Endpoint), nil
561+
}
562+
return opts.FallbackHosts, nil //REC2a2
563+
}
564+
509565
logger := opts.LogHandler
510566
_, isDefaultPort := opts.activePort()
511567
if opts.FallbackHostsUseDefault {
@@ -525,7 +581,7 @@ func (opts *clientOptions) getFallbackHosts() ([]string, error) {
525581
if opts.isProductionEnvironment() {
526582
return defaultOptions.FallbackHosts, nil
527583
}
528-
return getEnvFallbackHosts(opts.Environment), nil
584+
return getEndpointFallbackHosts(opts.Environment), nil // REC2c5
529585
}
530586
return opts.FallbackHosts, nil
531587
}
@@ -1070,9 +1126,23 @@ func WithEchoMessages(echo bool) ClientOption {
10701126
}
10711127
}
10721128

1073-
// WithEnvironment is used for setting Environment using [ably.ClientOption].
1074-
// Environment enables a custom environment to be used with the Ably service.
1075-
// Optional: prefixes both hostname with the environment string (RSC15b, TO3k1).
1129+
// WithEndpoint sets a custom endpoint for connecting to the Ably service (see
1130+
// [Platform Customization] for more information).
1131+
//
1132+
// [Platform Customization]: https://ably.com/docs/platform-customization
1133+
func WithEndpoint(env string) ClientOption {
1134+
return func(os *clientOptions) {
1135+
os.Endpoint = env
1136+
}
1137+
}
1138+
1139+
// WithEnvironment sets a custom endpoint for connecting to the Ably service
1140+
// (see [Platform Customization] for more information).
1141+
//
1142+
// Deprecated: this option is deprecated and will be removed in a future
1143+
// version.
1144+
//
1145+
// [Platform Customization]: https://ably.com/docs/platform-customization
10761146
func WithEnvironment(env string) ClientOption {
10771147
return func(os *clientOptions) {
10781148
os.Environment = env
@@ -1130,6 +1200,9 @@ func WithQueueMessages(queue bool) ClientOption {
11301200
// WithRESTHost is used for setting RESTHost using [ably.ClientOption].
11311201
// RESTHost enables a non-default Ably host to be specified. For development environments only.
11321202
// The default value is rest.ably.io (RSC12, TO3k2).
1203+
//
1204+
// Deprecated: this option is deprecated and will be removed in a future
1205+
// version.
11331206
func WithRESTHost(host string) ClientOption {
11341207
return func(os *clientOptions) {
11351208
os.RESTHost = host
@@ -1149,6 +1222,9 @@ func WithHTTPRequestTimeout(timeout time.Duration) ClientOption {
11491222
// WithRealtimeHost is used for setting RealtimeHost using [ably.ClientOption].
11501223
// RealtimeHost enables a non-default Ably host to be specified for realtime connections.
11511224
// For development environments only. The default value is realtime.ably.io (RTC1d, TO3k3).
1225+
//
1226+
// Deprecated: this option is deprecated and will be removed in a future
1227+
// version.
11521228
func WithRealtimeHost(host string) ClientOption {
11531229
return func(os *clientOptions) {
11541230
os.RealtimeHost = host
@@ -1331,6 +1407,7 @@ func WithInsecureAllowBasicAuthWithoutTLS() ClientOption {
13311407
func applyOptionsWithDefaults(opts ...ClientOption) *clientOptions {
13321408
to := defaultOptions
13331409
// No need to set hosts by default
1410+
to.Endpoint = ""
13341411
to.RESTHost = ""
13351412
to.RealtimeHost = ""
13361413
to.FallbackHosts = nil

0 commit comments

Comments
 (0)