Skip to content

Commit c49f680

Browse files
authored
Multi-AZ integration tests (#328)
1 parent ecf04a0 commit c49f680

31 files changed

+871
-497
lines changed

.github/workflows/autoscaling_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
run: |
5454
./gradlew --no-parallel --no-daemon test-autoscaling --info
5555
env:
56-
AURORA_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
56+
RDS_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
5757
AURORA_DB_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
5858
AWS_ACCESS_KEY_ID: ${{ env.TEMP_AWS_ACCESS_KEY_ID }}
5959
AWS_SECRET_ACCESS_KEY: ${{ env.TEMP_AWS_SECRET_ACCESS_KEY }}

.github/workflows/integration_tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ jobs:
5656
run: |
5757
./gradlew --no-parallel --no-daemon test-all-environments --info
5858
env:
59-
AURORA_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
59+
RDS_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
6060
AURORA_DB_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
61+
MULTI_AZ_DB_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
6162
AWS_ACCESS_KEY_ID: ${{ env.TEMP_AWS_ACCESS_KEY_ID }}
6263
AWS_SECRET_ACCESS_KEY: ${{ env.TEMP_AWS_SECRET_ACCESS_KEY }}
6364
AWS_SESSION_TOKEN: ${{ env.TEMP_AWS_SESSION_TOKEN }}

.github/workflows/mysql_performance_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
run: |
5454
./gradlew --no-parallel --no-daemon test-mysql-aurora-performance --info
5555
env:
56-
AURORA_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
56+
RDS_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
5757
AURORA_DB_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
5858
AWS_ACCESS_KEY_ID: ${{ env.TEMP_AWS_ACCESS_KEY_ID }}
5959
AWS_SECRET_ACCESS_KEY: ${{ env.TEMP_AWS_SECRET_ACCESS_KEY }}

.github/workflows/pg_performance_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
run: |
5454
./gradlew --no-parallel --no-daemon test-pg-aurora-performance --info
5555
env:
56-
AURORA_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
56+
RDS_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }}
5757
AURORA_DB_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
5858
AWS_ACCESS_KEY_ID: ${{ env.TEMP_AWS_ACCESS_KEY_ID }}
5959
AWS_SECRET_ACCESS_KEY: ${{ env.TEMP_AWS_SECRET_ACCESS_KEY }}

aws_advanced_python_wrapper/database_dialect.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from aws_advanced_python_wrapper.errors import (AwsWrapperError,
3333
QueryTimeoutError)
3434
from aws_advanced_python_wrapper.host_list_provider import (
35-
ConnectionStringHostListProvider, MultiAzRdsHostListProvider,
35+
ConnectionStringHostListProvider, MultiAzHostListProvider,
3636
RdsHostListProvider)
3737
from aws_advanced_python_wrapper.hostinfo import HostInfo
3838
from aws_advanced_python_wrapper.utils.decorators import \
@@ -253,7 +253,7 @@ def prepare_conn_props(self, props: Properties):
253253

254254

255255
class RdsMysqlDialect(MysqlDatabaseDialect):
256-
_DIALECT_UPDATE_CANDIDATES = (DialectCode.AURORA_MYSQL,)
256+
_DIALECT_UPDATE_CANDIDATES = (DialectCode.AURORA_MYSQL, DialectCode.MULTI_AZ_MYSQL)
257257

258258
def is_dialect(self, conn: Connection) -> bool:
259259
try:
@@ -278,7 +278,7 @@ class RdsPgDialect(PgDatabaseDialect):
278278
"(setting LIKE '%aurora_stat_utils%') AS aurora_stat_utils "
279279
"FROM pg_settings "
280280
"WHERE name='rds.extensions'")
281-
_DIALECT_UPDATE_CANDIDATES = (DialectCode.AURORA_PG,)
281+
_DIALECT_UPDATE_CANDIDATES = (DialectCode.AURORA_PG, DialectCode.MULTI_AZ_PG)
282282

283283
def is_dialect(self, conn: Connection) -> bool:
284284
if not super().is_dialect(conn):
@@ -337,6 +337,8 @@ def get_host_list_provider_supplier(self) -> Callable:
337337

338338

