Skip to content

Commit 7f1b120

Browse files
authored
feat: add configurable leader placement support (#399)
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/python-spanner/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #<issue_number_goes_here> 🦕
1 parent 9c529f3 commit 7f1b120

File tree

4 files changed

+109
-1
lines changed

4 files changed

+109
-1
lines changed

google/cloud/spanner_v1/database.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ def __init__(
144144
self._version_retention_period = None
145145
self._earliest_version_time = None
146146
self._encryption_info = None
147+
self._default_leader = None
147148
self.log_commit_stats = False
148149
self._logger = logger
149150
self._encryption_config = encryption_config
@@ -279,6 +280,15 @@ def encryption_info(self):
279280
"""
280281
return self._encryption_info
281282

283+
@property
284+
def default_leader(self):
285+
"""The read-write region which contains the database's leader replicas.
286+
287+
:rtype: str
288+
:returns: a string representing the read-write region
289+
"""
290+
return self._default_leader
291+
282292
@property
283293
def ddl_statements(self):
284294
"""DDL Statements used to define database schema.
@@ -414,6 +424,7 @@ def reload(self):
414424
self._earliest_version_time = response.earliest_version_time
415425
self._encryption_config = response.encryption_config
416426
self._encryption_info = response.encryption_info
427+
self._default_leader = response.default_leader
417428

418429
def update_ddl(self, ddl_statements, operation_id=""):
419430
"""Update DDL for this database.

tests/system/test_system.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
INSTANCE_ID = os.environ.get(
6969
"GOOGLE_CLOUD_TESTS_SPANNER_INSTANCE", "google-cloud-python-systest"
7070
)
71+
MULTI_REGION_INSTANCE_ID = "multi-region" + unique_resource_id("-")
7172
EXISTING_INSTANCES = []
7273
COUNTERS_TABLE = "counters"
7374
COUNTERS_COLUMNS = ("name", "value")
@@ -353,9 +354,25 @@ def setUpClass(cls):
353354
SPANNER_OPERATION_TIMEOUT_IN_SECONDS
354355
) # raises on failure / timeout.
355356

357+
# Create a multi-region instance
358+
multi_region_config = "nam3"
359+
config_name = "{}/instanceConfigs/{}".format(
360+
Config.CLIENT.project_name, multi_region_config
361+
)
362+
create_time = str(int(time.time()))
363+
labels = {"python-spanner-systests": "true", "created": create_time}
364+
cls._instance = Config.CLIENT.instance(
365+
instance_id=MULTI_REGION_INSTANCE_ID,
366+
configuration_name=config_name,
367+
labels=labels,
368+
)
369+
operation = cls._instance.create()
370+
operation.result(SPANNER_OPERATION_TIMEOUT_IN_SECONDS)
371+
356372
@classmethod
357373
def tearDownClass(cls):
358374
cls._db.drop()
375+
cls._instance.delete()
359376

360377
def setUp(self):
361378
self.to_delete = []
@@ -443,6 +460,42 @@ def test_create_database_pitr_success(self):
443460
for result in results:
444461
self.assertEqual(result[0], retention_period)
445462

463+
@unittest.skipIf(
464+
USE_EMULATOR, "Default leader setting is not supported by the emulator"
465+
)
466+
def test_create_database_with_default_leader_success(self):
467+
pool = BurstyPool(labels={"testcase": "create_database_default_leader"})
468+
469+
temp_db_id = "temp_db" + unique_resource_id("_")
470+
default_leader = "us-east4"
471+
ddl_statements = [
472+
"ALTER DATABASE {}"
473+
" SET OPTIONS (default_leader = '{}')".format(temp_db_id, default_leader)
474+
]
475+
temp_db = self._instance.database(
476+
temp_db_id, pool=pool, ddl_statements=ddl_statements
477+
)
478+
operation = temp_db.create()
479+
self.to_delete.append(temp_db)
480+
481+
# We want to make sure the operation completes.
482+
operation.result(30) # raises on failure / timeout.
483+
484+
database_ids = [database.name for database in self._instance.list_databases()]
485+
self.assertIn(temp_db.name, database_ids)
486+
487+
temp_db.reload()
488+
self.assertEqual(temp_db.default_leader, default_leader)
489+
490+
with temp_db.snapshot() as snapshot:
491+
results = snapshot.execute_sql(
492+
"SELECT OPTION_VALUE AS default_leader "
493+
"FROM INFORMATION_SCHEMA.DATABASE_OPTIONS "
494+
"WHERE SCHEMA_NAME = '' AND OPTION_NAME = 'default_leader'"
495+
)
496+
for result in results:
497+
self.assertEqual(result[0], default_leader)
498+
446499
def test_table_not_found(self):
447500
temp_db_id = "temp_db" + unique_resource_id("_")
448501

@@ -551,6 +604,36 @@ def test_update_database_ddl_pitr_success(self):
551604
self.assertEqual(temp_db.version_retention_period, retention_period)
552605
self.assertEqual(len(temp_db.ddl_statements), len(ddl_statements))
553606

607+
@unittest.skipIf(
608+
USE_EMULATOR, "Default leader update is not supported by the emulator"
609+
)
610+
def test_update_database_ddl_default_leader_success(self):
611+
pool = BurstyPool(labels={"testcase": "update_database_ddl_default_leader"})
612+
613+
temp_db_id = "temp_db" + unique_resource_id("_")
614+
default_leader = "us-east4"
615+
temp_db = self._instance.database(temp_db_id, pool=pool)
616+
create_op = temp_db.create()
617+
self.to_delete.append(temp_db)
618+
619+
# We want to make sure the operation completes.
620+
create_op.result(240) # raises on failure / timeout.
621+
622+
self.assertIsNone(temp_db.default_leader)
623+
624+
ddl_statements = DDL_STATEMENTS + [
625+
"ALTER DATABASE {}"
626+
" SET OPTIONS (default_leader = '{}')".format(temp_db_id, default_leader)
627+
]
628+
operation = temp_db.update_ddl(ddl_statements)
629+
630+
# We want to make sure the operation completes.
631+
operation.result(240) # raises on failure / timeout.
632+
633+
temp_db.reload()
634+
self.assertEqual(temp_db.default_leader, default_leader)
635+
self.assertEqual(len(temp_db.ddl_statements), len(ddl_statements))
636+
554637
def test_db_batch_insert_then_db_snapshot_read(self):
555638
retry = RetryInstanceState(_has_all_ddl)
556639
retry(self._db.reload)()

tests/unit/test_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class TestClient(unittest.TestCase):
4040
PROCESSING_UNITS = 5000
4141
LABELS = {"test": "true"}
4242
TIMEOUT_SECONDS = 80
43+
LEADER_OPTIONS = ["leader1", "leader2"]
4344

4445
def _get_target_class(self):
4546
from google.cloud import spanner
@@ -457,7 +458,9 @@ def test_list_instance_configs(self):
457458
instance_config_pbs = ListInstanceConfigsResponse(
458459
instance_configs=[
459460
InstanceConfigPB(
460-
name=self.CONFIGURATION_NAME, display_name=self.DISPLAY_NAME
461+
name=self.CONFIGURATION_NAME,
462+
display_name=self.DISPLAY_NAME,
463+
leader_options=self.LEADER_OPTIONS,
461464
)
462465
]
463466
)
@@ -473,6 +476,7 @@ def test_list_instance_configs(self):
473476
self.assertIsInstance(instance_config, InstanceConfigPB)
474477
self.assertEqual(instance_config.name, self.CONFIGURATION_NAME)
475478
self.assertEqual(instance_config.display_name, self.DISPLAY_NAME)
479+
self.assertEqual(instance_config.leader_options, self.LEADER_OPTIONS)
476480

477481
expected_metadata = (
478482
("google-cloud-resource-prefix", client.project_name),

tests/unit/test_database.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@ def test_encryption_info(self):
333333
]
334334
self.assertEqual(database.encryption_info, encryption_info)
335335

336+
def test_default_leader(self):
337+
instance = _Instance(self.INSTANCE_NAME)
338+
pool = _Pool()
339+
database = self._make_one(self.DATABASE_ID, instance, pool=pool)
340+
default_leader = database._default_leader = "us-east4"
341+
self.assertEqual(database.default_leader, default_leader)
342+
336343
def test_spanner_api_property_w_scopeless_creds(self):
337344

338345
client = _Client()
@@ -715,6 +722,7 @@ def test_reload_success(self):
715722
kms_key_version="kms_key_version",
716723
)
717724
]
725+
default_leader = "us-east4"
718726
api = client.database_admin_api = self._make_database_admin_api()
719727
api.get_database_ddl.return_value = ddl_pb
720728
db_pb = Database(
@@ -725,6 +733,7 @@ def test_reload_success(self):
725733
earliest_version_time=_datetime_to_pb_timestamp(timestamp),
726734
encryption_config=encryption_config,
727735
encryption_info=encryption_info,
736+
default_leader=default_leader,
728737
)
729738
api.get_database.return_value = db_pb
730739
instance = _Instance(self.INSTANCE_NAME, client=client)
@@ -740,6 +749,7 @@ def test_reload_success(self):
740749
self.assertEqual(database._ddl_statements, tuple(DDL_STATEMENTS))
741750
self.assertEqual(database._encryption_config, encryption_config)
742751
self.assertEqual(database._encryption_info, encryption_info)
752+
self.assertEqual(database._default_leader, default_leader)
743753

744754
api.get_database_ddl.assert_called_once_with(
745755
database=self.DATABASE_NAME,

0 commit comments

Comments
 (0)