Skip to content

Commit 9bea38c

Browse files
ScottLinnncopybara-github
authored andcommitted
Implement backup restore for DSQL
PiperOrigin-RevId: 845429012
1 parent 3947758 commit 9bea38c

File tree

4 files changed

+334
-62
lines changed

4 files changed

+334
-62
lines changed

perfkitbenchmarker/linux_benchmarks/benchbase_benchmark.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
_WARMUP_DURATION = benchbase.BENCHBASE_WARMUP_DURATION
7373
_WORKLOAD_DURATION = benchbase.BENCHBASE_WORKLOAD_DURATION
7474
_WAREHOUSES = benchbase.BENCHBASE_WAREHOUSES
75+
_RECOVERY_POINT_ARN = aws_aurora_dsql_db.AWS_AURORA_DSQL_RECOVERY_POINT_ARN
7576

7677

7778
def GetConfig(user_config: Dict[str, Any]) -> Dict[str, Any]:
@@ -87,7 +88,8 @@ def GetConfig(user_config: Dict[str, Any]) -> Dict[str, Any]:
8788
if _COOLDOWN_DURATION.value > 0 and _WARMUP_DURATION.value <= 0:
8889
raise errors.Config.InvalidValue(
8990
'benchbase_warmup_duration must be positive if'
90-
' benchbase_cooldown_duration is positive.')
91+
' benchbase_cooldown_duration is positive.'
92+
)
9193
return config
9294

9395

@@ -107,6 +109,7 @@ def Prepare(benchmark_spec: bm_spec.BenchmarkSpec) -> None:
107109

