Skip to content

Commit 9535e5e

Browse files
sagnghosrahul2393
andauthored
feat: add support for experimental host (#1452)
* feat: add support for experimental host * fix lint issues * fixed unit tests * added docmentation for new client option --------- Co-authored-by: rahul2393 <irahul@google.com>
1 parent d51a7a8 commit 9535e5e

File tree

14 files changed

+128
-13
lines changed

14 files changed

+128
-13
lines changed

google/cloud/spanner_dbapi/connection.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""DB-API Connection for the Google Cloud Spanner."""
1616
import warnings
1717

18+
from google.api_core.client_options import ClientOptions
1819
from google.api_core.exceptions import Aborted
1920
from google.api_core.gapic_v1.client_info import ClientInfo
2021
from google.auth.credentials import AnonymousCredentials
@@ -734,6 +735,7 @@ def connect(
734735
client=None,
735736
route_to_leader_enabled=True,
736737
database_role=None,
738+
experimental_host=None,
737739
**kwargs,
738740
):
739741
"""Creates a connection to a Google Cloud Spanner database.
@@ -805,6 +807,10 @@ def connect(
805807
client_options = None
806808
if isinstance(credentials, AnonymousCredentials):
807809
client_options = kwargs.get("client_options")
810+
if experimental_host is not None:
811+
project = "default"
812+
credentials = AnonymousCredentials()
813+
client_options = ClientOptions(api_endpoint=experimental_host)
808814
client = spanner.Client(
809815
project=project,
810816
credentials=credentials,

google/cloud/spanner_v1/client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ class Client(ClientWithProject):
176176
or :class:`dict`
177177
:param default_transaction_options: (Optional) Default options to use for all transactions.
178178
179+
:type experimental_host: str
180+
:param experimental_host: (Optional) The endpoint for a spanner experimental host deployment.
181+
This is intended only for experimental host spanner endpoints.
182+
If set, this will override the `api_endpoint` in `client_options`.
183+
179184
:raises: :class:`ValueError <exceptions.ValueError>` if both ``read_only``
180185
and ``admin`` are :data:`True`
181186
"""
@@ -200,8 +205,10 @@ def __init__(
200205
directed_read_options=None,
201206
observability_options=None,
202207
default_transaction_options: Optional[DefaultTransactionOptions] = None,
208+
experimental_host=None,
203209
):
204210
self._emulator_host = _get_spanner_emulator_host()
211+
self._experimental_host = experimental_host
205212

206213
if client_options and type(client_options) is dict:
207214
self._client_options = google.api_core.client_options.from_dict(
@@ -212,6 +219,8 @@ def __init__(
212219

213220
if self._emulator_host:
214221
credentials = AnonymousCredentials()
222+
elif self._experimental_host:
223+
credentials = AnonymousCredentials()
215224
elif isinstance(credentials, AnonymousCredentials):
216225
self._emulator_host = self._client_options.api_endpoint
217226

@@ -324,6 +333,15 @@ def instance_admin_api(self):
324333
client_options=self._client_options,
325334
transport=transport,
326335
)
336+
elif self._experimental_host:
337+
transport = InstanceAdminGrpcTransport(
338+
channel=grpc.insecure_channel(target=self._experimental_host)
339+
)
340+
self._instance_admin_api = InstanceAdminClient(
341+
client_info=self._client_info,
342+
client_options=self._client_options,
343+
transport=transport,
344+
)
327345
else:
328346
self._instance_admin_api = InstanceAdminClient(
329347
credentials=self.credentials,
@@ -345,6 +363,15 @@ def database_admin_api(self):
345363
client_options=self._client_options,
346364
transport=transport,
347365
)
366+
elif self._experimental_host:
367+
transport = DatabaseAdminGrpcTransport(
368+
channel=grpc.insecure_channel(target=self._experimental_host)
369+
)
370+
self._database_admin_api = DatabaseAdminClient(
371+
client_info=self._client_info,
372+
client_options=self._client_options,
373+
transport=transport,
374+
)
348375
else:
349376
self._database_admin_api = DatabaseAdminClient(
350377
credentials=self.credentials,
@@ -485,6 +512,7 @@ def instance(
485512
self._emulator_host,
486513
labels,
487514
processing_units,
515+
self._experimental_host,
488516
)
489517

490518
def list_instances(self, filter_="", page_size=None):

google/cloud/spanner_v1/database.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,11 @@ def __init__(
203203

204204
self._pool = pool
205205
pool.bind(self)
206+
is_experimental_host = self._instance.experimental_host is not None
206207

207-
self._sessions_manager = DatabaseSessionsManager(self, pool)
208+
self._sessions_manager = DatabaseSessionsManager(
209+
self, pool, is_experimental_host
210+
)
208211

209212
@classmethod
210213
def from_pb(cls, database_pb, instance, pool=None):
@@ -449,6 +452,16 @@ def spanner_api(self):
449452
client_info=client_info, transport=transport
450453
)
451454
return self._spanner_api
455+
if self._instance.experimental_host is not None:
456+
transport = SpannerGrpcTransport(
457+
channel=grpc.insecure_channel(self._instance.experimental_host)
458+
)
459+
self._spanner_api = SpannerClient(
460+
client_info=client_info,
461+
transport=transport,
462+
client_options=client_options,
463+
)
464+
return self._spanner_api
452465
credentials = self._instance._client.credentials
453466
if isinstance(credentials, google.auth.credentials.Scoped):
454467
credentials = credentials.with_scopes((SPANNER_DATA_SCOPE,))

google/cloud/spanner_v1/database_sessions_manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ class DatabaseSessionsManager(object):
6262
_MAINTENANCE_THREAD_POLLING_INTERVAL = timedelta(minutes=10)
6363
_MAINTENANCE_THREAD_REFRESH_INTERVAL = timedelta(days=7)
6464

65-
def __init__(self, database, pool):
65+
def __init__(self, database, pool, is_experimental_host: bool = False):
6666
self._database = database
6767
self._pool = pool
68+
self._is_experimental_host = is_experimental_host
6869

6970
# Declare multiplexed session attributes. When a multiplexed session for the
7071
# database session manager is created, a maintenance thread is initialized to
@@ -88,7 +89,7 @@ def get_session(self, transaction_type: TransactionType) -> Session:
8889

8990
session = (
9091
self._get_multiplexed_session()
91-
if self._use_multiplexed(transaction_type)
92+
if self._use_multiplexed(transaction_type) or self._is_experimental_host
9293
else self._pool.get()
9394
)
9495

google/cloud/spanner_v1/instance.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def __init__(
122122
emulator_host=None,
123123
labels=None,
124124
processing_units=None,
125+
experimental_host=None,
125126
):
126127
self.instance_id = instance_id
127128
self._client = client
@@ -142,6 +143,7 @@ def __init__(
142143
self._node_count = processing_units // PROCESSING_UNITS_PER_NODE
143144
self.display_name = display_name or instance_id
144145
self.emulator_host = emulator_host
146+
self.experimental_host = experimental_host
145147
if labels is None:
146148
labels = {}
147149
self.labels = labels

google/cloud/spanner_v1/testing/database_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ def spanner_api(self):
8686
transport=transport,
8787
)
8888
return self._spanner_api
89+
if self._instance.experimental_host is not None:
90+
channel = grpc.insecure_channel(self._instance.experimental_host)
91+
self._x_goog_request_id_interceptor = XGoogRequestIDHeaderInterceptor()
92+
self._interceptors.append(self._x_goog_request_id_interceptor)
93+
channel = grpc.intercept_channel(channel, *self._interceptors)
94+
transport = SpannerGrpcTransport(channel=channel)
95+
self._spanner_api = SpannerClient(
96+
client_info=client_info,
97+
transport=transport,
98+
client_options=client_options,
99+
)
100+
return self._spanner_api
89101
credentials = client.credentials
90102
if isinstance(credentials, google.auth.credentials.Scoped):
91103
credentials = credentials.with_scopes((SPANNER_DATA_SCOPE,))

tests/system/_helpers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
EMULATOR_PROJECT_DEFAULT = "emulator-test-project"
5757
EMULATOR_PROJECT = os.getenv(EMULATOR_PROJECT_ENVVAR, EMULATOR_PROJECT_DEFAULT)
5858

59+
USE_EXPERIMENTAL_HOST_ENVVAR = "SPANNER_EXPERIMENTAL_HOST"
60+
EXPERIMENTAL_HOST = os.getenv(USE_EXPERIMENTAL_HOST_ENVVAR)
61+
USE_EXPERIMENTAL_HOST = EXPERIMENTAL_HOST is not None
62+
63+
EXPERIMENTAL_HOST_PROJECT = "default"
64+
EXPERIMENTAL_HOST_INSTANCE = "default"
5965

6066
DDL_STATEMENTS = (
6167
_fixtures.PG_DDL_STATEMENTS

tests/system/conftest.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ def not_emulator():
4949
pytest.skip(f"{_helpers.USE_EMULATOR_ENVVAR} set in environment.")
5050

5151

52+
@pytest.fixture(scope="module")
53+
def not_experimental_host():
54+
if _helpers.USE_EXPERIMENTAL_HOST:
55+
pytest.skip(f"{_helpers.USE_EXPERIMENTAL_HOST_ENVVAR} set in environment.")
56+
57+
5258
@pytest.fixture(scope="session")
5359
def not_postgres(database_dialect):
5460
if database_dialect == DatabaseDialect.POSTGRESQL:
@@ -104,6 +110,15 @@ def spanner_client():
104110
project=_helpers.EMULATOR_PROJECT,
105111
credentials=credentials,
106112
)
113+
elif _helpers.USE_EXPERIMENTAL_HOST:
114+
from google.auth.credentials import AnonymousCredentials
115+
116+
credentials = AnonymousCredentials()
117+
return spanner_v1.Client(
118+
project=_helpers.EXPERIMENTAL_HOST_PROJECT,
119+
credentials=credentials,
120+
experimental_host=_helpers.EXPERIMENTAL_HOST,
121+
)
107122
else:
108123
client_options = {"api_endpoint": _helpers.API_ENDPOINT}
109124
return spanner_v1.Client(
@@ -130,15 +145,16 @@ def backup_operation_timeout():
130145
def shared_instance_id():
131146
if _helpers.CREATE_INSTANCE:
132147
return f"{_helpers.unique_id('google-cloud')}"
133-
148+
if _helpers.USE_EXPERIMENTAL_HOST:
149+
return _helpers.EXPERIMENTAL_HOST_INSTANCE
134150
return _helpers.INSTANCE_ID
135151

136152

137153
@pytest.fixture(scope="session")
138154
def instance_configs(spanner_client):
139155
configs = list(_helpers.retry_503(spanner_client.list_instance_configs)())
140156

141-
if not _helpers.USE_EMULATOR:
157+
if not _helpers.USE_EMULATOR and not _helpers.USE_EXPERIMENTAL_HOST:
142158
# Defend against back-end returning configs for regions we aren't
143159
# actually allowed to use.
144160
configs = [config for config in configs if "-us-" in config.name]

tests/system/test_backup_api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@
2626
Remove {_helpers.SKIP_BACKUP_TESTS_ENVVAR} from environment to run these tests.\
2727
"""
2828
skip_emulator_reason = "Backup operations not supported by emulator."
29+
skip_experimental_host_reason = (
30+
"Backup operations not supported on experimental host yet."
31+
)
2932

3033
pytestmark = [
3134
pytest.mark.skipif(_helpers.SKIP_BACKUP_TESTS, reason=skip_env_reason),
3235
pytest.mark.skipif(_helpers.USE_EMULATOR, reason=skip_emulator_reason),
36+
pytest.mark.skipif(
37+
_helpers.USE_EXPERIMENTAL_HOST, reason=skip_experimental_host_reason
38+
),
3339
]
3440

3541

tests/system/test_database_api.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747

4848

4949
@pytest.fixture(scope="module")
50-
def multiregion_instance(spanner_client, instance_operation_timeout, not_postgres):
50+
def multiregion_instance(
51+
spanner_client, instance_operation_timeout, not_postgres, not_experimental_host
52+
):
5153
multi_region_instance_id = _helpers.unique_id("multi-region")
5254
multi_region_config = "nam3"
5355
config_name = "{}/instanceConfigs/{}".format(
@@ -97,6 +99,7 @@ def test_database_binding_of_fixed_size_pool(
9799
databases_to_delete,
98100
not_postgres,
99101
proto_descriptor_file,
102+
not_experimental_host,
100103
):
101104
temp_db_id = _helpers.unique_id("fixed_size_db", separator="_")
102105
temp_db = shared_instance.database(temp_db_id)
@@ -130,6 +133,7 @@ def test_database_binding_of_pinging_pool(
130133
databases_to_delete,
131134
not_postgres,
132135
proto_descriptor_file,
136+
not_experimental_host,
133137
):
134138
temp_db_id = _helpers.unique_id("binding_db", separator="_")
135139
temp_db = shared_instance.database(temp_db_id)
@@ -217,6 +221,7 @@ def test_create_database_pitr_success(
217221
def test_create_database_with_default_leader_success(
218222
not_emulator, # Default leader setting not supported by the emulator
219223
not_postgres,
224+
not_experimental_host,
220225
multiregion_instance,
221226
databases_to_delete,
222227
):
@@ -253,6 +258,7 @@ def test_create_database_with_default_leader_success(
253258

254259
def test_iam_policy(
255260
not_emulator,
261+
not_experimental_host,
256262
shared_instance,
257263
databases_to_delete,
258264
):
@@ -414,6 +420,7 @@ def test_update_ddl_w_pitr_success(
414420
def test_update_ddl_w_default_leader_success(
415421
not_emulator,
416422
not_postgres,
423+
not_experimental_host,
417424
multiregion_instance,
418425
databases_to_delete,
419426
proto_descriptor_file,
@@ -448,6 +455,7 @@ def test_update_ddl_w_default_leader_success(
448455

449456
def test_create_role_grant_access_success(
450457
not_emulator,
458+
not_experimental_host,
451459
shared_instance,
452460
databases_to_delete,
453461
database_dialect,
@@ -514,6 +522,7 @@ def test_create_role_grant_access_success(
514522

515523
def test_list_database_role_success(
516524
not_emulator,
525+
not_experimental_host,
517526
shared_instance,
518527
databases_to_delete,
519528
database_dialect,
@@ -757,7 +766,11 @@ def test_information_schema_referential_constraints_fkadc(
757766

758767

759768
def test_update_database_success(
760-
not_emulator, shared_database, shared_instance, database_operation_timeout
769+
not_emulator,
770+
not_experimental_host,
771+
shared_database,
772+
shared_instance,
773+
database_operation_timeout,
761774
):
762775
old_protection = shared_database.enable_drop_protection
763776
new_protection = True

0 commit comments

Comments
 (0)