Skip to content

Commit 6612a60

Browse files
authored
feat: GDB database dialects (#616)
1 parent 2882430 commit 6612a60

12 files changed

Lines changed: 274 additions & 48 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { HostInfo } from "../host_info";
18+
import { HostListProvider } from "../host_list_provider/host_list_provider";
19+
import { HostRole } from "../host_role";
20+
import { ClientWrapper } from "../client_wrapper";
21+
22+
export interface TopologyAwareDatabaseDialect {
23+
queryForTopology(client: ClientWrapper, hostListProvider: HostListProvider): Promise<HostInfo[]>;
24+
25+
identifyConnection(targetClient: ClientWrapper): Promise<string>;
26+
27+
getHostRole(client: ClientWrapper): Promise<HostRole>;
28+
29+
// Returns the host id of the targetClient if it is connected to a writer, null otherwise.
30+
getWriterId(targetClient: ClientWrapper): Promise<string | null>;
31+
}
32+
33+
export interface GlobalAuroraTopologyDialect extends TopologyAwareDatabaseDialect {
34+
getRegionByInstanceId(targetClient: ClientWrapper, instanceId: string): Promise<string | null>;
35+
}

common/lib/topology_aware_database_dialect.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

common/lib/utils/rds_url_type.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@
1515
*/
1616

1717
export class RdsUrlType {
18-
public static readonly IP_ADDRESS = new RdsUrlType(false, false);
19-
public static readonly RDS_WRITER_CLUSTER = new RdsUrlType(true, true);
20-
public static readonly RDS_READER_CLUSTER = new RdsUrlType(true, true);
21-
public static readonly RDS_CUSTOM_CLUSTER = new RdsUrlType(true, false);
22-
public static readonly RDS_PROXY = new RdsUrlType(true, false);
23-
public static readonly RDS_INSTANCE = new RdsUrlType(true, false);
24-
public static readonly RDS_AURORA_LIMITLESS_DB_SHARD_GROUP = new RdsUrlType(true, false);
25-
public static readonly OTHER = new RdsUrlType(false, false);
18+
public static readonly IP_ADDRESS = new RdsUrlType(false, false, false);
19+
public static readonly RDS_WRITER_CLUSTER = new RdsUrlType(true, true, true);
20+
public static readonly RDS_READER_CLUSTER = new RdsUrlType(true, true, true);
21+
public static readonly RDS_CUSTOM_CLUSTER = new RdsUrlType(true, false, true);
22+
public static readonly RDS_PROXY = new RdsUrlType(true, false, true);
23+
public static readonly RDS_INSTANCE = new RdsUrlType(true, false, true);
24+
public static readonly RDS_AURORA_LIMITLESS_DB_SHARD_GROUP = new RdsUrlType(true, false, true);
25+
public static readonly RDS_GLOBAL_WRITER_CLUSTER = new RdsUrlType(true, true, false);
26+
public static readonly OTHER = new RdsUrlType(false, false, false);
2627

2728
private constructor(
2829
public readonly isRds: boolean,
29-
public readonly isRdsCluster: boolean
30+
public readonly isRdsCluster: boolean,
31+
public readonly hasRegion: boolean
3032
) {}
3133
}

common/lib/utils/rds_utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export class RdsUtils {
6060
// https://aws.amazon.com/compliance/fips/#FIPS_Endpoints_by_Service
6161
// https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/Region.html
6262

63+
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.Aurora_Fea_Regions_DB-eng.Feature.GlobalDatabase.html
64+
private static readonly AURORA_GLOBAL_WRITER_DNS_PATTERN =
65+
/^(?<instance>.+)\.(?<dns>global-)?(?<domain>[a-zA-Z0-9]+\.global\.rds\.amazonaws\.com\.?)$/i;
66+
6367
private static readonly AURORA_DNS_PATTERN =
6468
/^(?<instance>.+)\.(?<dns>proxy-|cluster-|cluster-ro-|cluster-custom-|shardgrp-)?(?<domain>[a-zA-Z0-9]+\.(?<region>[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)$/i;
6569
private static readonly AURORA_INSTANCE_PATTERN = /^(?<instance>.+)\.(?<domain>[a-zA-Z0-9]+\.(?<region>[a-zA-Z0-9-]+)\.rds\.amazonaws\.com)$/i;
@@ -231,6 +235,11 @@ export class RdsUtils {
231235
return null;
232236
}
233237

238+
public isGlobalDbWriterClusterDns(host: string): boolean {
239+
const dnsGroup = this.getDnsGroup(host);
240+
return equalsIgnoreCase(dnsGroup, "global-");
241+
}
242+
234243
public isWriterClusterDns(host: string): boolean {
235244
const dnsGroup = this.getDnsGroup(host);
236245
return equalsIgnoreCase(dnsGroup, "cluster-");
@@ -300,6 +309,8 @@ export class RdsUtils {
300309

301310
if (this.isIPv4(host) || this.isIPv6(host)) {
302311
return RdsUrlType.IP_ADDRESS;
312+
} else if (this.isGlobalDbWriterClusterDns(host)) {
313+
return RdsUrlType.RDS_GLOBAL_WRITER_CLUSTER;
303314
} else if (this.isWriterClusterDns(host)) {
304315
return RdsUrlType.RDS_WRITER_CLUSTER;
305316
} else if (this.isReaderClusterDns(host)) {
@@ -382,7 +393,8 @@ export class RdsUtils {
382393
RdsUtils.AURORA_DNS_PATTERN,
383394
RdsUtils.AURORA_CHINA_DNS_PATTERN,
384395
RdsUtils.AURORA_OLD_CHINA_DNS_PATTERN,
385-
RdsUtils.AURORA_GOV_DNS_PATTERN
396+
RdsUtils.AURORA_GOV_DNS_PATTERN,
397+
RdsUtils.AURORA_GLOBAL_WRITER_DNS_PATTERN
386398
);
387399
return this.getRegexGroup(matcher, RdsUtils.DNS_GROUP);
388400
}

common/lib/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { WrapperProperties } from "../wrapper_property";
2020
import { HostRole } from "../host_role";
2121
import { logger } from "../../logutils";
2222
import { AwsWrapperError, InternalQueryTimeoutError } from "./errors";
23-
import { TopologyAwareDatabaseDialect } from "../topology_aware_database_dialect";
23+
import { TopologyAwareDatabaseDialect } from "../database_dialect/topology_aware_database_dialect";
2424

2525
export function sleep(ms: number) {
2626
return new Promise((resolve) => setTimeout(resolve, ms));

docs/using-the-nodejs-wrapper/DatabaseDialects.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Dialect codes specify what kind of database any connections will be made to.
3333

3434
If you are interested in using the AWS Advanced NodeJS Wrapper but your desired database type is not currently supported, it is possible to create a custom dialect.
3535

36-
To create a custom dialect, implement the [`DatabaseDialect`](../../common/lib/database_dialect/database_dialect.ts) interface. For databases clusters that are aware of their topology, the [`TopologyAwareDatabaseDialect`](../../common/lib/topology_aware_database_dialect.ts) interface should also be implemented. For database clusters that use an [Aurora Limitless Database](../../docs/using-the-nodejs-wrapper/using-plugins/UsingTheLimitlessConnectionPlugin.md#what-is-amazon-aurora-limitless-database) then [`LimitlessDatabaseDialect`](../../common/lib/database_dialect/limitless_database_dialect.ts) should be implemented.
36+
To create a custom dialect, implement the [`DatabaseDialect`](../../common/lib/database_dialect/database_dialect.ts) interface. For databases clusters that are aware of their topology, the [`TopologyAwareDatabaseDialect`](../../common/lib/database_dialect/topology_aware_database_dialect.ts) interface should also be implemented. For database clusters that use an [Aurora Limitless Database](../../docs/using-the-nodejs-wrapper/using-plugins/UsingTheLimitlessConnectionPlugin.md#what-is-amazon-aurora-limitless-database) then [`LimitlessDatabaseDialect`](../../common/lib/database_dialect/limitless_database_dialect.ts) should be implemented.
3737

3838
See the following classes for examples:
3939

mysql/lib/dialect/aurora_mysql_database_dialect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { HostListProviderService } from "../../../common/lib/host_list_provider_
1919
import { HostListProvider } from "../../../common/lib/host_list_provider/host_list_provider";
2020
import { RdsHostListProvider } from "../../../common/lib/host_list_provider/rds_host_list_provider";
2121
import { HostInfo } from "../../../common/lib/host_info";
22-
import { TopologyAwareDatabaseDialect } from "../../../common/lib/topology_aware_database_dialect";
22+
import { TopologyAwareDatabaseDialect } from "../../../common/lib/database_dialect/topology_aware_database_dialect";
2323
import { HostRole } from "../../../common/lib/host_role";
2424
import { ClientWrapper } from "../../../common/lib/client_wrapper";
2525
import { DatabaseDialectCodes } from "../../../common/lib/database_dialect/database_dialect_codes";
@@ -87,7 +87,7 @@ export class AuroraMySQLDatabaseDialect extends MySQLDatabaseDialect implements
8787
try {
8888
const writerId: string = res[0][0]["server_id"];
8989
return writerId ? writerId : null;
90-
} catch (e) {
90+
} catch (e: any) {
9191
if (e.message.includes("Cannot read properties of undefined")) {
9292
// Query returned no result, targetClient is not connected to a writer.
9393
return null;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { AuroraMySQLDatabaseDialect } from "./aurora_mysql_database_dialect";
17+
import { GlobalAuroraTopologyDialect } from "../../../common/lib/database_dialect/topology_aware_database_dialect";
18+
import { ClientWrapper } from "../../../common/lib/client_wrapper";
19+
import { HostInfo } from "../../../common/lib";
20+
import { HostListProvider } from "../../../common/lib/host_list_provider/host_list_provider";
21+
22+
export class GlobalAuroraMySQLDatabaseDialect extends AuroraMySQLDatabaseDialect implements GlobalAuroraTopologyDialect {
23+
private static readonly GLOBAL_STATUS_TABLE_EXISTS_QUERY =
24+
"SELECT 1 AS tmp FROM information_schema.tables WHERE" +
25+
" upper(table_schema) = 'INFORMATION_SCHEMA' AND upper(table_name) = 'AURORA_GLOBAL_DB_STATUS'";
26+
27+
private static readonly GLOBAL_INSTANCE_STATUS_EXISTS_QUERY =
28+
"SELECT 1 AS tmp FROM information_schema.tables WHERE" +
29+
" upper(table_schema) = 'INFORMATION_SCHEMA' AND upper(table_name) = 'AURORA_GLOBAL_DB_INSTANCE_STATUS'";
30+
31+
private static readonly GLOBAL_TOPOLOGY_QUERY =
32+
"SELECT SERVER_ID, CASE WHEN SESSION_ID = 'MASTER_SESSION_ID' THEN TRUE ELSE FALSE END AS IS_WRITER, " +
33+
"VISIBILITY_LAG_IN_MSEC, AWS_REGION " +
34+
"FROM information_schema.aurora_global_db_instance_status";
35+
36+
private static readonly REGION_COUNT_QUERY = "SELECT count(1) FROM information_schema.aurora_global_db_status";
37+
38+
private static readonly REGION_BY_INSTANCE_ID_QUERY =
39+
"SELECT AWS_REGION FROM information_schema.aurora_global_db_instance_status WHERE SERVER_ID = ?";
40+
41+
async isDialect(targetClient: ClientWrapper): Promise<boolean> {
42+
try {
43+
// Check if both global status tables exist
44+
const [statusRows] = await targetClient.query(GlobalAuroraMySQLDatabaseDialect.GLOBAL_STATUS_TABLE_EXISTS_QUERY);
45+
if (!statusRows?.[0]) {
46+
return false;
47+
}
48+
49+
const [instanceStatusRows] = await targetClient.query(GlobalAuroraMySQLDatabaseDialect.GLOBAL_INSTANCE_STATUS_EXISTS_QUERY);
50+
if (!instanceStatusRows?.[0]) {
51+
return false;
52+
}
53+
54+
// Check if there are multiple regions
55+
const [regionCountRows] = await targetClient.query(GlobalAuroraMySQLDatabaseDialect.REGION_COUNT_QUERY);
56+
if (!regionCountRows?.[0]) {
57+
return false;
58+
}
59+
60+
const awsRegionCount = regionCountRows[0]["count(1)"];
61+
return awsRegionCount > 1;
62+
} catch {
63+
return false;
64+
}
65+
}
66+
67+
getDialectUpdateCandidates(): string[] {
68+
return [];
69+
}
70+
71+
async queryForTopology(targetClient: ClientWrapper, hostListProvider: HostListProvider): Promise<HostInfo[]> {
72+
const res = await targetClient.query(GlobalAuroraMySQLDatabaseDialect.GLOBAL_TOPOLOGY_QUERY);
73+
const hosts: HostInfo[] = [];
74+
const rows: any[] = res[0];
75+
rows.forEach((row) => {
76+
// According to the topology query the result set
77+
// should contain 4 columns: node ID, 1/0 (writer/reader), CPU utilization, node lag in time.
78+
const hostName: string = row["server_id"];
79+
const isWriter: boolean = row["is_writer"];
80+
const hostLag: number = row["visibility_lag_in_msec"] ?? 0; // visibility_lag_in_msec is nullable.
81+
const awsRegion: string = row["aws_region"]; // TODO: update this after topologyUtils PR is merged.
82+
const host: HostInfo = hostListProvider.createHost(hostName, isWriter, Math.round(hostLag) * 100, Date.now() /* TODO: update this after topologyUtils PR is merged */);
83+
hosts.push(host);
84+
});
85+
return hosts;
86+
}
87+
88+
async getRegionByInstanceId(targetClient: ClientWrapper, instanceId: string): Promise<string | null> {
89+
try {
90+
const [rows] = await targetClient.query(GlobalAuroraMySQLDatabaseDialect.REGION_BY_INSTANCE_ID_QUERY, [instanceId]);
91+
if (!rows?.[0]) {
92+
return null;
93+
}
94+
return rows[0]["AWS_REGION"] ?? null;
95+
} catch {
96+
return null;
97+
}
98+
}
99+
}

mysql/lib/dialect/rds_multi_az_mysql_database_dialect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { HostRole } from "../../../common/lib/host_role";
2323
import { Messages } from "../../../common/lib/utils/messages";
2424
import { logger } from "../../../common/logutils";
2525
import { AwsWrapperError } from "../../../common/lib/utils/errors";
26-
import { TopologyAwareDatabaseDialect } from "../../../common/lib/topology_aware_database_dialect";
26+
import { TopologyAwareDatabaseDialect } from "../../../common/lib/database_dialect/topology_aware_database_dialect";
2727
import { RdsHostListProvider } from "../../../common/lib/host_list_provider/rds_host_list_provider";
2828
import { FailoverRestriction } from "../../../common/lib/plugins/failover/failover_restriction";
2929
import { WrapperProperties } from "../../../common/lib/wrapper_property";

pg/lib/dialect/aurora_pg_database_dialect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { PgDatabaseDialect } from "./pg_database_dialect";
1818
import { HostListProviderService } from "../../../common/lib/host_list_provider_service";
1919
import { HostListProvider } from "../../../common/lib/host_list_provider/host_list_provider";
2020
import { RdsHostListProvider } from "../../../common/lib/host_list_provider/rds_host_list_provider";
21-
import { TopologyAwareDatabaseDialect } from "../../../common/lib/topology_aware_database_dialect";
21+
import { TopologyAwareDatabaseDialect } from "../../../common/lib/database_dialect/topology_aware_database_dialect";
2222
import { HostInfo, HostRole } from "../../../common/lib";
2323
import { ClientWrapper } from "../../../common/lib/client_wrapper";
2424
import { DatabaseDialectCodes } from "../../../common/lib/database_dialect/database_dialect_codes";

0 commit comments

Comments
 (0)