108110
# Create the configuration file on the client VM
109111
benchbase.CreateConfigFile(client_vm)
112+
110113
if FLAGS.db_engine == sql_engine_utils.AURORA_DSQL_POSTGRES:
111114
dsql: aws_aurora_dsql_db.AwsAuroraDsqlRelationalDb = (
112115
benchmark_spec.relational_db
@@ -117,19 +120,27 @@ def Prepare(benchmark_spec: bm_spec.BenchmarkSpec) -> None:
117120
# https://docs.aws.amazon.com/aurora-dsql/latest/userguide/SECTION_authentication-token.html#authentication-token-cli
118121
endpoint: str = f'{dsql.cluster_id}.dsql.{dsql.region}.on.aws'
119122
benchbase.OverrideEndpoint(client_vm, endpoint)
120-
profile: str = (
121-
'postgres'
122-
if FLAGS.db_engine == sql_engine_utils.SPANNER_POSTGRES
123-
else 'auroradsql'
124-
)
125-
load_command: str = (
126-
f'source /etc/profile.d/maven.sh && cd {benchbase.BENCHBASE_DIR} && mvn'
127-
f' clean compile exec:java -P {profile} -Dexec.args="-b tpcc -c'
128-
f' {benchbase.CONFIG_FILE_NAME} --create=true --load=true'
129-
' --execute=false"'
130-
)
131123

132-
client_vm.RemoteCommand(load_command)
124+
dsql_create_from_raw: bool = (
125+
FLAGS.db_engine == sql_engine_utils.AURORA_DSQL_POSTGRES
126+
and _RECOVERY_POINT_ARN.value is None
127+
)
128+
if (
129+
dsql_create_from_raw
130+
or FLAGS.db_engine == sql_engine_utils.SPANNER_POSTGRES
131+
):
132+
profile: str = (
133+
'postgres'
134+
if FLAGS.db_engine == sql_engine_utils.SPANNER_POSTGRES
135+
else 'auroradsql'
136+
)
137+
load_command: str = (
138+
f'source /etc/profile.d/maven.sh && cd {benchbase.BENCHBASE_DIR} && mvn'
139+
f' clean compile exec:java -P {profile} -Dexec.args="-b tpcc -c'
140+
f' {benchbase.CONFIG_FILE_NAME} --create=true --load=true'
141+
' --execute=false"'
142+
)
143+
client_vm.RemoteCommand(load_command)
133144

134145

135146
def Run(benchmark_spec: bm_spec.BenchmarkSpec) -> List[sample.Sample]:

perfkitbenchmarker/providers/aws/aws_aurora_dsql_db.py

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
"""Managed relational database provisioning and teardown for AWS Aurora DSQL."""
1515

16+
import functools
1617
import json
1718
from typing import Any
1819

@@ -27,11 +28,16 @@
2728
from perfkitbenchmarker.providers.aws import util
2829

2930

30-
# TODO(shuninglin): Add cluster creation from a backup.
3131
# TODO(shuninglin): Add reaper for this new resource.
3232

3333
FLAGS = flags.FLAGS
3434

35+
AWS_AURORA_DSQL_RECOVERY_POINT_ARN = flags.DEFINE_string(
36+
'aws_aurora_dsql_recovery_point_arn',
37+
None,
38+
'The ARN of the recovery point to restore AWS Aurora DSQL cluster from. If '
39+
'not provided, a new cluster is created from scratch.',
40+
)
3541
DEFAULT_AURORA_DSQL_POSTGRES_VERSION = '16.2'
3642

3743
_MAP_ENGINE_TO_DEFAULT_VERSION = {
@@ -69,7 +75,15 @@ class AwsAuroraDsqlRelationalDb(aws_relational_db.BaseAwsRelationalDb):
6975
def __init__(self, dsql_spec: AwsAuroraDsqlSpec):
7076
super().__init__(dsql_spec)
7177
self.cluster_id = None
78+
self.cluster_arn = None
7279
self.assigned_name = f'pkb-{FLAGS.run_uri}'
80+
self.use_backup = bool(AWS_AURORA_DSQL_RECOVERY_POINT_ARN.value)
81+
self.restore_job_id = None
82+
83+
@functools.cached_property
84+
def account_id(self) -> str:
85+
"""Returns the AWS account ID."""
86+
return util.GetAccount()
7387

7488
# DSQL has different format for tags:
7589
# https://docs.aws.amazon.com/cli/v1/reference/rds/create-db-cluster.html
@@ -83,6 +97,64 @@ def _MakeDsqlTags(self) -> list[str]:
8397
return [formatted_tags_str]
8498

8599
def _Create(self) -> None:
100+
"""Creates AWS Aurora DSQL cluster, from backup if recovery point ARN is provided."""
101+
if not self.use_backup:
102+
self._CreateRawCluster()
103+
return
104+
if self.restore_job_id:
105+
logging.info(
106+
'Restore job %s already exists. Skipping creation.',
107+
self.restore_job_id,
108+
)
109+
return
110+
cmd = util.AWS_PREFIX + [
111+
'backup',
112+
'start-restore-job',
113+
'--recovery-point-arn',
114+
AWS_AURORA_DSQL_RECOVERY_POINT_ARN.value,
115+
'--iam-role-arn',
116+
(
117+
f'arn:aws:iam::{self.account_id}:role/service-role/'
118+
'AWSBackupDefaultServiceRole'
119+
),
120+
'--metadata',
121+
'{"regionalConfig": "[{\\"region\\": \\"%s\\",'
122+
' \\"isDeletionProtectionEnabled\\": false}]"}'
123+
% self.region,
124+
]
125+
stdout, _, _ = vm_util.IssueCommand(cmd)
126+
response = json.loads(stdout)
127+
self.restore_job_id = response['RestoreJobId']
128+
if self.restore_job_id:
129+
# Mark created so we don't try to create it again on a retry.
130+
self.created = True
131+
132+
def _DescribeRestoreJob(self, job_id: str) -> dict[str, Any]:
133+
"""Describes the restore job."""
134+
cmd = util.AWS_PREFIX + [
135+
'backup',
136+
'describe-restore-job',
137+
'--restore-job-id',
138+
job_id,
139+
]
140+
stdout, _, _ = vm_util.IssueCommand(cmd)
141+
return json.loads(stdout)
142+
143+
def _AddTagsToCluster(self, cluster_arn: str) -> None:
144+
"""Adds tags to the DSQL cluster."""
145+
cmd = (
146+
util.AWS_PREFIX
147+
+ [
148+
'dsql',
149+
'tag-resource',
150+
'--resource-arn=%s' % cluster_arn,
151+
'--tags',
152+
]
153+
+ self._MakeDsqlTags()
154+
)
155+
vm_util.IssueCommand(cmd)
156+
157+
def _CreateRawCluster(self) -> None:
86158
"""Creates the AWS Aurora DSQL instance.
87159
88160
Raises:
@@ -108,6 +180,9 @@ def _Create(self) -> None:
108180
self.cluster_id = response['identifier']
109181

110182
def _DescribeCluster(self) -> dict[str, Any] | None:
183+
if not self.cluster_id:
184+
logging.info('Cluster id is not set.')
185+
return None
111186
cmd = util.AWS_PREFIX + [
112187
'dsql',
113188
'get-cluster',
@@ -122,8 +197,29 @@ def _DescribeCluster(self) -> dict[str, Any] | None:
122197

123198
def _IsReady(self, timeout=aws_relational_db.IS_READY_TIMEOUT) -> bool:
124199
"""Returns true if the cluster is ready."""
125-
json_output = self._DescribeCluster()
126-
return bool(json_output and json_output['status'] == 'ACTIVE')
200+
if self.use_backup:
201+
if not self.restore_job_id:
202+
return False
203+
job_description = self._DescribeRestoreJob(self.restore_job_id)
204+
status = job_description['Status']
205+
if status == 'COMPLETED':
206+
self.cluster_id = job_description['CreatedResourceArn'].split('/')[-1]
207+
self.cluster_arn = job_description['CreatedResourceArn']
208+
return True
209+
if status in ['ABORTED', 'FAILED']:
210+
raise errors.Resource.CreationError(
211+
f'Restore job {self.restore_job_id} failed with status {status}'
212+
)
213+
return False
214+
else:
215+
json_output = self._DescribeCluster()
216+
return bool(json_output and json_output['status'] == 'ACTIVE')
217+
218+
def _PostCreate(self) -> None:
219+
"""Add tags if we are restoring from backup."""
220+
super()._PostCreate()
221+
if self.use_backup:
222+
self._AddTagsToCluster(self.cluster_arn)
127223

128224
def _Exists(self) -> bool:
129225
"""Returns true if the underlying cluster exists."""

tests/linux_benchmarks/benchbase_benchmark_test.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import mock
2222
from perfkitbenchmarker import benchmark_spec
2323
from perfkitbenchmarker import configs
24+
from perfkitbenchmarker import sql_engine_utils
2425
from perfkitbenchmarker.linux_benchmarks import benchbase_benchmark
2526
from perfkitbenchmarker.linux_packages import benchbase
2627
from tests import pkb_common_test_case
@@ -39,9 +40,17 @@ def setUp(self):
3940
self.mock_benchmark_spec.vms = [self.mock_vm]
4041
self.mock_benchmark_spec.relational_db = mock.Mock()
4142
self.mock_benchmark_spec.relational_db.GetResourceMetadata.return_value = {}
43+
self.mock_benchmark_spec.relational_db.cluster_id = 'cluster'
44+
self.mock_benchmark_spec.relational_db.region = 'region'
4245
self.mock_load_config = self.enter_context(
4346
mock.patch.object(configs, 'LoadConfig', autospec=True)
4447
)
48+
self.mock_create_config = self.enter_context(
49+
mock.patch.object(benchbase, 'CreateConfigFile', autospec=True)
50+
)
51+
self.mock_override_endpoint = self.enter_context(
52+
mock.patch.object(benchbase, 'OverrideEndpoint', autospec=True)
53+
)
4554

4655
def test_get_config(self):
4756
user_config = {'key': 'value'}
@@ -52,13 +61,44 @@ def test_get_config(self):
5261
benchbase_benchmark.BENCHMARK_NAME,
5362
)
5463

55-
@flagsaver.flagsaver(db_engine='spanner-postgres')
56-
@mock.patch.object(benchbase, 'CreateConfigFile', autospec=True)
57-
def test_prepare(self, mock_create_config):
64+
@flagsaver.flagsaver(db_engine=sql_engine_utils.SPANNER_POSTGRES)
65+
def test_prepare_spanner_postgres_loads(self):
66+
benchbase_benchmark.Prepare(self.mock_benchmark_spec)
67+
68+
self.mock_vm.Install.assert_called_once_with('benchbase')
69+
self.mock_create_config.assert_called_once_with(self.mock_vm)
70+
self.mock_override_endpoint.assert_not_called()
71+
self.mock_vm.RemoteCommand.assert_called_once()
72+
self.assertIn(
73+
'--create=true --load=true', self.mock_vm.RemoteCommand.call_args[0][0]
74+
)
75+
self.assertIn('-P postgres', self.mock_vm.RemoteCommand.call_args[0][0])
76+
77+
@flagsaver.flagsaver(
78+
db_engine=sql_engine_utils.AURORA_DSQL_POSTGRES,
79+
aws_aurora_dsql_recovery_point_arn=None,
80+
)
81+
def test_prepare_dsql_raw_loads(self):
5882
benchbase_benchmark.Prepare(self.mock_benchmark_spec)
83+
self.mock_vm.Install.assert_called_once_with('benchbase')
84+
self.mock_create_config.assert_called_once_with(self.mock_vm)
85+
self.mock_override_endpoint.assert_called_once()
86+
self.mock_vm.RemoteCommand.assert_called_once()
87+
self.assertIn(
88+
'--create=true --load=true', self.mock_vm.RemoteCommand.call_args[0][0]
89+
)
90+
self.assertIn('-P auroradsql', self.mock_vm.RemoteCommand.call_args[0][0])
5991

92+
@flagsaver.flagsaver(
93+
db_engine=sql_engine_utils.AURORA_DSQL_POSTGRES,
94+
aws_aurora_dsql_recovery_point_arn='arn',
95+
)
96+
def test_prepare_dsql_restore_skips_loading(self):
97+
benchbase_benchmark.Prepare(self.mock_benchmark_spec)
6098
self.mock_vm.Install.assert_called_once_with('benchbase')
61-
mock_create_config.assert_called_once_with(self.mock_vm)
99+
self.mock_create_config.assert_called_once_with(self.mock_vm)
100+
self.mock_override_endpoint.assert_called_once()
101+
self.mock_vm.RemoteCommand.assert_not_called()
62102

63103
@mock.patch('time.sleep')
64104
@mock.patch.object(benchbase, 'ParseResults', autospec=True)

0 commit comments

Comments
 (0)