Skip to content

Commit 14c1bb3

Browse files
committed
feat: gdb failover support
1 parent 00d8176 commit 14c1bb3

46 files changed

Lines changed: 1798 additions & 1072 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/integration_tests.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ on:
55
push:
66
branches:
77
- main
8-
- refactor/monitor-service
98
paths-ignore:
109
- "**/*.md"
1110
- "**/*.jpg"

common/lib/connection_plugin_chain_builder.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { CustomEndpointPluginFactory } from "./plugins/custom_endpoint/custom_en
4343
import { ConfigurationProfile } from "./profile/configuration_profile";
4444
import { HostMonitoring2PluginFactory } from "./plugins/efm2/host_monitoring2_plugin_factory";
4545
import { BlueGreenPluginFactory } from "./plugins/bluegreen/blue_green_plugin_factory";
46+
import { GlobalDbFailoverPluginFactory } from "./plugins/gdb_failover/global_db_failover_plugin_factory";
4647
import { FullServicesContainer } from "./utils/full_services_container";
4748

4849
/*
@@ -66,6 +67,7 @@ export class ConnectionPluginChainBuilder {
6667
["readWriteSplitting", { factory: ReadWriteSplittingPluginFactory, weight: 600 }],
6768
["failover", { factory: FailoverPluginFactory, weight: 700 }],
6869
["failover2", { factory: Failover2PluginFactory, weight: 710 }],
70+
["gdbFailover", { factory: GlobalDbFailoverPluginFactory, weight: 720 }],
6971
["efm", { factory: HostMonitoringPluginFactory, weight: 800 }],
7072
["efm2", { factory: HostMonitoring2PluginFactory, weight: 810 }],
7173
["fastestResponseStrategy", { factory: FastestResponseStrategyPluginFactory, weight: 900 }],
@@ -87,6 +89,7 @@ export class ConnectionPluginChainBuilder {
8789
[ReadWriteSplittingPluginFactory, 600],
8890
[FailoverPluginFactory, 700],
8991
[Failover2PluginFactory, 710],
92+
[GlobalDbFailoverPluginFactory, 720],
9093
[HostMonitoringPluginFactory, 800],
9194
[HostMonitoring2PluginFactory, 810],
9295
[LimitlessConnectionPluginFactory, 950],

common/lib/database_dialect/database_dialect_codes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
*/
1616

