diff --git a/common/lib/host_list_provider/rds_host_list_provider.ts b/common/lib/host_list_provider/rds_host_list_provider.ts index 655f827d..30c80a3f 100644 --- a/common/lib/host_list_provider/rds_host_list_provider.ts +++ b/common/lib/host_list_provider/rds_host_list_provider.ts @@ -44,16 +44,12 @@ export class RdsHostListProvider implements DynamicHostListProvider { private initialHostList: HostInfo[]; protected initialHost: HostInfo; private refreshRateNano: number; - private suggestedClusterIdRefreshRateNano: number = 10 * 60 * 1_000_000_000; // 10 minutes private hostList?: HostInfo[]; protected readonly connectionUrlParser: ConnectionUrlParser; protected readonly hostListProviderService: HostListProviderService; - public static readonly suggestedPrimaryClusterIdCache: CacheMap = new CacheMap(); - public static readonly primaryClusterIdCache: CacheMap = new CacheMap(); public clusterId: string = Date.now().toString(); public isInitialized: boolean = false; - public isPrimaryClusterId?: boolean; public clusterInstanceTemplate?: HostInfo; constructor(properties: Map, originalUrl: string, hostListProviderService: HostListProviderService) { @@ -87,8 +83,6 @@ export class RdsHostListProvider implements DynamicHostListProvider { return; } - this.isPrimaryClusterId = false; - const hostInfoBuilder = this.hostListProviderService.getHostInfoBuilder(); this.clusterInstanceTemplate = hostInfoBuilder @@ -98,29 +92,7 @@ export class RdsHostListProvider implements DynamicHostListProvider { this.validateHostPatternSetting(this.clusterInstanceTemplate.host); - const clusterIdSetting: string = WrapperProperties.CLUSTER_ID.get(this.properties); - if (clusterIdSetting) { - this.clusterId = clusterIdSetting; - } else if (this.rdsUrlType === RdsUrlType.RDS_PROXY) { - // Each proxy is associated with a single cluster, so it's safe to use RDS Proxy Url as cluster - // identification - this.clusterId = this.initialHost.url; - } else if (this.rdsUrlType.isRds) { - const clusterSuggestedResult: ClusterSuggestedResult | null = this.getSuggestedClusterId(this.initialHost.hostAndPort); - if (clusterSuggestedResult && clusterSuggestedResult.clusterId) { - this.clusterId = clusterSuggestedResult.clusterId; - this.isPrimaryClusterId = clusterSuggestedResult.isPrimaryClusterId; - } else { - const clusterRdsHostUrl: string | null = this.rdsHelper.getRdsClusterHostUrl(this.initialHost.host); - if (clusterRdsHostUrl) { - this.clusterId = this.clusterInstanceTemplate.isPortSpecified() - ? `${clusterRdsHostUrl}:${this.clusterInstanceTemplate.port}` - : clusterRdsHostUrl; - this.isPrimaryClusterId = true; - RdsHostListProvider.primaryClusterIdCache.put(this.clusterId, true, this.suggestedClusterIdRefreshRateNano); - } - } - } + this.clusterId = WrapperProperties.CLUSTER_ID.get(this.properties); this.isInitialized = true; } @@ -195,18 +167,11 @@ export class RdsHostListProvider implements DynamicHostListProvider { throw new AwsWrapperError("no cluster id"); } - const suggestedPrimaryClusterId: string | null = RdsHostListProvider.suggestedPrimaryClusterIdCache.get(this.clusterId); - if (suggestedPrimaryClusterId && this.clusterId !== suggestedPrimaryClusterId) { - this.clusterId = suggestedPrimaryClusterId; - this.isPrimaryClusterId = true; - } - const cachedHosts: HostInfo[] | null = this.getStoredTopology(); // This clusterId is a primary one and is about to create a new entry in the cache. // When a primary entry is created it needs to be suggested for other (non-primary) entries. // Remember a flag to do suggestion after cache is updated. - const needToSuggest: boolean = !cachedHosts && this.isPrimaryClusterId === true; if (!cachedHosts || forceUpdate) { // need to re-fetch the topology. if (!targetClient || !(await this.hostListProviderService.isClientValid(targetClient))) { @@ -216,9 +181,6 @@ export class RdsHostListProvider implements DynamicHostListProvider { const hosts = await this.queryForTopology(targetClient, this.hostListProviderService.getDialect()); if (hosts && hosts.length > 0) { this.storageService.set(this.clusterId, new Topology(hosts)); - if (needToSuggest) { - this.suggestPrimaryCluster(hosts); - } return new FetchTopologyResult(false, hosts); } } @@ -230,63 +192,6 @@ export class RdsHostListProvider implements DynamicHostListProvider { } } - private getSuggestedClusterId(hostAndPort: string): ClusterSuggestedResult | null { - const cache: ExpirationCache = this.storageService.getAll(Topology) as ExpirationCache; - if (!cache) { - return null; - } - for (const [key, hosts] of cache.getEntries()) { - const isPrimaryCluster: boolean = RdsHostListProvider.primaryClusterIdCache.get(key, false, this.suggestedClusterIdRefreshRateNano) ?? false; - if (key === hostAndPort) { - return new ClusterSuggestedResult(hostAndPort, isPrimaryCluster); - } - - if (hosts) { - for (const hostInfo of hosts.hosts) { - if (hostInfo.hostAndPort === hostAndPort) { - logger.debug(Messages.get("RdsHostListProvider.suggestedClusterId", key, hostAndPort)); - return new ClusterSuggestedResult(key, isPrimaryCluster); - } - } - } - } - return null; - } - - suggestPrimaryCluster(primaryClusterHosts: HostInfo[]): void { - if (!primaryClusterHosts) { - return; - } - - const primaryClusterHostUrls: Set = new Set(); - primaryClusterHosts.forEach((hostInfo) => { - primaryClusterHostUrls.add(hostInfo.url); - }); - - const cache: ExpirationCache = this.storageService.getAll(Topology) as ExpirationCache; - if (!cache) { - return; - } - for (const [clusterId, clusterHosts] of cache.getEntries()) { - const isPrimaryCluster: boolean | null = RdsHostListProvider.primaryClusterIdCache.get( - clusterId, - false, - this.suggestedClusterIdRefreshRateNano - ); - const suggestedPrimaryClusterId: string | null = RdsHostListProvider.suggestedPrimaryClusterIdCache.get(clusterId); - if (isPrimaryCluster || suggestedPrimaryClusterId || !clusterHosts) { - continue; - } - - for (const clusterHost of clusterHosts.hosts) { - if (primaryClusterHostUrls.has(clusterHost.url)) { - RdsHostListProvider.suggestedPrimaryClusterIdCache.put(clusterId, this.clusterId, this.suggestedClusterIdRefreshRateNano); - break; - } - } - } - } - async queryForTopology(targetClient: ClientWrapper, dialect: DatabaseDialect): Promise { if (!isDialectTopologyAware(dialect)) { throw new TypeError(Messages.get("RdsHostListProvider.incorrectDialect")); @@ -366,8 +271,8 @@ export class RdsHostListProvider implements DynamicHostListProvider { } static clearAll(): void { - RdsHostListProvider.primaryClusterIdCache.clear(); - RdsHostListProvider.suggestedPrimaryClusterIdCache.clear(); + // No-op + // TODO: remove if still not used after full service container refactoring } clear(): void { @@ -420,13 +325,3 @@ export class FetchTopologyResult { this.isCachedData = isCachedData; } } - -class ClusterSuggestedResult { - clusterId: string; - isPrimaryClusterId: boolean; - - constructor(clusterId: string, isPrimaryClusterId: boolean) { - this.clusterId = clusterId; - this.isPrimaryClusterId = isPrimaryClusterId; - } -} diff --git a/common/lib/utils/storage/storage_service.ts b/common/lib/utils/storage/storage_service.ts index d13bc052..0c32d376 100644 --- a/common/lib/utils/storage/storage_service.ts +++ b/common/lib/utils/storage/storage_service.ts @@ -56,10 +56,6 @@ export interface StorageService { */ set(key: unknown, item: V): void; - // TODO: temporary method to return all storage services for a specific item class. - // Should be removed along with the cluster id refactoring. - getAll(itemClass: Constructor): ExpirationCache | null; - /** * Gets an item stored in the storage service. * diff --git a/common/lib/wrapper_property.ts b/common/lib/wrapper_property.ts index 215714c7..0dbc2652 100644 --- a/common/lib/wrapper_property.ts +++ b/common/lib/wrapper_property.ts @@ -234,8 +234,8 @@ export class WrapperProperties { "clusterId", "A unique identifier for the cluster. " + "Connections with the same cluster id share a cluster topology cache. " + - "If unspecified, a cluster id is automatically created for AWS RDS clusters.", - null + "If unspecified, a cluster id is '1'.", + "1" ); static readonly CLUSTER_INSTANCE_HOST_PATTERN = new WrapperProperty( diff --git a/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailover2Plugin.md b/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailover2Plugin.md index e943fe2d..37789e59 100644 --- a/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailover2Plugin.md +++ b/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailover2Plugin.md @@ -62,14 +62,15 @@ Please refer to the [failover configuration guide](../FailoverConfigurationGuide In addition to the parameters that you can configure for the underlying driver, you can pass the following parameters to the AWS Advanced NodeJS Wrapper through the connection URL to specify additional failover behavior. -| Parameter | Value | Required | Description | Default Value | -| ------------------------------------ | :-----: | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| `failoverMode` | String | No | Defines a mode for failover process. Failover process may prioritize nodes with different roles and connect to them. Possible values:

- `strict-writer` - Failover process follows writer instance and connects to a new writer when it changes.
- `reader-or-writer` - During failover, the wrapper tries to connect to any available/accessible reader instance. If no reader is available, the wrapper will connect to a writer instance. This logic mimics the logic of the Aurora read-only cluster endpoint.
- `strict-reader` - During failover, the wrapper tries to connect to any available reader instance. If no reader is available, the wrapper raises an error. Reader failover to a writer instance will only be allowed for single-instance clusters. This logic mimics the logic of the Aurora read-only cluster endpoint. | Default value depends on connection url. For Aurora read-only cluster endpoint, it's set to `reader-or-writer`. Otherwise, it's `strict-writer`. | -| `enableClusterAwareFailover` | Boolean | No | Set to `true` to enable the fast failover behavior offered by the AWS Advanced NodeJS Wrapper. Set to `false` for simple connections that do not require fast failover functionality. | `true` | -| `clusterTopologyRefreshRateMs` | Number | No | Cluster topology refresh rate in milliseconds when a cluster is not in failover. | `30000` | -| `clusterTopologyHighRefreshRateMs` | Number | No | Interval of time in milliseconds to wait between attempts to update cluster topology after the writer has come back online following a failover event. Usually, the topology monitoring component uses this increased monitoring rate for 30s after a new writer is detected. | `100` | -| `failoverTimeoutMs` | Number | No | Maximum allowed time in milliseconds to attempt reconnecting to a new writer or reader instance after a cluster failover is initiated. | `300000` | -| `failoverReaderHostSelectorStrategy` | String | No | The strategy that should be used to select a new reader host while opening a new connection. See: [Reader Selection Strategies](./../ReaderSelectionStrategies.md) for options. | `random` | +| Parameter | Value | Required | Description | Default Value | +| ------------------------------------ | :-----: | :-----------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `failoverMode` | String | No | Defines a mode for failover process. Failover process may prioritize nodes with different roles and connect to them. Possible values:

- `strict-writer` - Failover process follows writer instance and connects to a new writer when it changes.
- `reader-or-writer` - During failover, the wrapper tries to connect to any available/accessible reader instance. If no reader is available, the wrapper will connect to a writer instance. This logic mimics the logic of the Aurora read-only cluster endpoint.
- `strict-reader` - During failover, the wrapper tries to connect to any available reader instance. If no reader is available, the wrapper raises an error. Reader failover to a writer instance will only be allowed for single-instance clusters. This logic mimics the logic of the Aurora read-only cluster endpoint. | Default value depends on connection url. For Aurora read-only cluster endpoint, it's set to `reader-or-writer`. Otherwise, it's `strict-writer`. | +| `enableClusterAwareFailover` | Boolean | No | Set to `true` to enable the fast failover behavior offered by the AWS Advanced NodeJS Wrapper. Set to `false` for simple connections that do not require fast failover functionality. | `true` | +| `clusterTopologyRefreshRateMs` | Number | No | Cluster topology refresh rate in milliseconds when a cluster is not in failover. | `30000` | +| `clusterTopologyHighRefreshRateMs` | Number | No | Interval of time in milliseconds to wait between attempts to update cluster topology after the writer has come back online following a failover event. Usually, the topology monitoring component uses this increased monitoring rate for 30s after a new writer is detected. | `100` | +| `failoverTimeoutMs` | Number | No | Maximum allowed time in milliseconds to attempt reconnecting to a new writer or reader instance after a cluster failover is initiated. | `300000` | +| `clusterId` | String | If using multiple database clusters, yes; otherwise, no | A unique identifier for the cluster. Connections with the same cluster id share a cluster topology cache. This parameter is optional and defaults to `1`. When supporting multiple database clusters, this parameter becomes mandatory. Each connection string must include the `clusterId` parameter with a value that can be any number or string. However, all connection strings associated with the same database cluster must use identical `clusterId` values, while connection strings belonging to different database clusters must specify distinct values. Examples of value: `1`, `2`, `1234`, `abc-1`, `abc-2`. | `1` | +| `failoverReaderHostSelectorStrategy` | String | No | The strategy that should be used to select a new reader host while opening a new connection. See: [Reader Selection Strategies](./../ReaderSelectionStrategies.md) for options. | `random` | Please refer to the original [Failover Plugin](./UsingTheFailoverPlugin.md) for more details about error codes, configurations, connection pooling and sample codes. diff --git a/tests/unit/rds_host_list_provider.test.ts b/tests/unit/rds_host_list_provider.test.ts index 1801169c..86f20d00 100644 --- a/tests/unit/rds_host_list_provider.test.ts +++ b/tests/unit/rds_host_list_provider.test.ts @@ -201,199 +201,6 @@ describe("testRdsHostListProvider", () => { expect(rdsHostListProvider.getStoredTopology()).toBeNull(); }); - it("testTopologyCache_noSuggestedClusterId", async () => { - RdsHostListProvider.clearAll(); - - when(mockPluginService.isClientValid(anything())).thenResolve(true); - - const provider1 = getRdsHostListProvider("cluster-a.xyz.us-east-2.rds.amazonaws.com"); - const spiedProvider1 = spy(provider1); - - const topologyClusterA: HostInfo[] = [ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-1.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.WRITER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-2.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-3.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }) - ]; - - when(spiedProvider1.queryForTopology(mockClientWrapper, anything())).thenReturn(Promise.resolve(topologyClusterA)); - expect(storageService.getAll(Topology)).toBeNull(); - - const topologyProvider1: HostInfo[] = await provider1.refresh(mockClientWrapper); - expect(topologyProvider1).toEqual(topologyClusterA); - - const provider2 = getRdsHostListProvider("cluster-b.xyz.us-east-2.rds.amazonaws.com"); - const spiedProvider2 = spy(provider2); - - const topologyClusterB: HostInfo[] = [ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-b-1.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.WRITER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-b-2.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-b-3.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }) - ]; - when(spiedProvider2.queryForTopology(instance(mockClientWrapper), anything())).thenReturn(Promise.resolve(topologyClusterB)); - - expect(await provider2.refresh(instance(mockClientWrapper))).toEqual(topologyClusterB); - expect(storageService.getAll(Topology).size()).toEqual(2); - }); - - it("testTopologyCache_suggestedClusterIdForRds", async () => { - RdsHostListProvider.clearAll(); - - when(mockPluginService.isClientValid(anything())).thenResolve(true); - - const topologyClusterA: HostInfo[] = [ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-1.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.WRITER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-2.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-3.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }) - ]; - - const provider1 = getRdsHostListProvider("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com"); - const spiedProvider1 = spy(provider1); - - when(spiedProvider1.queryForTopology(mockClientWrapper, anything())).thenReturn(Promise.resolve(topologyClusterA)); - expect(storageService.getAll(Topology)).toBeNull(); - - const topologyProvider1: HostInfo[] = await provider1.refresh(mockClientWrapper); - expect(topologyProvider1).toEqual(topologyClusterA); - - const provider2 = getRdsHostListProvider("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com"); - - expect(provider2.clusterId).toEqual(provider1.clusterId); - expect(provider1.isPrimaryClusterId).toBeTruthy(); - expect(provider2.isPrimaryClusterId).toBeTruthy(); - - expect(await provider2.refresh(mockClientWrapper)).toEqual(topologyClusterA); - expect(storageService.getAll(Topology).size()).toEqual(1); - }); - - it("testTopologyCache_suggestedClusterIdForInstance", async () => { - RdsHostListProvider.clearAll(); - - when(mockPluginService.isClientValid(anything())).thenResolve(true); - - const topologyClusterA: HostInfo[] = [ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-1.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.WRITER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-2.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-3.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }) - ]; - - const provider1 = getRdsHostListProvider("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com"); - const spiedProvider1 = spy(provider1); - - when(spiedProvider1.queryForTopology(mockClientWrapper, anything())).thenReturn(Promise.resolve(topologyClusterA)); - expect(storageService.getAll(Topology)).toBeNull(); - - const topologyProvider1: HostInfo[] = await provider1.refresh(mockClientWrapper); - expect(topologyProvider1).toEqual(topologyClusterA); - - const provider2 = getRdsHostListProvider("instance-a-3.xyz.us-east-2.rds.amazonaws.com"); - - expect(provider2.clusterId).toEqual(provider1.clusterId); - expect(provider1.isPrimaryClusterId).toBeTruthy(); - expect(provider2.isPrimaryClusterId).toBeTruthy(); - - expect(await provider2.refresh(mockClientWrapper)).toEqual(topologyClusterA); - expect(storageService.getAll(Topology).size()).toEqual(1); - }); - - it("testTopologyCache_acceptSuggestion", async () => { - RdsHostListProvider.clearAll(); - - when(mockPluginService.isClientValid(anything())).thenResolve(true); - - const topologyClusterA: HostInfo[] = [ - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-1.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.WRITER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-2.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }), - createHost({ - hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy(), - host: "instance-a-3.xyz.us-east-2.rds.amazonaws.com", - role: HostRole.READER - }) - ]; - - const provider1 = getRdsHostListProvider("instance-a-2.xyz.us-east-2.rds.amazonaws.com"); - const spiedProvider1 = spy(provider1); - - when(spiedProvider1.queryForTopology(anything(), anything())).thenReturn(Promise.resolve(topologyClusterA)); - expect(storageService.getAll(Topology)).toBeNull(); - - const topologyProvider1: HostInfo[] = await provider1.refresh(mockClientWrapper); - expect(topologyProvider1).toEqual(topologyClusterA); - - const provider2 = getRdsHostListProvider("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com"); - const spiedProvider2 = spy(provider2); - - when(spiedProvider2.queryForTopology(anything(), anything())).thenReturn(Promise.resolve(topologyClusterA)); - expect(provider2.clusterId).not.toEqual(provider1.clusterId); - expect(provider1.isPrimaryClusterId).toBeFalsy(); - expect(provider2.isPrimaryClusterId).toBeTruthy(); - - expect(await provider2.refresh(instance(mockClientWrapper))).toEqual(topologyClusterA); - expect(storageService.getAll(Topology).size()).toEqual(2); - expect(RdsHostListProvider.suggestedPrimaryClusterIdCache.get(provider1.clusterId)).toEqual("cluster-a.cluster-xyz.us-east-2.rds.amazonaws.com"); - - expect(await provider1.forceRefresh(instance(mockClientWrapper))).toEqual(topologyClusterA); - expect(provider2.clusterId).toEqual(provider1.clusterId); - expect(storageService.getAll(Topology).size()).toEqual(2); - expect(provider1.isPrimaryClusterId).toBeTruthy(); - expect(provider2.isPrimaryClusterId).toBeTruthy(); - }); - it("testIdentifyConnectionWithInvalidHostIdQuery", async () => { when(mockDialect.identifyConnection(anything())).thenThrow(new AwsWrapperError("bad things"));