339339
class AuroraPgDialect(PgDatabaseDialect, TopologyAwareDatabaseDialect):
340+
_DIALECT_UPDATE_CANDIDATES: Tuple[DialectCode, ...] = (DialectCode.MULTI_AZ_PG,)
341+
340342
_EXTENSIONS_QUERY = "SELECT (setting LIKE '%aurora_stat_utils%') AS aurora_stat_utils " \
341343
"FROM pg_settings WHERE name='rds.extensions'"
342344

@@ -352,6 +354,10 @@ class AuroraPgDialect(PgDatabaseDialect, TopologyAwareDatabaseDialect):
352354
_HOST_ID_QUERY = "SELECT aurora_db_instance_identifier()"
353355
_IS_READER_QUERY = "SELECT pg_is_in_recovery()"
354356

357+
@property
358+
def dialect_update_candidates(self) -> Optional[Tuple[DialectCode, ...]]:
359+
return AuroraPgDialect._DIALECT_UPDATE_CANDIDATES
360+
355361
def is_dialect(self, conn: Connection) -> bool:
356362
if not super().is_dialect(conn):
357363
return False
@@ -385,7 +391,7 @@ def get_host_list_provider_supplier(self) -> Callable:
385391
return lambda provider_service, props: RdsHostListProvider(provider_service, props)
386392

387393

388-
class MultiAzMysqlDialect(MysqlDatabaseDialect):
394+
class MultiAzMysqlDialect(MysqlDatabaseDialect, TopologyAwareDatabaseDialect):
389395
_TOPOLOGY_QUERY = "SELECT id, endpoint, port FROM mysql.rds_topology"
390396
_WRITER_HOST_QUERY = "SHOW REPLICA STATUS"
391397
_WRITER_HOST_COLUMN_INDEX = 39
@@ -400,15 +406,16 @@ def is_dialect(self, conn: Connection) -> bool:
400406
try:
401407
with closing(conn.cursor()) as cursor:
402408
cursor.execute(MultiAzMysqlDialect._TOPOLOGY_QUERY)
403-
if cursor.fetchone() is not None:
409+
records = cursor.fetchall()
410+
if records is not None and len(records) > 0:
404411
return True
405412
except Exception:
406413
pass
407414

408415
return False
409416