1717
export class DatabaseDialectCodes {
18+
static readonly GLOBAL_AURORA_MYSQL: string = "global-aurora-mysql";
1819
static readonly AURORA_MYSQL: string = "aurora-mysql";
1920
static readonly RDS_MYSQL: string = "rds-mysql";
2021
static readonly MYSQL: string = "mysql";
2122
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html
2223
static readonly RDS_MULTI_AZ_MYSQL: string = "rds-multi-az-mysql";
24+
static readonly GLOBAL_AURORA_PG: string = "global-aurora-pg";
2325
static readonly AURORA_PG: string = "aurora-pg";
2426
static readonly RDS_PG: string = "rds-pg";
2527
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html

common/lib/database_dialect/database_dialect_manager.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ export class DatabaseDialectManager implements DatabaseDialectProvider {
9595

9696
if (this.dbType === DatabaseType.MYSQL) {
9797
const type = this.rdsHelper.identifyRdsType(host);
98+
if (type == RdsUrlType.RDS_GLOBAL_WRITER_CLUSTER) {
99+
this.canUpdate = false;
100+
this.dialectCode = DatabaseDialectCodes.GLOBAL_AURORA_MYSQL;
101+
this.dialect = <DatabaseDialect>this.knownDialectsByCode.get(DatabaseDialectCodes.GLOBAL_AURORA_MYSQL);
102+
this.logCurrentDialect();
103+
return this.dialect;
104+
}
105+
98106
if (type.isRdsCluster) {
99107
this.canUpdate = true;
100108
this.dialectCode = DatabaseDialectCodes.AURORA_MYSQL;
@@ -128,6 +136,14 @@ export class DatabaseDialectManager implements DatabaseDialectProvider {
128136
return this.dialect;
129137
}
130138

139+
if (type == RdsUrlType.RDS_GLOBAL_WRITER_CLUSTER) {
140+
this.canUpdate = false;
141+
this.dialectCode = DatabaseDialectCodes.GLOBAL_AURORA_PG;
142+
this.dialect = <DatabaseDialect>this.knownDialectsByCode.get(DatabaseDialectCodes.GLOBAL_AURORA_PG);
143+
this.logCurrentDialect();
144+
return this.dialect;
145+
}
146+
131147
if (type.isRdsCluster) {
132148
this.canUpdate = true;
133149
this.dialectCode = DatabaseDialectCodes.AURORA_PG;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 { TopologyQueryResult, TopologyUtils } from "./topology_utils";
18+
import { ClientWrapper } from "../client_wrapper";
19+
import { DatabaseDialect } from "../database_dialect/database_dialect";
20+
import { HostInfo } from "../host_info";
21+
import { isDialectTopologyAware } from "../utils/utils";
22+
import { Messages } from "../utils/messages";
23+
24+
/**
25+
* TopologyUtils implementation for Aurora clusters using a single HostInfo template.
26+
*/
27+
export class AuroraTopologyUtils extends TopologyUtils {
28+
async queryForTopology(
29+
targetClient: ClientWrapper,
30+
dialect: DatabaseDialect,
31+
initialHost: HostInfo,
32+
clusterInstanceTemplate: HostInfo
33+
): Promise<HostInfo[]> {
34+
if (!isDialectTopologyAware(dialect)) {
35+
throw new TypeError(Messages.get("RdsHostListProvider.incorrectDialect"));
36+
}
37+
38+
return await dialect
39+
.queryForTopology(targetClient)
40+
.then((res: TopologyQueryResult[]) => this.verifyWriter(this.createHosts(res, initialHost, clusterInstanceTemplate)));
41+
}
42+
43+
public createHosts(topologyQueryResults: TopologyQueryResult[], initialHost: HostInfo, clusterInstanceTemplate: HostInfo): HostInfo[] {
44+
const hostsMap = new Map<string, HostInfo>();
45+
topologyQueryResults.forEach((row) => {
46+
const lastUpdateTime = row.lastUpdateTime ?? Date.now();
47+
48+
const host = this.createHost(
49+
row.id,
50+
row.host,
51+
row.isWriter,
52+
row.weight,
53+
lastUpdateTime,
54+
initialHost,
55+
clusterInstanceTemplate,
56+
row.endpoint,
57+
row.port
58+
);
59+
60+
const existing = hostsMap.get(host.host);
61+
if (!existing || existing.lastUpdateTime < host.lastUpdateTime) {
62+
hostsMap.set(host.host, host);
63+
}
64+
});
65+
66+
return Array.from(hostsMap.values());
67+
}
68+
}

common/lib/host_list_provider/connection_string_host_list_provider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,8 @@ export class ConnectionStringHostListProvider implements StaticHostListProvider
112112
getClusterId(): string {
113113
throw new AwsWrapperError("ConnectionStringHostListProvider does not support getClusterId.");
114114
}
115+
116+
forceMonitoringRefresh(shouldVerifyWriter: boolean, timeoutMs: number): Promise<HostInfo[]> {
117+
throw new AwsWrapperError("ConnectionStringHostListProvider does not support forceMonitoringRefresh.");
118+
}
115119
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 { RdsHostListProvider } from "./rds_host_list_provider";
18+
import { FullServicesContainer } from "../utils/full_services_container";
19+
import { HostInfo } from "../host_info";
20+
import { WrapperProperties } from "../wrapper_property";
21+
import { ClusterTopologyMonitor, ClusterTopologyMonitorImpl } from "./monitoring/cluster_topology_monitor";
22+
import { GlobalAuroraTopologyMonitor } from "./monitoring/global_aurora_topology_monitor";
23+
import { MonitorInitializer } from "../utils/monitoring/monitor";
24+
import { ClientWrapper } from "../client_wrapper";
25+
import { DatabaseDialect } from "../database_dialect/database_dialect";
26+
import { parseInstanceTemplates } from "../utils/utils";
27+
28+
export class GlobalAuroraHostListProvider extends RdsHostListProvider {
29+
protected instanceTemplatesByRegion: Map<string, HostInfo>;
30+
protected override initSettings(): void {
31+
super.initSettings();
32+
33+
const instanceTemplates = WrapperProperties.GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS.get(this.properties);
34+
this.instanceTemplatesByRegion = parseInstanceTemplates(
35+
instanceTemplates,
36+
(hostPattern: string) => this.validateHostPatternSetting(hostPattern),
37+
() => this.hostListProviderService.getHostInfoBuilder()
38+
);
39+
}
40+
41+
protected override async getOrCreateMonitor(): Promise<ClusterTopologyMonitor> {
42+
const initializer: MonitorInitializer = {
43+
createMonitor: (servicesContainer: FullServicesContainer): ClusterTopologyMonitor => {
44+
return new GlobalAuroraTopologyMonitor(
45+
servicesContainer,
46+
this.topologyUtils,
47+
this.clusterId,
48+
this.initialHost,
49+
this.properties,
50+
this.clusterInstanceTemplate,
51+
this.refreshRateNano,
52+
this.highRefreshRateNano,
53+
this.instanceTemplatesByRegion
54+
);
55+
}
56+
};
57+
58+
return await this.servicesContainers
59+
.getMonitorService()
60+
.runIfAbsent(ClusterTopologyMonitorImpl, this.clusterId, this.servicesContainers, this.properties, initializer);
61+
}
62+
63+
override async getCurrentTopology(targetClient: ClientWrapper, dialect: DatabaseDialect): Promise<HostInfo[]> {
64+
this.init();
65+
return await this.topologyUtils.queryForTopology(targetClient, dialect, this.initialHost, this.instanceTemplatesByRegion);
66+
}
67+
}

common/lib/host_list_provider/global_topology_utils.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,36 @@ import { isDialectTopologyAware } from "../utils/utils";
2222
import { Messages } from "../utils/messages";
2323
import { AwsWrapperError } from "../utils/errors";
2424

25-
export class GlobalTopologyUtils extends TopologyUtils {
26-
async queryForTopology(
27-
targetClient: ClientWrapper,
28-
dialect: DatabaseDialect,
29-
initialHost: HostInfo,
30-
clusterInstanceTemplate: HostInfo
31-
): Promise<HostInfo[]> {
32-
throw new AwsWrapperError("Not implemented");
33-
}
25+
export interface GdbTopologyUtils {
26+
getRegion(instanceId: string, targetClient: ClientWrapper, dialect: DatabaseDialect): Promise<string | null>;
27+
}
3428

35-
async queryForTopologyWithRegion(
29+
export class GlobalTopologyUtils extends TopologyUtils implements GdbTopologyUtils {
30+
async queryForTopology(
3631
targetClient: ClientWrapper,
3732
dialect: DatabaseDialect,
3833
initialHost: HostInfo,
3934
instanceTemplateByRegion: Map<string, HostInfo>
4035
): Promise<HostInfo[]> {
4136
if (!isDialectTopologyAware(dialect)) {
42-
throw new TypeError(Messages.get("RdsHostListProvider.incorrectDialect"));
37+
throw new AwsWrapperError(Messages.get("RdsHostListProvider.incorrectDialect"));
4338
}
4439

4540
return await dialect
4641
.queryForTopology(targetClient)
4742
.then((res: TopologyQueryResult[]) => this.verifyWriter(this.createHostsWithTemplateMap(res, initialHost, instanceTemplateByRegion)));
4843
}
4944

45+
async getRegion(instanceId: string, targetClient: ClientWrapper, dialect: DatabaseDialect): Promise<string | null> {
46+
if (!isDialectTopologyAware(dialect)) {
47+
throw new AwsWrapperError(Messages.get("RdsHostListProvider.incorrectDialect"));
48+
}
49+
50+
const results = await dialect.queryForTopology(targetClient);
51+
const match = results.find((row) => row.id === instanceId);
52+
return match?.awsRegion ?? null;
53+
}
54+
5055
private createHostsWithTemplateMap(
5156
topologyQueryResults: TopologyQueryResult[],
5257
initialHost: HostInfo,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 { ClusterTopologyMonitorImpl } from "./cluster_topology_monitor";
18+
import { GdbTopologyUtils, GlobalTopologyUtils } from "../global_topology_utils";
19+
import { FullServicesContainer } from "../../utils/full_services_container";
20+
import { HostInfo } from "../../host_info";
21+
import { ClientWrapper } from "../../client_wrapper";
22+
import { AwsWrapperError } from "../../utils/errors";
23+
import { Messages } from "../../utils/messages";
24+
import { TopologyUtils } from "../topology_utils";
25+
26+
function isGdbTopologyUtils(utils: TopologyUtils): utils is TopologyUtils & GdbTopologyUtils {
27+
return "getRegion" in utils && typeof (utils as unknown as GdbTopologyUtils).getRegion === "function";
28+
}
29+
30+
export class GlobalAuroraTopologyMonitor extends ClusterTopologyMonitorImpl {
31+
protected readonly instanceTemplatesByRegion: Map<string, HostInfo>;
32+
declare public readonly topologyUtils: TopologyUtils;
33+
34+
constructor(
35+
servicesContainer: FullServicesContainer,
36+
topologyUtils: TopologyUtils,
37+
clusterId: string,
38+
initialHostInfo: HostInfo,
39+
properties: Map<string, any>,
40+
instanceTemplate: HostInfo,
41+
refreshRateNano: number,
42+
highRefreshRateNano: number,
43+
instanceTemplatesByRegion: Map<string, HostInfo>
44+
) {
45+
super(servicesContainer, topologyUtils, clusterId, initialHostInfo, properties, instanceTemplate, refreshRateNano, highRefreshRateNano);
46+
47+
this.instanceTemplatesByRegion = instanceTemplatesByRegion;
48+
this.topologyUtils = topologyUtils;
49+
}
50+
51+
protected override async getInstanceTemplate(hostId: string, targetClient: ClientWrapper): Promise<HostInfo> {
52+
if (!isGdbTopologyUtils(this.topologyUtils)) {
53+
throw new AwsWrapperError(Messages.get("GlobalAuroraTopologyMonitor.invalidTopologyUtils"));
54+
}
55+
56+
const dialect = this.hostListProviderService.getDialect();
57+
const region = await this.topologyUtils.getRegion(hostId, targetClient, dialect);
58+
59+
if (region) {
60+
const instanceTemplate = this.instanceTemplatesByRegion.get(region);
61+
if (!instanceTemplate) {
62+
throw new AwsWrapperError(Messages.get("GlobalAuroraTopologyMonitor.cannotFindRegionTemplate", region));
63+
}
64+
return instanceTemplate;
65+
}
66+
67+
return this.instanceTemplate;
68+
}
69+
}

common/lib/host_list_provider/rds_host_list_provider.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,9 @@ export class RdsHostListProvider implements DynamicHostListProvider {
257257
return topology == null ? null : topology.hosts;
258258
}
259259

260-
static clearAll(): void {
261-
// No-op
262-
// TODO: remove if still not used after full service container refactoring
263-
}
264-
265260
clear(): void {
266261
if (this.clusterId) {
267-
CoreServicesContainer.getInstance().getStorageService().remove(Topology, this.clusterId);
262+
this.servicesContainers.getStorageService().remove(Topology, this.clusterId);
268263
}
269264
}
270265

0 commit comments

Comments
 (0)