Skip to content

Commit acc62a2

Browse files
zoercailarkee
authored andcommitted
feat: add support for PITR-lite backups (#1)
* Backup changes * Basic tests * Add system tests * Fix system tests * Add retention period to backup systests
1 parent b3b05ad commit acc62a2

File tree

4 files changed

+175
-10
lines changed

4 files changed

+175
-10
lines changed

google/cloud/spanner_v1/backup.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,23 @@ class Backup(object):
5151
:param expire_time: (Optional) The expire time that will be used to
5252
create the backup. Required if the create method
5353
needs to be called.
54+
55+
:type version_time: :class:`datetime.datetime`
56+
:param version_time: (Optional) The version time that was specified for
57+
the externally consistent copy of the database. If
58+
not present, it is the same as the `create_time` of
59+
the backup.
5460
"""
5561

56-
def __init__(self, backup_id, instance, database="", expire_time=None):
62+
def __init__(
63+
self, backup_id, instance, database="", expire_time=None, version_time=None
64+
):
5765
self.backup_id = backup_id
5866
self._instance = instance
5967
self._database = database
6068
self._expire_time = expire_time
6169
self._create_time = None
70+
self._version_time = version_time
6271
self._size_bytes = None
6372
self._state = None
6473
self._referencing_databases = None
@@ -109,6 +118,16 @@ def create_time(self):
109118
"""
110119
return self._create_time
111120

121+
@property
122+
def version_time(self):
123+
"""Version time of this backup.
124+
125+
:rtype: :class:`datetime.datetime`
126+
:returns: a datetime object representing the version time of
127+
this backup
128+
"""
129+
return self._version_time
130+
112131
@property
113132
def size_bytes(self):
114133
"""Size of this backup in bytes.
@@ -190,7 +209,11 @@ def create(self):
190209
raise ValueError("database not set")
191210
api = self._instance._client.database_admin_api
192211
metadata = _metadata_with_prefix(self.name)
193-
backup = BackupPB(database=self._database, expire_time=self.expire_time,)
212+
backup = BackupPB(
213+
database=self._database,
214+
expire_time=self.expire_time,
215+
version_time=self.version_time,
216+
)
194217

195218
future = api.create_backup(
196219
parent=self._instance.name,
@@ -228,6 +251,7 @@ def reload(self):
228251
self._database = pb.database
229252
self._expire_time = pb.expire_time
230253
self._create_time = pb.create_time
254+
self._version_time = pb.version_time
231255
self._size_bytes = pb.size_bytes
232256
self._state = BackupPB.State(pb.state)
233257
self._referencing_databases = pb.referencing_databases

google/cloud/spanner_v1/instance.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ def list_databases(self, page_size=None):
400400
)
401401
return page_iter
402402

403-
def backup(self, backup_id, database="", expire_time=None):
403+
def backup(self, backup_id, database="", expire_time=None, version_time=None):
404404
"""Factory to create a backup within this instance.
405405
406406
:type backup_id: str
@@ -415,13 +415,29 @@ def backup(self, backup_id, database="", expire_time=None):
415415
:param expire_time:
416416
Optional. The expire time that will be used when creating the backup.
417417
Required if the create method needs to be called.
418+
419+
:type version_time: :class:`datetime.datetime`
420+
:param version_time:
421+
Optional. The version time that will be used to create the externally
422+
consistent copy of the database. If not present, it is the same as
423+
the `create_time` of the backup.
418424
"""
419425
try:
420426
return Backup(
421-
backup_id, self, database=database.name, expire_time=expire_time
427+
backup_id,
428+
self,
429+
database=database.name,
430+
expire_time=expire_time,
431+
version_time=version_time,
422432
)
423433
except AttributeError:
424-
return Backup(backup_id, self, database=database, expire_time=expire_time)
434+
return Backup(
435+
backup_id,
436+
self,
437+
database=database,
438+
expire_time=expire_time,
439+
version_time=version_time,
440+
)
425441

426442
def list_backups(self, filter_="", page_size=None):
427443
"""List backups for the instance.

tests/system/test_system.py

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,22 @@ def setUpClass(cls):
613613
op1.result(SPANNER_OPERATION_TIMEOUT_IN_SECONDS) # raises on failure / timeout.
614614
op2.result(SPANNER_OPERATION_TIMEOUT_IN_SECONDS) # raises on failure / timeout.
615615

616+
# Add retention period for backups
617+
retention_period = "7d"
618+
ddl_statements = DDL_STATEMENTS + [
619+
"ALTER DATABASE {}"
620+
" SET OPTIONS (version_retention_period = '{}')".format(
621+
cls.DATABASE_NAME, retention_period
622+
)
623+
]
624+
db = Config.INSTANCE.database(
625+
cls.DATABASE_NAME, pool=pool, ddl_statements=ddl_statements
626+
)
627+
operation = db.update_ddl(ddl_statements)
628+
# We want to make sure the operation completes.
629+
operation.result(240) # raises on failure / timeout.
630+
db.reload()
631+
616632
current_config = Config.INSTANCE.configuration_name
617633
same_config_instance_id = "same-config" + unique_resource_id("-")
618634
create_time = str(int(time.time()))
@@ -685,9 +701,16 @@ def test_backup_workflow(self):
685701
backup_id = "backup_id" + unique_resource_id("_")
686702
expire_time = datetime.utcnow() + timedelta(days=3)
687703
expire_time = expire_time.replace(tzinfo=UTC)
704+
version_time = datetime.utcnow() - timedelta(seconds=5)
705+
version_time = version_time.replace(tzinfo=UTC)
688706

689707
# Create backup.
690-
backup = instance.backup(backup_id, database=self._db, expire_time=expire_time)
708+
backup = instance.backup(
709+
backup_id,
710+
database=self._db,
711+
expire_time=expire_time,
712+
version_time=version_time,
713+
)
691714
operation = backup.create()
692715
self.to_delete.append(backup)
693716

@@ -702,6 +725,7 @@ def test_backup_workflow(self):
702725
self.assertEqual(self._db.name, backup._database)
703726
self.assertEqual(expire_time, backup.expire_time)
704727
self.assertIsNotNone(backup.create_time)
728+
self.assertEqual(version_time, backup.version_time)
705729
self.assertIsNotNone(backup.size_bytes)
706730
self.assertIsNotNone(backup.state)
707731

@@ -722,6 +746,80 @@ def test_backup_workflow(self):
722746
backup.delete()
723747
self.assertFalse(backup.exists())
724748

749+
def test_backup_version_time_defaults_to_create_time(self):
750+
from datetime import datetime
751+
from datetime import timedelta
752+
from pytz import UTC
753+
754+
instance = Config.INSTANCE
755+
backup_id = "backup_id" + unique_resource_id("_")
756+
expire_time = datetime.utcnow() + timedelta(days=3)
757+
expire_time = expire_time.replace(tzinfo=UTC)
758+
759+
# Create backup.
760+
backup = instance.backup(backup_id, database=self._db, expire_time=expire_time,)
761+
operation = backup.create()
762+
self.to_delete.append(backup)
763+
764+
# Check metadata.
765+
metadata = operation.metadata
766+
self.assertEqual(backup.name, metadata.name)
767+
self.assertEqual(self._db.name, metadata.database)
768+
operation.result()
769+
770+
# Check backup object.
771+
backup.reload()
772+
self.assertEqual(self._db.name, backup._database)
773+
self.assertIsNotNone(backup.create_time)
774+
self.assertEqual(backup.create_time, backup.version_time)
775+
776+
backup.delete()
777+
self.assertFalse(backup.exists())
778+
779+
def test_create_backup_invalid_version_time_past(self):
780+
from datetime import datetime
781+
from datetime import timedelta
782+
from pytz import UTC
783+
784+
backup_id = "backup_id" + unique_resource_id("_")
785+
expire_time = datetime.utcnow() + timedelta(days=3)
786+
expire_time = expire_time.replace(tzinfo=UTC)
787+
version_time = datetime.utcnow() - timedelta(days=10)
788+
version_time = version_time.replace(tzinfo=UTC)
789+
790+
backup = Config.INSTANCE.backup(
791+
backup_id,
792+
database=self._db,
793+
expire_time=expire_time,
794+
version_time=version_time,
795+
)
796+
797+
with self.assertRaises(exceptions.InvalidArgument):
798+
op = backup.create()
799+
op.result()
800+
801+
def test_create_backup_invalid_version_time_future(self):
802+
from datetime import datetime
803+
from datetime import timedelta
804+
from pytz import UTC
805+
806+
backup_id = "backup_id" + unique_resource_id("_")
807+
expire_time = datetime.utcnow() + timedelta(days=3)
808+
expire_time = expire_time.replace(tzinfo=UTC)
809+
version_time = datetime.utcnow() + timedelta(days=2)
810+
version_time = version_time.replace(tzinfo=UTC)
811+
812+
backup = Config.INSTANCE.backup(
813+
backup_id,
814+
database=self._db,
815+
expire_time=expire_time,
816+
version_time=version_time,
817+
)
818+
819+
with self.assertRaises(exceptions.InvalidArgument):
820+
op = backup.create()
821+
op.result()
822+
725823
def test_restore_to_diff_instance(self):
726824
from datetime import datetime
727825
from datetime import timedelta
@@ -818,9 +916,14 @@ def test_list_backups(self):
818916
instance = Config.INSTANCE
819917
expire_time_1 = datetime.utcnow() + timedelta(days=21)
820918
expire_time_1 = expire_time_1.replace(tzinfo=UTC)
919+
version_time_1 = datetime.utcnow() - timedelta(minutes=5)
920+
version_time_1 = version_time_1.replace(tzinfo=UTC)
821921

822922
backup1 = Config.INSTANCE.backup(
823-
backup_id_1, database=self._dbs[0], expire_time=expire_time_1
923+
backup_id_1,
924+
database=self._dbs[0],
925+
expire_time=expire_time_1,
926+
version_time=version_time_1,
824927
)
825928

826929
expire_time_2 = datetime.utcnow() + timedelta(days=1)
@@ -860,6 +963,13 @@ def test_list_backups(self):
860963
for backup in instance.list_backups(filter_=filter_):
861964
self.assertEqual(backup.name, backup2.name)
862965

966+
# List backups filtered by version time.
967+
filter_ = 'version_time > "{0}"'.format(
968+
create_time_compare.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
969+
)
970+
for backup in instance.list_backups(filter_=filter_):
971+
self.assertEqual(backup.name, backup2.name)
972+
863973
# List backups filtered by expire time.
864974
filter_ = 'expire_time > "{0}"'.format(
865975
expire_time_1.strftime("%Y-%m-%dT%H:%M:%S.%fZ")

tests/unit/test_backup.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,32 @@ def test_create_database_not_set(self):
266266

267267
def test_create_success(self):
268268
from google.cloud.spanner_admin_database_v1 import Backup
269+
from datetime import datetime
270+
from datetime import timedelta
271+
from pytz import UTC
269272

270273
op_future = object()
271274
client = _Client()
272275
api = client.database_admin_api = self._make_database_admin_api()
273276
api.create_backup.return_value = op_future
274277

275278
instance = _Instance(self.INSTANCE_NAME, client=client)
276-
timestamp = self._make_timestamp()
279+
version_timestamp = datetime.utcnow() - timedelta(minutes=5)
280+
version_timestamp = version_timestamp.replace(tzinfo=UTC)
281+
expire_timestamp = self._make_timestamp()
277282
backup = self._make_one(
278-
self.BACKUP_ID, instance, database=self.DATABASE_NAME, expire_time=timestamp
283+
self.BACKUP_ID,
284+
instance,
285+
database=self.DATABASE_NAME,
286+
expire_time=expire_timestamp,
287+
version_time=version_timestamp,
279288
)
280289

281-
backup_pb = Backup(database=self.DATABASE_NAME, expire_time=timestamp,)
290+
backup_pb = Backup(
291+
database=self.DATABASE_NAME,
292+
expire_time=expire_timestamp,
293+
version_time=version_timestamp,
294+
)
282295

283296
future = backup.create()
284297
self.assertIs(future, op_future)
@@ -437,6 +450,7 @@ def test_reload_success(self):
437450
name=self.BACKUP_NAME,
438451
database=self.DATABASE_NAME,
439452
expire_time=timestamp,
453+
version_time=timestamp,
440454
create_time=timestamp,
441455
size_bytes=10,
442456
state=1,
@@ -452,6 +466,7 @@ def test_reload_success(self):
452466
self.assertEqual(backup.database, self.DATABASE_NAME)
453467
self.assertEqual(backup.expire_time, timestamp)
454468
self.assertEqual(backup.create_time, timestamp)
469+
self.assertEqual(backup.version_time, timestamp)
455470
self.assertEqual(backup.size_bytes, 10)
456471
self.assertEqual(backup.state, Backup.State.CREATING)
457472
self.assertEqual(backup.referencing_databases, [])

0 commit comments

Comments
 (0)