From de43e508dd147ab4b74f71f7ed952f88acbee162 Mon Sep 17 00:00:00 2001 From: Karen Chen <64801825+karenc-bq@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:57:59 -0700 Subject: [PATCH] feat: gdb rw splitting plugin --- common/lib/connection_plugin_chain_builder.ts | 2 + ...gdb_read_write_splitting_plugin_factory.ts | 36 +++++++ .../gdb_read_writer_splitting_plugin.ts | 100 ++++++++++++++++++ .../read_write_splitting_plugin_factory.ts | 6 +- common/lib/utils/errors.ts | 2 + common/lib/utils/messages.ts | 8 +- common/lib/wrapper_property.ts | 18 ++++ 7 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 common/lib/plugins/read_write_splitting/gdb_read_write_splitting_plugin_factory.ts create mode 100644 common/lib/plugins/read_write_splitting/gdb_read_writer_splitting_plugin.ts diff --git a/common/lib/connection_plugin_chain_builder.ts b/common/lib/connection_plugin_chain_builder.ts index 9c399dd7c..215a95673 100644 --- a/common/lib/connection_plugin_chain_builder.ts +++ b/common/lib/connection_plugin_chain_builder.ts @@ -43,6 +43,7 @@ import { CustomEndpointPluginFactory } from "./plugins/custom_endpoint/custom_en import { ConfigurationProfile } from "./profile/configuration_profile"; import { HostMonitoring2PluginFactory } from "./plugins/efm2/host_monitoring2_plugin_factory"; import { BlueGreenPluginFactory } from "./plugins/bluegreen/blue_green_plugin_factory"; +import { GdbReadWriteSplittingPluginFactory } from "./plugins/read_write_splitting/gdb_read_write_splitting_plugin_factory"; /* Type alias used for plugin factory sorting. It holds a reference to a plugin @@ -63,6 +64,7 @@ export class ConnectionPluginChainBuilder { ["staleDns", { factory: StaleDnsPluginFactory, weight: 500 }], ["bg", { factory: BlueGreenPluginFactory, weight: 550 }], ["readWriteSplitting", { factory: ReadWriteSplittingPluginFactory, weight: 600 }], + ["gdbReadWriteSplitting", { factory: GdbReadWriteSplittingPluginFactory, weight: 610 }], ["failover", { factory: FailoverPluginFactory, weight: 700 }], ["failover2", { factory: Failover2PluginFactory, weight: 710 }], ["efm", { factory: HostMonitoringPluginFactory, weight: 800 }], diff --git a/common/lib/plugins/read_write_splitting/gdb_read_write_splitting_plugin_factory.ts b/common/lib/plugins/read_write_splitting/gdb_read_write_splitting_plugin_factory.ts new file mode 100644 index 000000000..a37c48feb --- /dev/null +++ b/common/lib/plugins/read_write_splitting/gdb_read_write_splitting_plugin_factory.ts @@ -0,0 +1,36 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ConnectionPluginFactory } from "../../plugin_factory"; +import { PluginService } from "../../plugin_service"; +import { ConnectionPlugin } from "../../connection_plugin"; +import { AwsWrapperError } from "../../utils/errors"; +import { Messages } from "../../utils/messages"; + +export class GdbReadWriteSplittingPluginFactory extends ConnectionPluginFactory { + private static gdbReadWriteSplittingPlugin: any; + + async getInstance(pluginService: PluginService, properties: Map): Promise { + try { + if (!GdbReadWriteSplittingPluginFactory.gdbReadWriteSplittingPlugin) { + GdbReadWriteSplittingPluginFactory.gdbReadWriteSplittingPlugin = await import("./gdb_read_writer_splitting_plugin"); + } + return new GdbReadWriteSplittingPluginFactory.gdbReadWriteSplittingPlugin.GdbReadWriteSplittingPlugin(pluginService, properties); + } catch (error: any) { + throw new AwsWrapperError(Messages.get("ConnectionPluginChainBuilder.errorImportingPlugin", error.message, "gdbReadWriteSplittingPlugin")); + } + } +} diff --git a/common/lib/plugins/read_write_splitting/gdb_read_writer_splitting_plugin.ts b/common/lib/plugins/read_write_splitting/gdb_read_writer_splitting_plugin.ts new file mode 100644 index 000000000..2906b1a0a --- /dev/null +++ b/common/lib/plugins/read_write_splitting/gdb_read_writer_splitting_plugin.ts @@ -0,0 +1,100 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ReadWriteSplittingPlugin } from "./read_write_splitting_plugin"; +import { PluginService } from "../../plugin_service"; +import { WrapperProperties } from "../../wrapper_property"; +import { HostInfo } from "../../host_info"; +import { RdsUtils } from "../../utils/rds_utils"; +import { ReadWriteSplittingError } from "../../utils/errors"; +import { Messages } from "../../utils/messages"; +import { logger } from "../../../logutils"; +import { ClientWrapper } from "../../client_wrapper"; + +export class GdbReadWriteSplittingPlugin extends ReadWriteSplittingPlugin { + protected readonly rdsUtils: RdsUtils = new RdsUtils(); + + protected readonly restrictWriterToHomeRegion: boolean; + protected readonly restrictReaderToHomeRegion: boolean; + + protected isInitialized: boolean = false; + protected homeRegion: string; + + constructor(pluginService: PluginService, properties: Map) { + super(pluginService, properties); + this.restrictWriterToHomeRegion = WrapperProperties.GDB_RW_RESTRICT_WRITER_TO_HOME_REGION.get(properties); + this.restrictReaderToHomeRegion = WrapperProperties.GDB_RW_RESTRICT_READER_TO_HOME_REGION.get(properties); + } + + protected initSettings(initHostInfo: HostInfo, properties: Map): void { + if (this.isInitialized) { + return; + } + + this.isInitialized = true; + + this.homeRegion = WrapperProperties.GDB_RW_HOME_REGION.get(properties); + if (!this.homeRegion) { + const rdsUrlType = this.rdsUtils.identifyRdsType(initHostInfo.host); + if (rdsUrlType.hasRegion) { + this.homeRegion = this.rdsUtils.getRdsRegion(initHostInfo.host); + } + } + + if (!this.homeRegion) { + throw new ReadWriteSplittingError(Messages.get("GdbReadWriteSplittingPlugin.missingHomeRegion", initHostInfo.host)); + } + + logger.debug(Messages.get("GdbReadWriteSplittingPlugin.parameterValue", "gdbRwHomeRegion", this.homeRegion)); + } + + override async connect( + hostInfo: HostInfo, + props: Map, + isInitialConnection: boolean, + connectFunc: () => Promise + ): Promise { + this.initSettings(hostInfo, props); + return super.connect(hostInfo, props, isInitialConnection, connectFunc); + } + + override setWriterClient(writerTargetClient: ClientWrapper | undefined, writerHostInfo: HostInfo) { + if ( + this.restrictWriterToHomeRegion && + this.writerHostInfo != null && + this.homeRegion?.toLowerCase() !== this.rdsUtils.getRdsRegion(this.writerHostInfo.host)?.toLowerCase() + ) { + throw new ReadWriteSplittingError( + Messages.get("GdbReadWriteSplittingPlugin.cantConnectWriterOutOfHomeRegion", writerHostInfo.host, this.homeRegion) + ); + } + super.setWriterClient(writerTargetClient, writerHostInfo); + } + + protected getReaderHostCandidates(): HostInfo[] { + if (this.restrictReaderToHomeRegion) { + const hostsInRegion: HostInfo[] = this.pluginService + .getHosts() + .filter((x) => this.rdsUtils.getRdsRegion(x.host)?.toLowerCase() === this.homeRegion?.toLowerCase()); + + if (hostsInRegion.length === 0) { + throw new ReadWriteSplittingError(Messages.get("GdbReadWriteSplittingPlugin.noAvailableReadersInHomeRegion", this.homeRegion)); + } + return hostsInRegion; + } + return super.getReaderHostCandidates(); + } +} diff --git a/common/lib/plugins/read_write_splitting/read_write_splitting_plugin_factory.ts b/common/lib/plugins/read_write_splitting/read_write_splitting_plugin_factory.ts index 62485db1e..8b63f78cf 100644 --- a/common/lib/plugins/read_write_splitting/read_write_splitting_plugin_factory.ts +++ b/common/lib/plugins/read_write_splitting/read_write_splitting_plugin_factory.ts @@ -1,12 +1,12 @@ /* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - + Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/common/lib/utils/errors.ts b/common/lib/utils/errors.ts index 4d854c0c5..299804175 100644 --- a/common/lib/utils/errors.ts +++ b/common/lib/utils/errors.ts @@ -48,6 +48,8 @@ export class FailoverFailedError extends FailoverError {} export class TransactionResolutionUnknownError extends FailoverError {} +export class ReadWriteSplittingError extends AwsWrapperError {} + export class LoginError extends AwsWrapperError {} export class InternalQueryTimeoutError extends AwsWrapperError {} diff --git a/common/lib/utils/messages.ts b/common/lib/utils/messages.ts index 1f46e6249..ecc0f7256 100644 --- a/common/lib/utils/messages.ts +++ b/common/lib/utils/messages.ts @@ -384,7 +384,13 @@ const MESSAGES: Record = { "TopologyUtils.instanceIdRequired": "InstanceId must not be en empty string.", "TopologyUtils.errorGettingHostRole": "An error occurred while trying to get the host role.", "GlobalTopologyUtils.missingRegion": "Host '%s' is missing region information in the topology query result.", - "GlobalTopologyUtils.missingTemplateForRegion": "No cluster instance template found for region '%s' when processing host '%s'." + "GlobalTopologyUtils.missingTemplateForRegion": "No cluster instance template found for region '%s' when processing host '%s'.", + "GdbReadWriteSplittingPlugin.missingHomeRegion": + "Unable to parse home region from endpoint '%s'. Please ensure you have set the 'gdbRwHomeRegion' connection parameter.", + "GdbReadWriteSplittingPlugin.cantConnectWriterOutOfHomeRegion": + "Writer connection to '%s' is not allowed since it is out of home region '%s'.", + "GdbReadWriteSplittingPlugin.noAvailableReadersInHomeRegion": "No available reader nodes in home region '%s'.", + "GdbReadWriteSplittingPlugin.parameterValue": "%s=%s" }; export class Messages { diff --git a/common/lib/wrapper_property.ts b/common/lib/wrapper_property.ts index 40f739884..628f72592 100644 --- a/common/lib/wrapper_property.ts +++ b/common/lib/wrapper_property.ts @@ -478,6 +478,24 @@ export class WrapperProperties { 0 ); + static readonly GDB_RW_HOME_REGION = new WrapperProperty( + "gdbRwHomeRegion", + "Specifies the home region for read/write splitting.", + null + ); + + static readonly GDB_RW_RESTRICT_WRITER_TO_HOME_REGION = new WrapperProperty( + "gdbRwRestrictWriterToHomeRegion", + "Prevents connections to a writer node outside of the defined home region.", + true + ); + + static readonly GDB_RW_RESTRICT_READER_TO_HOME_REGION = new WrapperProperty( + "gdbRwRestrictReaderToHomeRegion", + "Prevents connections to a reader node outside of the defined home region.", + true + ); + private static readonly PREFIXES = [ WrapperProperties.MONITORING_PROPERTY_PREFIX, ClusterTopologyMonitorImpl.MONITORING_PROPERTY_PREFIX,