410417
def get_host_list_provider_supplier(self) -> Callable:
411-
return lambda provider_service, props: MultiAzRdsHostListProvider(
418+
return lambda provider_service, props: MultiAzHostListProvider(
412419
provider_service,
413420
props,
414421
self._TOPOLOGY_QUERY,
@@ -430,7 +437,7 @@ def prepare_conn_props(self, props: Properties):
430437
props["conn_attrs"].update(extra_conn_attrs)
431438

432439

433-
class MultiAzPgDialect(PgDatabaseDialect):
440+
class MultiAzPgDialect(PgDatabaseDialect, TopologyAwareDatabaseDialect):
434441
# The driver name passed to show_topology is used for RDS metrics purposes.
435442
# It is not required for functional correctness.
436443
_TOPOLOGY_QUERY = \
@@ -464,7 +471,7 @@ def is_dialect(self, conn: Connection) -> bool:
464471
return False
465472

466473
def get_host_list_provider_supplier(self) -> Callable:
467-
return lambda provider_service, props: MultiAzRdsHostListProvider(
474+
return lambda provider_service, props: MultiAzHostListProvider(
468475
provider_service,
469476
props,
470477
self._TOPOLOGY_QUERY,

aws_advanced_python_wrapper/host_list_provider.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,16 @@ def _initialize(self):
185185
self._initial_hosts: Tuple[HostInfo, ...] = (self._initial_host_info,)
186186
self._host_list_provider_service.initial_connection_host_info = self._initial_host_info
187187

188-
self._cluster_instance_template: HostInfo
189188
host_pattern = WrapperProperties.CLUSTER_INSTANCE_HOST_PATTERN.get(self._props)
190189
if host_pattern:
190+
if host_pattern.find(":") > -1:
191+
host_pattern, port = host_pattern.split(":")
192+
else:
193+
port = HostInfo.NO_PORT
194+
191195
self._cluster_instance_template = HostInfo(
192-
host=WrapperProperties.CLUSTER_INSTANCE_HOST_PATTERN.get(self._props),
196+
host=host_pattern,
197+
port=port,
193198
host_availability_strategy=host_availability_strategy)
194199
else:
195200
self._cluster_instance_template = HostInfo(
@@ -503,7 +508,7 @@ class FetchTopologyResult:
503508
is_cached_data: bool
504509

505510

506-
class MultiAzRdsHostListProvider(RdsHostListProvider):
511+
class MultiAzHostListProvider(RdsHostListProvider):
507512
def __init__(
508513
self,
509514
provider_service: HostListProviderService,
@@ -560,13 +565,23 @@ def _process_multi_az_query_results(self, cursor: Cursor, writer_id: str) -> Tup
560565
return tuple(hosts)
561566

562567
def _create_multi_az_host(self, record: Tuple, writer_id: str) -> HostInfo:
563-
host_id = record[0]
568+
id = record[0] # The ID will look something like '0123456789' (MySQL) or 'db-ABC1DE2FGHI' (Postgres)
564569
host = record[1]
565570
port = record[2]
566-
role = HostRole.WRITER if host_id == writer_id else HostRole.READER
571+
role = HostRole.WRITER if id == writer_id else HostRole.READER
572+
573+
host_pattern = WrapperProperties.CLUSTER_INSTANCE_HOST_PATTERN.get(self._props)
574+
if host_pattern:
575+
instance_name = self._rds_utils.get_instance_id(host) # e.g. 'postgres-instance-1'
576+
if instance_name is None:
577+
raise AwsWrapperError(Messages.get("MultiAzHostListProvider.UnableToParseInstanceName"))
578+
579+
host = host_pattern.replace("?", instance_name)
580+
if host.find(":") > -1:
581+
host, port = host.split(":")
567582

568583
host_info = HostInfo(
569-
host=host, port=port, role=role, availability=HostAvailability.AVAILABLE, weight=0, host_id=host_id)
584+
host=host, port=port, role=role, availability=HostAvailability.AVAILABLE, weight=0, host_id=id)
570585
host_info.add_alias(host)
571586
return host_info
572587

aws_advanced_python_wrapper/plugin_service.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ def __init__(
269269
self._target_func = target_func
270270
self._driver_dialect_manager = driver_dialect_manager
271271
self._driver_dialect = driver_dialect
272-
self._dialect = self._dialect_provider.get_dialect(driver_dialect.dialect_code, props)
272+
self._database_dialect = self._dialect_provider.get_dialect(driver_dialect.dialect_code, props)
273273

274274
@property
275275
def hosts(self) -> Tuple[HostInfo, ...]:
@@ -331,7 +331,7 @@ def driver_dialect(self) -> DriverDialect:
331331

332332
@property
333333
def database_dialect(self) -> DatabaseDialect:
334-
return self._dialect
334+
return self._database_dialect
335335

336336
@property
337337
def network_bound_methods(self) -> Set[str]:
@@ -362,16 +362,16 @@ def update_dialect(self, connection: Optional[Connection] = None):
362362
if connection is None:
363363
raise AwsWrapperError(Messages.get("PluginServiceImpl.UpdateDialectConnectionNone"))
364364

365-
original_dialect = self._dialect
366-
self._dialect = \
365+
original_dialect = self._database_dialect
366+
self._database_dialect = \
367367
self._dialect_provider.query_for_dialect(
368368
self._original_url,
369369
self._initial_connection_host_info,
370370
connection,
371371
self.driver_dialect)
372372

373-
if original_dialect != self._dialect:
374-
host_list_provider_init = self._dialect.get_host_list_provider_supplier()
373+
if original_dialect != self._database_dialect:
374+
host_list_provider_init = self._database_dialect.get_host_list_provider_supplier()
375375
self.host_list_provider = host_list_provider_init(self, self._props)
376376

377377
def update_driver_dialect(self, connection_provider: ConnectionProvider):

aws_advanced_python_wrapper/resources/aws_advanced_python_wrapper_messages.properties

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ AuroraHostListPlugin.ProviderAlreadySet=[AuroraHostListPlugin]Another dynamic ho
1919
AuroraPgDialect.HasExtensionsTrue=[AuroraPgDialect] has_extensions: True
2020
AuroraPgDialect.HasTopologyTrue=[AuroraPgDialect] has_topology: True
2121

22-
AuroraTestUtility.ClusterMemberNotFound=[AuroraTestUtility] Cannot find cluster member whose db instance identifier is '{}'.
23-
AuroraTestUtility.CreateDBInstanceFailed=[AuroraTestUtility] Could not create database instance `{}`.
24-
AuroraTestUtility.FailoverRequestNotSuccessful=[AuroraTestUtility] Failover cluster request was not successful.
25-
AuroraTestUtility.InstanceDescriptionTimeout=[AuroraTestUtility] Instance description timeout for {}. The instance did not reach status '{}' within {} minutes.
26-
AuroraTestUtility.InvalidDatabaseEngine=[AuroraTestUtility] The detected database engine is not valid: {}
27-
AuroraTestUtility.WriterInstanceNotFound=[AuroraTestUtility] Cannot find writer instance for cluster '{}'.
28-
2922
AwsSdk.UnsupportedRegion=[AwsSdk] Unsupported AWS region {}. For supported regions please read https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
3023

3124
AwsSecretsManagerPlugin.ConnectException=[AwsSecretsManagerPlugin] Error occurred while opening a connection: {}
@@ -136,6 +129,8 @@ MonitoringThreadContainer.SupplierMonitorNone=[MonitorThreadContainer] The monit
136129
MonitorService.EmptyAliasSet=[MonitorService] Empty alias set passed for '{}'. The alias set should not be empty.
137130
MonitorService.ErrorPopulatingAliases=[MonitorService] An error occurred while populating aliases: '{}'.
138131

132+
MultiAzHostListProvider.UnableToParseInstanceName=[MultiAzHostListProvider] The MultiAzHostListProvider was unable to parse the instance name from the endpoint returned by the topology query.
133+
139134
OpenedConnectionTracker.OpenedConnectionsTracked=[OpenedConnectionTracker] Opened Connections Tracked: {}
140135
OpenedConnectionTracker.InvalidatingConnections=[OpenedConnectionTracker] Invalidating opened connections to host: {}
141136
OpenedConnectionTracker.UnableToPopulateOpenedConnectionSet=[OpenedConnectionTracker] The driver is unable to track this opened connection because the instance endpoint is unknown.
@@ -181,6 +176,14 @@ RdsHostListProvider.UninitializedInitialHostInfo=[RdsHostListProvider] The drive
181176

182177
RdsPgDialect.RdsToolsAuroraUtils=[RdsPgDialect] rds_tools: {}, aurora_utils: {}
183178

179+
RdsTestUtility.ClusterMemberNotFound=[RdsTestUtility] Cannot find cluster member whose db instance identifier is '{}'.
180+
RdsTestUtility.CreateDBInstanceFailed=[RdsTestUtility] Could not create database instance `{}`.
181+
RdsTestUtility.FailoverRequestNotSuccessful=[RdsTestUtility] Failover cluster request was not successful.
182+
RdsTestUtility.InstanceDescriptionTimeout=[RdsTestUtility] Instance description timeout for {}. The instance did not reach status '{}' within {} minutes.
183+
RdsTestUtility.InvalidDatabaseEngine=[RdsTestUtility] The detected database engine is not valid: {}
184+
RdsTestUtility.MethodNotSupportedForDeployment=[RdsTestUtility] Method '{}' is not supported for the current database engine deployment: '{}'
185+
RdsTestUtility.WriterInstanceNotFound=[RdsTestUtility] Cannot find writer instance for cluster '{}'.
186+
184187
ReaderFailoverHandler.AttemptingReaderConnection=[ReaderFailoverHandler] Trying to connect to reader: '{}', with properties '{}'
185188
ReaderFailoverHandler.FailedReaderConnection=[ReaderFailoverHandler] Failed to connect to reader: '{}'
186189
ReaderFailoverHandler.InvalidTopology=[ReaderFailoverHandler] '{}' was called with an invalid (None or empty) topology.

aws_advanced_python_wrapper/utils/rdsutils.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,43 +59,43 @@ class RdsUtils:
5959
"""
6060

6161
AURORA_DNS_PATTERN = r"(?P<instance>.+)\." \
62-
r"(?P<dns>proxy-|cluster-|cluster-ro-|cluster-custom-)" \
63-
r"?(?P<domain>[a-zA-Z0-9]+\." \
64-
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)"
62+
r"(?P<dns>proxy-|cluster-|cluster-ro-|cluster-custom-)?" \
63+
r"(?P<domain>[a-zA-Z0-9]+\." \
64+
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)(?!\.cn$)"
6565
AURORA_INSTANCE_PATTERN = r"(?P<instance>.+)\." \
6666
r"(?P<domain>[a-zA-Z0-9]+\." \
67-
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)"
67+
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)(?!\.cn$)"
6868
AURORA_CLUSTER_PATTERN = r"(?P<instance>.+)\." \
6969
r"(?P<dns>cluster-|cluster-ro-)+" \
7070
r"(?P<domain>[a-zA-Z0-9]+\." \
71-
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)"
71+
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)(?!\.cn$)"
7272
AURORA_CUSTOM_CLUSTER_PATTERN = r"(?P<instance>.+)\." \
7373
r"(?P<dns>cluster-custom-)+" \
7474
r"(?P<domain>[a-zA-Z0-9]+\." \
75-
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)"
75+
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com)(?!\.cn$)"
7676
AURORA_PROXY_DNS_PATTERN = r"(?P<instance>.+)\." \
7777
r"(?P<dns>proxy-)+" \
7878
r"(?P<domain>[a-zA-Z0-9]+\." \
79-
r"(?P<region>[a-zA-Z0-9\\-]+)\.rds\.amazonaws\.com)"
79+
r"(?P<region>[a-zA-Z0-9\\-]+)\.rds\.amazonaws\.com)(?!\.cn$)"
8080
AURORA_CHINA_DNS_PATTERN = r"(?P<instance>.+)\." \
8181
r"(?P<dns>proxy-|cluster-|cluster-ro-|cluster-custom-)?" \
82-
r"(?P<domain>[a-zA-Z0-9]+\.rds\." \
83-
r"(?P<region>[a-zA-Z0-9\-]+)\.amazonaws\.com\.cn)"
82+
r"(?P<domain>[a-zA-Z0-9]+\." \
83+
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com\.cn)"
8484
AURORA_CHINA_INSTANCE_PATTERN = r"(?P<instance>.+)\." \
8585
r"(?P<domain>[a-zA-Z0-9]+\." \
8686
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com\.cn)"
8787
AURORA_CHINA_CLUSTER_PATTERN = r"(?P<instance>.+)\." \
8888
r"(?P<dns>cluster-|cluster-ro-)+" \
89-
r"(?P<domain>[a-zA-Z0-9]+\.rds\." \
90-
r"(?P<region>[a-zA-Z0-9\-]+)\.amazonaws\.com\.cn)"
89+
r"(?P<domain>[a-zA-Z0-9]+\." \
90+
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com\.cn)"
9191
AURORA_CHINA_CUSTOM_CLUSTER_PATTERN = r"(?P<instance>.+)\." \
9292
r"(?P<dns>cluster-custom-)+" \
93-
r"(?P<domain>[a-zA-Z0-9]+\.rds\." \
94-
r"(?P<region>[a-zA-Z0-9\-]+)\.amazonaws\.com\.cn)"
93+
r"(?P<domain>[a-zA-Z0-9]+\." \
94+
r"(?P<region>[a-zA-Z0-9\-]+)\.rds\.amazonaws\.com\.cn)"
9595
AURORA_CHINA_PROXY_DNS_PATTERN = r"(?P<instance>.+)\." \
9696
r"(?P<dns>proxy-)+" \
97-
r"(?P<domain>[a-zA-Z0-9]+\.rds\." \
98-
r"(?P<region>[a-zA-Z0-9\-])+\.amazonaws\.com\.cn)"
97+
r"(?P<domain>[a-zA-Z0-9]+\." \
98+
r"(?P<region>[a-zA-Z0-9\-])+\.rds\.amazonaws\.com\.cn)"
9999

100100
IP_V4 = r"^(([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){1}" \
101101
r"(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){2}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
@@ -104,6 +104,7 @@ class RdsUtils:
104104

105105
DNS_GROUP = "dns"
106106
DOMAIN_GROUP = "domain"
107+
INSTANCE_GROUP = "instance"
107108
REGION_GROUP = "region"
108109

109110
def is_rds_cluster_dns(self, host: str) -> bool:
@@ -170,6 +171,16 @@ def get_rds_cluster_host_url(self, host: str):
170171

171172
return None
172173

174+
def get_instance_id(self, host: str) -> Optional[str]:
175+
if not host or not host.strip():
176+
return None
177+
178+
match = self._find(host, [self.AURORA_INSTANCE_PATTERN, self.AURORA_CHINA_INSTANCE_PATTERN])
179+
if match:
180+
return match.group(self.INSTANCE_GROUP)
181+
182+
return None
183+
173184
def is_ipv4(self, host: str) -> bool:
174185
return self._contains(host, [self.IP_V4])
175186

0 commit comments

Comments
 (0)