Skip to content

Commit 761c421

Browse files
surminusttypic
authored andcommitted
feat: add endpoint option
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 7926339 commit 761c421

9 files changed

Lines changed: 153 additions & 67 deletions

File tree

ably/realtime/realtime.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,14 @@ def __init__(self, key: Optional[str] = None, loop: Optional[asyncio.AbstractEve
4848
You can set this to false and explicitly connect to Ably using the
4949
connect() method. The default is true.
5050
**kwargs: client options
51+
endpoint: str
52+
Endpoint specifies either a routing policy name or fully qualified domain name to connect to Ably.
5153
realtime_host: str
54+
Deprecated: this property is deprecated and will be removed in a future version.
5255
Enables a non-default Ably host to be specified for realtime connections.
5356
For development environments only. The default value is realtime.ably.io.
5457
environment: str
58+
Deprecated: this property is deprecated and will be removed in a future version.
5559
Enables a custom environment to be used with the Ably service. Defaults to `production`
5660
realtime_request_timeout: float
5761
Timeout (in milliseconds) for the wait of acknowledgement for operations performed via a realtime

ably/rest/rest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ def __init__(self, key: Optional[str] = None, token: Optional[str] = None,
3232
3333
**Optional Parameters**
3434
- `client_id`: Undocumented
35-
- `rest_host`: The host to connect to. Defaults to rest.ably.io
36-
- `environment`: The environment to use. Defaults to 'production'
35+
- `endpoint`: Endpoint specifies either a routing policy name or
36+
fully qualified domain name to connect to Ably.
37+
- `rest_host`: Deprecated: this property is deprecated and will
38+
be removed in a future version. The host to connect to.
39+
Defaults to rest.ably.io
40+
- `environment`: Deprecated: this property is deprecated and
41+
will be removed in a future version. The environment to use.
42+
Defaults to 'production'
3743
- `port`: The port to connect to. Defaults to 80
3844
- `tls_port`: The tls_port to connect to. Defaults to 443
3945
- `tls`: Specifies whether the client should use TLS. Defaults

ably/transport/defaults.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
11
class Defaults:
22
protocol_version = "2"
3-
fallback_hosts = [
4-
"a.ably-realtime.com",
5-
"b.ably-realtime.com",
6-
"c.ably-realtime.com",
7-
"d.ably-realtime.com",
8-
"e.ably-realtime.com",
9-
]
10-
11-
rest_host = "rest.ably.io"
12-
realtime_host = "realtime.ably.io" # RTN2
3+
134
connectivity_check_url = "https://internet-up.ably-realtime.com/is-the-internet-up.txt"
14-
environment = 'production'
5+
endpoint = 'main'
156

167
port = 80
178
tls_port = 443
@@ -53,11 +44,34 @@ def get_scheme(options):
5344
return "http"
5445

5546
@staticmethod
56-
def get_environment_fallback_hosts(environment):
47+
def get_hostname(endpoint):
48+
if "." in endpoint or "::" in endpoint or "localhost" in endpoint:
49+
return endpoint
50+
51+
if endpoint.startswith("nonprod:"):
52+
return endpoint[len("nonprod:"):] + ".realtime.ably-nonprod.net"
53+
54+
return endpoint + ".realtime.ably.net"
55+
56+
@staticmethod
57+
def get_fallback_hosts(endpoint="main"):
58+
if "." in endpoint or "::" in endpoint or "localhost" in endpoint:
59+
return []
60+
61+
if endpoint.startswith("nonprod:"):
62+
root = endpoint.replace("nonprod:", "")
63+
return [
64+
root + ".a.fallback.ably-realtime-nonprod.com",
65+
root + ".b.fallback.ably-realtime-nonprod.com",
66+
root + ".c.fallback.ably-realtime-nonprod.com",
67+
root + ".d.fallback.ably-realtime-nonprod.com",
68+
root + ".e.fallback.ably-realtime-nonprod.com",
69+
]
70+
5771
return [
58-
environment + "-a-fallback.ably-realtime.com",
59-
environment + "-b-fallback.ably-realtime.com",
60-
environment + "-c-fallback.ably-realtime.com",
61-
environment + "-d-fallback.ably-realtime.com",
62-
environment + "-e-fallback.ably-realtime.com",
72+
endpoint + ".a.fallback.ably-realtime.com",
73+
endpoint + ".b.fallback.ably-realtime.com",
74+
endpoint + ".c.fallback.ably-realtime.com",
75+
endpoint + ".d.fallback.ably-realtime.com",
76+
endpoint + ".e.fallback.ably-realtime.com",
6377
]

ably/types/options.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,21 @@ def decode(self, delta: bytes, base: bytes) -> bytes:
2626

2727
class Options(AuthOptions):
2828
def __init__(self, client_id=None, log_level=0, tls=True, rest_host=None, realtime_host=None, port=0,
29-
tls_port=0, use_binary_protocol=True, queue_messages=True, recover=False, environment=None,
30-
http_open_timeout=None, http_request_timeout=None, realtime_request_timeout=None,
31-
http_max_retry_count=None, http_max_retry_duration=None, fallback_hosts=None,
32-
fallback_retry_timeout=None, disconnected_retry_timeout=None, idempotent_rest_publishing=None,
33-
loop=None, auto_connect=True, suspended_retry_timeout=None, connectivity_check_url=None,
29+
tls_port=0, use_binary_protocol=True, queue_messages=True, recover=False, endpoint=None,
30+
environment=None, http_open_timeout=None, http_request_timeout=None,
31+
realtime_request_timeout=None, http_max_retry_count=None, http_max_retry_duration=None,
32+
fallback_hosts=None, fallback_retry_timeout=None, disconnected_retry_timeout=None,
33+
idempotent_rest_publishing=None, loop=None, auto_connect=True,
34+
suspended_retry_timeout=None, connectivity_check_url=None,
3435
channel_retry_timeout=Defaults.channel_retry_timeout, add_request_ids=False,
3536
vcdiff_decoder: VCDiffDecoder = None, transport_params=None, **kwargs):
3637

3738
super().__init__(**kwargs)
3839

40+
if endpoint is not None:
41+
if environment is not None or rest_host is not None or realtime_host is not None:
42+
raise ValueError('endpoint is incompatible with any of environment, rest_host or realtime_host')
43+
3944
# TODO check these defaults
4045
if fallback_retry_timeout is None:
4146
fallback_retry_timeout = Defaults.fallback_retry_timeout
@@ -64,8 +69,11 @@ def __init__(self, client_id=None, log_level=0, tls=True, rest_host=None, realti
6469
from ably import api_version
6570
idempotent_rest_publishing = api_version >= '1.2'
6671

67-
if environment is None:
68-
environment = Defaults.environment
72+
if environment is not None and endpoint is None:
73+
endpoint = environment
74+
75+
if endpoint is None:
76+
endpoint = Defaults.endpoint
6977

7078
self.__client_id = client_id
7179
self.__log_level = log_level
@@ -77,7 +85,7 @@ def __init__(self, client_id=None, log_level=0, tls=True, rest_host=None, realti
7785
self.__use_binary_protocol = use_binary_protocol
7886
self.__queue_messages = queue_messages
7987
self.__recover = recover
80-
self.__environment = environment
88+
self.__endpoint = endpoint
8189
self.__http_open_timeout = http_open_timeout
8290
self.__http_request_timeout = http_request_timeout
8391
self.__realtime_request_timeout = realtime_request_timeout
@@ -183,8 +191,8 @@ def recover(self, value):
183191
self.__recover = value
184192

185193
@property
186-
def environment(self):
187-
return self.__environment
194+
def endpoint(self):
195+
return self.__endpoint
188196

189197
@property
190198
def http_open_timeout(self):
@@ -296,27 +304,19 @@ def __get_rest_hosts(self):
296304
# Defaults
297305
host = self.rest_host
298306
if host is None:
299-
host = Defaults.rest_host
300-
301-
environment = self.environment
307+
host = Defaults.get_hostname(self.endpoint)
302308

303309
http_max_retry_count = self.http_max_retry_count
304310
if http_max_retry_count is None:
305311
http_max_retry_count = Defaults.http_max_retry_count
306312

307-
# Prepend environment
308-
if environment != 'production':
309-
host = f'{environment}-{host}'
310-
311313
# Fallback hosts
312314
fallback_hosts = self.fallback_hosts
313315
if fallback_hosts is None:
314-
if host == Defaults.rest_host:
315-
fallback_hosts = Defaults.fallback_hosts
316-
elif environment != 'production':
317-
fallback_hosts = Defaults.get_environment_fallback_hosts(environment)
318-
else:
316+
if self.rest_host is not None:
319317
fallback_hosts = []
318+
else:
319+
fallback_hosts = Defaults.get_fallback_hosts(self.endpoint)
320320

321321
# Shuffle
322322
fallback_hosts = list(fallback_hosts)
@@ -332,11 +332,8 @@ def __get_realtime_hosts(self):
332332
if self.realtime_host is not None:
333333
host = self.realtime_host
334334
return [host]
335-
elif self.environment != "production":
336-
host = f'{self.environment}-{Defaults.realtime_host}'
337-
else:
338-
host = Defaults.realtime_host
339335

336+
host = Defaults.get_hostname(self.endpoint)
340337
return [host] + self.__fallback_hosts
341338

342339
def get_rest_hosts(self):

test/ably/rest/restinit_test.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ def test_rest_host_and_environment(self):
7373
ably = AblyRest(token='foo', rest_host="some.other.host")
7474
assert "some.other.host" == ably.options.rest_host, "Unexpected host mismatch"
7575

76-
# environment: production
77-
ably = AblyRest(token='foo', environment="production")
76+
# environment: main
77+
ably = AblyRest(token='foo', environment="main")
7878
host = ably.options.get_rest_host()
79-
assert "rest.ably.io" == host, f"Unexpected host mismatch {host}"
79+
assert "main.realtime.ably.net" == host, f"Unexpected host mismatch {host}"
8080

8181
# environment: other
82-
ably = AblyRest(token='foo', environment="sandbox")
82+
ably = AblyRest(token='foo', environment="nonprod:sandbox")
8383
host = ably.options.get_rest_host()
84-
assert "sandbox-rest.ably.io" == host, f"Unexpected host mismatch {host}"
84+
assert "sandbox.realtime.ably-nonprod.net" == host, f"Unexpected host mismatch {host}"
8585

8686
# both, as per #TO3k2
8787
with pytest.raises(ValueError):
@@ -103,13 +103,13 @@ def test_fallback_hosts(self):
103103
assert sorted(aux) == sorted(ably.options.get_fallback_rest_hosts())
104104

105105
# Specify environment (RSC15g2)
106-
ably = AblyRest(token='foo', environment='sandbox', http_max_retry_count=10)
107-
assert sorted(Defaults.get_environment_fallback_hosts('sandbox')) == sorted(
106+
ably = AblyRest(token='foo', environment='nonprod:sandbox', http_max_retry_count=10)
107+
assert sorted(Defaults.get_fallback_hosts('nonprod:sandbox')) == sorted(
108108
ably.options.get_fallback_rest_hosts())
109109

110110
# Fallback hosts and environment not specified (RSC15g3)
111111
ably = AblyRest(token='foo', http_max_retry_count=10)
112-
assert sorted(Defaults.fallback_hosts) == sorted(ably.options.get_fallback_rest_hosts())
112+
assert sorted(Defaults.get_fallback_hosts()) == sorted(ably.options.get_fallback_rest_hosts())
113113

114114
# RSC15f
115115
ably = AblyRest(token='foo')
@@ -182,13 +182,17 @@ async def test_query_time_param(self):
182182
@dont_vary_protocol
183183
def test_requests_over_https_production(self):
184184
ably = AblyRest(token='token')
185-
assert 'https://rest.ably.io' == f'{ably.http.preferred_scheme}://{ably.http.preferred_host}'
185+
assert 'https://main.realtime.ably.net' == f'{
186+
ably.http.preferred_scheme}://{ ably.http.preferred_host
187+
}'
186188
assert ably.http.preferred_port == 443
187189

188190
@dont_vary_protocol
189191
def test_requests_over_http_production(self):
190192
ably = AblyRest(token='token', tls=False)
191-
assert 'http://rest.ably.io' == f'{ably.http.preferred_scheme}://{ably.http.preferred_host}'
193+
assert 'http://main.realtime.ably.net' == f'{
194+
ably.http.preferred_scheme}://{ ably.http.preferred_host
195+
}'
192196
assert ably.http.preferred_port == 80
193197

194198
@dont_vary_protocol
@@ -211,7 +215,7 @@ async def test_environment(self):
211215
except AblyException:
212216
pass
213217
request = get_mock.call_args_list[0][0][0]
214-
assert request.url == 'https://custom-rest.ably.io:443/time'
218+
assert request.url == 'https://custom.realtime.ably.net:443/time'
215219

216220
await ably.close()
217221

test/ably/rest/restpaginatedresult_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async def setup(self):
3232
self.ably = await TestApp.get_ably_rest(use_binary_protocol=False)
3333
# Mocked responses
3434
# without specific headers
35-
self.mocked_api = respx.mock(base_url='http://rest.ably.io')
35+
self.mocked_api = respx.mock(base_url='http://main.realtime.ably.net')
3636
self.ch1_route = self.mocked_api.get('/channels/channel_name/ch1')
3737
self.ch1_route.return_value = Response(
3838
headers={'content-type': 'application/json'},
@@ -45,8 +45,8 @@ async def setup(self):
4545
headers={
4646
'content-type': 'application/json',
4747
'link':
48-
'<http://rest.ably.io/channels/channel_name/ch2?page=1>; rel="first",'
49-
' <http://rest.ably.io/channels/channel_name/ch2?page=2>; rel="next"'
48+
'<http://main.realtime.ably.net/channels/channel_name/ch2?page=1>; rel="first",'
49+
' <http://main.realtime.ably.net/channels/channel_name/ch2?page=2>; rel="next"'
5050
},
5151
body='[{"id": 0}, {"id": 1}]',
5252
status=200
@@ -56,11 +56,11 @@ async def setup(self):
5656

5757
self.paginated_result = await PaginatedResult.paginated_query(
5858
self.ably.http,
59-
url='http://rest.ably.io/channels/channel_name/ch1',
59+
url='http://main.realtime.ably.net/channels/channel_name/ch1',
6060
response_processor=lambda response: response.to_native())
6161
self.paginated_result_with_headers = await PaginatedResult.paginated_query(
6262
self.ably.http,
63-
url='http://rest.ably.io/channels/channel_name/ch2',
63+
url='http://main.realtime.ably.net/channels/channel_name/ch2',
6464
response_processor=lambda response: response.to_native())
6565
yield
6666
self.mocked_api.stop()

test/ably/rest/restrequest_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ async def test_timeout(self):
100100
await ably.request('GET', '/time', version=Defaults.protocol_version)
101101
await ably.close()
102102

103-
default_endpoint = 'https://sandbox-rest.ably.io/time'
104-
fallback_host = 'sandbox-a-fallback.ably-realtime.com'
103+
default_endpoint = 'https://sandbox.realtime.ably-nonprod.net/time'
104+
fallback_host = 'sandbox.a.fallback.ably-realtime-nonprod.com'
105105
fallback_endpoint = f'https://{fallback_host}/time'
106106
ably = await TestApp.get_ably_rest(fallback_hosts=[fallback_host])
107107
with respx.mock:

test/ably/testapp.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
app_spec_local = json.loads(f.read())
1515

1616
tls = (os.environ.get('ABLY_TLS') or "true").lower() == "true"
17-
rest_host = os.environ.get('ABLY_REST_HOST', 'sandbox-rest.ably.io')
18-
realtime_host = os.environ.get('ABLY_REALTIME_HOST', 'sandbox-realtime.ably.io')
17+
rest_host = os.environ.get('ABLY_REST_HOST', 'sandbox.realtime.ably-nonprod.net')
18+
realtime_host = os.environ.get('ABLY_REALTIME_HOST', 'sandbox.realtime.ably-nonprod.net')
1919

20-
environment = os.environ.get('ABLY_ENV', 'sandbox')
20+
environment = os.environ.get('ABLY_ENDPOINT', 'nonprod:sandbox')
2121

2222
port = 80
2323
tls_port = 443
2424

25-
if rest_host and not rest_host.endswith("rest.ably.io"):
25+
if rest_host and not rest_host.endswith("realtime.ably-nonprod.net"):
2626
tls = tls and rest_host != "localhost"
2727
port = 8080
2828
tls_port = 8081

test/unit/options_test.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
3+
from ably.types.options import Options
4+
5+
6+
def test_options_should_fail_early_with_incompatible_client_options():
7+
with pytest.raises(ValueError):
8+
Options(endpoint="foo", environment="foo")
9+
10+
with pytest.raises(ValueError):
11+
Options(endpoint="foo", rest_host="foo")
12+
13+
with pytest.raises(ValueError):
14+
Options(endpoint="foo", realtime_host="foo")
15+
16+
17+
# REC1a
18+
def test_options_should_return_the_default_hostnames():
19+
opts = Options()
20+
assert opts.get_realtime_host() == "main.realtime.ably.net"
21+
assert "main.a.fallback.ably-realtime.com" in opts.get_fallback_realtime_hosts()
22+
23+
24+
# REC1b4
25+
def test_options_should_return_the_correct_routing_policy_hostnames():
26+
opts = Options(endpoint="foo")
27+
assert opts.get_realtime_host() == "foo.realtime.ably.net"
28+
assert "foo.a.fallback.ably-realtime.com" in opts.get_fallback_realtime_hosts()
29+
30+
31+
# REC1b3
32+
def test_options_should_return_the_correct_nonprod_routing_policy_hostnames():
33+
opts = Options(endpoint="nonprod:foo")
34+
assert opts.get_realtime_host() == "foo.realtime.ably-nonprod.net"
35+
assert "foo.a.fallback.ably-realtime-nonprod.com" in opts.get_fallback_realtime_hosts()
36+
37+
38+
# REC1b2
39+
def test_options_should_return_the_correct_fqdn_hostnames():
40+
opts = Options(endpoint="foo.com")
41+
assert opts.get_realtime_host() == "foo.com"
42+
assert not opts.get_fallback_realtime_hosts()
43+
44+
45+
# REC1b2
46+
def test_options_should_return_an_ipv4_address():
47+
opts = Options(endpoint="127.0.0.1")
48+
assert opts.get_realtime_host() == "127.0.0.1"
49+
assert not opts.get_fallback_realtime_hosts()
50+
51+
52+
# REC1b2
53+
def test_options_should_return_an_ipv6_address():
54+
opts = Options(endpoint="::1")
55+
assert opts.get_realtime_host() == "::1"
56+
57+
58+
# REC1b2
59+
def test_options_should_return_localhost():
60+
opts = Options(endpoint="localhost")
61+
assert opts.get_realtime_host() == "localhost"

0 commit comments

Comments
 (0)