Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: fastest response docs and examples #362

Merged
merged 13 commits into from
Jan 6, 2025
2 changes: 1 addition & 1 deletion docs/using-the-nodejs-wrapper/UsingTheNodejsWrapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ The AWS Advanced NodeJS Wrapper has several built-in plugins that are available
| [Read Write Splitting Plugin](./using-plugins/UsingTheReadWriteSplittingPlugin.md) | `readWriteSplitting` | Aurora | Enables read write splitting functionality where users can switch between database reader and writer instances. | None |
| [Aurora Initial Connection Strategy Plugin](./using-plugins/UsingTheAuroraInitialConnectionStrategyPlugin.md) | `initialConnection` | Aurora | Allows users to configure their initial connection strategy to reader cluster endpoints. | None |
| [Aurora Limitless Connection Plugin](./using-plugins/UsingTheAuroraInitialConnectionStrategyPlugin.md) | `limitless` | Aurora | Allows users to use Aurora Limitless Database and effectively load-balance load between available transaction routers. | None |
| Fastest Response Strategy Plugin | `fastestResponseStrategy` | Aurora | When read-write splitting is enabled, this plugin selects the reader to switch to based on the host with the fastest response time. The plugin achieves this by periodically monitoring the hosts' response times and storing the fastest host in a cache. **Note:** the `readerHostSelector` strategy must be set to `fastestResponse` in the user-defined connection properties in order to enable this plugin. See [reader selection strategies](./ReaderSelectionStrategies.md) | None |
| [Fastest Response Strategy Plugin](./using-plugins/UsingTheFastestResponseStrategyPlugin.md) | `fastestResponseStrategy` | Aurora | When read-write splitting is enabled, this plugin selects the reader to switch to based on the host with the fastest response time. The plugin achieves this by periodically monitoring the hosts' response times and storing the fastest host in a cache. **Note:** the `readerHostSelector` strategy must be set to `fastestResponse` in the user-defined connection properties in order to enable this plugin. See [reader selection strategies](./ReaderSelectionStrategies.md) | None |
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

In addition to the built-in plugins, you can also create custom plugins more suitable for your needs.
For more information, see [Custom Plugins](../development-guide/LoadablePlugins.md#using-custom-plugins).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## Fastest Response Strategy Plugin

The Fastest Response Strategy Plugin is a host selection strategy plugin monitoring the response time of each reader host, and returns the host with the fastest response time. The plugin stores the fastest host in a cache so it can easily be retrieved again.
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

The host response time is measured at an interval set by `responseMeasurementIntervalMs`, at which time the old cache expires and is updated with the current fastest host.
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

## Using the Fastest Response Strategy Plugin

The plugin can be loaded by adding the plugin code `FastestResponseStrategy` to the [`plugins`](../UsingTheNodeJsWrapper#aws-advanced-nodejs-wrapper-parameters) parameter. The Fastest Response Strategy Plugin is not loaded by default, and must be loaded along with the [`readWriteSplitting`](./UsingTheReadWriteSplittingPlugin.md) plugin.
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

> [!IMPORTANT]\
> **`readerHostSelectorStrategy` must be set to `fastestReponse` when using this plugin. Otherwise an error will be thrown:** > `Unsupported host selector strategy: 'random'. To use the fastest response strategy plugin, please ensure the property readerHostSelectorStrategy is set to 'fastestResponse'.`
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

```ts
params = {
plugins: "readWriteSplitting,fastestResponseStrategy,failover,efm",
readerHostSelectorStrategy: "fastestResponse"
// Add other connection properties below...
};

// If using MySQL:
const client = new AwsMySQLClient(params);
await client.connect();

// If using Postgres:
const client = new AwsPGClient(params);
await client.connect();
```

## Configuration Parameters

| Parameter | Value | Required | Description | Default Value |
| ------------------------------- | :------: | :------: | :--------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `readerHostSelectorStrategy` | `string` | Yes | Setting to `fastestReponse` sets the reader host selector strategy to choose the fastest host using the Fastest Response Strategy Plugin | `random` |
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved
| `responseMeasurementIntervalMs` | `number` | No | Interval in milliseconds between measuring response time to a database host | `30_000` |
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

## Host Response Time Monitor

The Host Response Time Monitor measures the host response time in a separate monitoring thread. If the monitoring thread has not been called for a response time for 10 minutes, the thread is stopped. When the topology changes, the new hosts will be added to monitoring.
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

The host response time monitoring thread creates new database connections. By default it uses the same set of connection parameters provided for the main connection, but you can customize these connections with the `frt-` prefix, as in the following example:
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

```ts
const client = new AwsMySQLClient({
user: "john",
password: "pwd",
host: "database.cluster-xyz.us-east-1.rds.amazonaws.com",
database: "mysql",
port: 3306,
plugins: "readWriteSplitting,fastestResponseStrategy",
readerHostSelectorStrategy: "fastestResponse"
// Configure the timeout values for all non-monitoring connections.
connectTimeout: 30
// Configure different timeout values for the host response time monitoring connection.
frt_connectTimeout: 10
});
```

> [!IMPORTANT]\
> **When specifying a frt\_ prefixed timeout, always ensure you provide a non-zero timeout value**
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved

### Sample Code

[Postgres fastest response strategy sample code](../../../examples/aws_driver_example/fastest_response_strategy_postgres_example.ts)<br>
joyc-bq marked this conversation as resolved.
Show resolved Hide resolved
[MySQL fastest response strategy sample code](../../../examples/aws_driver_example/fastest_response_strategy_mysql_example.ts)
116 changes: 116 additions & 0 deletions examples/aws_driver_example/fastest_response_strategy_mysql_example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
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 { AwsMySQLClient } from "../../mysql/lib";
import { FailoverFailedError, FailoverSuccessError, TransactionResolutionUnknownError } from "../../common/lib/utils/errors";
import { PluginManager } from "../../common/lib";

const mysqlHost = "db-identifier.XYZ.us-east-2.rds.amazonaws.com";
const username = "john_smith";
const password = "employees";
const database = "database";
const port = 3306;

const client = new AwsMySQLClient({
// Configure connection parameters. Enable readWriteSplitting, failover, and efm plugins.
host: mysqlHost,
port: port,
user: username,
password: password,
database: database,
plugins: "readWriteSplitting, fastestResponseStrategy, failover, efm",
readerHostSelectorStrategy: "fastestResponse"
});

// Setup Step: Open connection and create tables - uncomment this section to create table and test values.
/* try {
await client.connect();
await setInitialSessionSettings(client);
await queryWithFailoverHandling(client,
"CREATE TABLE bank_test (id int primary key, name varchar(40), account_balance int)");
await queryWithFailoverHandling(client,
"INSERT INTO bank_test VALUES (0, 'Jane Doe', 200), (1, 'John Smith', 200), (2, 'Sally Smith', 200), (3, 'Joe Smith', 200)");
} catch (error: any) {
// Additional error handling can be added here. See transaction step for an example.
throw error;
} */

// Transaction Step: Open connection and perform transaction.
try {
await client.connect();
await setInitialSessionSettings(client);

// Example query
const result = await queryWithFailoverHandling(client, "UPDATE bank_test SET account_balance=account_balance - 100 WHERE name='Jane Doe'");
console.log(result);

// Internally switch to a reader connection.
await client.setReadOnly(true);

await queryWithFailoverHandling(client, "SELECT * FROM bank_test");

// Internally switch to a writer connection.
await client.setReadOnly(false);

// Use cached host when switching back to a reader
await client.setReadOnly(true);
} catch (error) {
if (error instanceof FailoverFailedError) {
// User application should open a new connection, check the results of the failed transaction and re-run it if
// needed. See:
// https://github.com/aws/aws-advanced-nodejs-wrapper/blob/main/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailoverPlugin.md#failoverfailederror
throw error;
} else if (error instanceof TransactionResolutionUnknownError) {
// User application should check the status of the failed transaction and restart it if needed. See:
// https://github.com/aws/aws-advanced-nodejs-wrapper/blob/main/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailoverPlugin.md#transactionresolutionunknownerror
throw error;
} else {
// Unexpected exception unrelated to failover. This should be handled by the user application.
throw error;
}
} finally {
await client.end();
}

async function setInitialSessionSettings(client: AwsMySQLClient) {
// User can edit settings.
await client.query({ sql: "SET time_zone = 'UTC'" });
}

async function queryWithFailoverHandling(client: AwsMySQLClient, query: string) {
try {
const result = await client.query({ sql: query });
return result;
} catch (error) {
if (error instanceof FailoverFailedError) {
// Connection failed, and Node.js wrapper failed to reconnect to a new instance.
throw error;
} else if (error instanceof FailoverSuccessError) {
// Query execution failed and Node.js wrapper successfully failed over to a new elected writer instance.
// Reconfigure the connection
await setInitialSessionSettings(client);
// Re-run query
return await client.query({ sql: query });
} else if (error instanceof TransactionResolutionUnknownError) {
// Transaction resolution unknown. Please re-configure session state if required and try
// restarting transaction.
throw error;
}
}
}

// Clean up resources used by the plugins.
await PluginManager.releaseResources();
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
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 { FailoverFailedError, FailoverSuccessError, TransactionResolutionUnknownError } from "../../common/lib/utils/errors";
import { PluginManager } from "../../common/lib";
import { AwsPGClient } from "../../pg/lib";

const mysqlHost = "db-identifier.XYZ.us-east-2.rds.amazonaws.com";
const username = "john_smith";
const password = "employees";
const database = "database";
const port = 3306;

const client = new AwsPGClient({
// Configure connection parameters. Enable readWriteSplitting, failover, and efm plugins.
host: mysqlHost,
port: port,
user: username,
password: password,
database: database,
plugins: "readWriteSplitting, fastestResponseStrategy, failover, efm",
readerHostSelectorStrategy: "fastestResponse"
});

// Setup Step: Open connection and create tables - uncomment this section to create table and test values.
/* try {
await client.connect();
await setInitialSessionSettings(client);
await queryWithFailoverHandling(client,
"CREATE TABLE bank_test (name varchar(40), account_balance int)");
await queryWithFailoverHandling(client,
"INSERT INTO bank_test VALUES ('Jane Doe', 200), ('John Smith', 200)");
} catch (error: any) {
// Additional error handling can be added here. See transaction step for an example.
throw error;
} */

// Transaction Step: Open connection and perform transaction.
try {
await client.connect();
await setInitialSessionSettings(client);

// Example query
const result = await queryWithFailoverHandling(client, "UPDATE bank_test SET account_balance=account_balance - 100 WHERE name='Jane Doe'");
console.log(result);

// Internally switch to a reader connection.
await client.setReadOnly(true);

await queryWithFailoverHandling(client, "SELECT * FROM bank_test");

// Internally switch to a writer connection.
await client.setReadOnly(false);

// Use cached host when switching back to a reader
await client.setReadOnly(true);
} catch (error) {
if (error instanceof FailoverFailedError) {
// User application should open a new connection, check the results of the failed transaction and re-run it if
// needed. See:
// https://github.com/aws/aws-advanced-nodejs-wrapper/blob/main/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailoverPlugin.md#failoverfailederror
throw error;
} else if (error instanceof TransactionResolutionUnknownError) {
// User application should check the status of the failed transaction and restart it if needed. See:
// https://github.com/aws/aws-advanced-nodejs-wrapper/blob/main/docs/using-the-nodejs-wrapper/using-plugins/UsingTheFailoverPlugin.md#transactionresolutionunknownerror
throw error;
} else {
// Unexpected exception unrelated to failover. This should be handled by the user application.
throw error;
}
} finally {
await client.end();
}

async function setInitialSessionSettings(client: AwsPGClient) {
// User can edit settings.
await client.query("SET TIME ZONE UTC");
}

async function queryWithFailoverHandling(client: AwsPGClient, query: string) {
try {
const result = await client.query(query);
return result;
} catch (error) {
if (error instanceof FailoverFailedError) {
// Connection failed, and Node.js wrapper failed to reconnect to a new instance.
throw error;
} else if (error instanceof FailoverSuccessError) {
// Query execution failed and Node.js wrapper successfully failed over to a new elected writer instance.
// Reconfigure the connection
await setInitialSessionSettings(client);
// Re-run query
return await client.query(query);
} else if (error instanceof TransactionResolutionUnknownError) {
// Transaction resolution unknown. Please re-configure session state if required and try
// restarting transaction.
throw error;
}
}
}

// Clean up resources used by the plugins.
await PluginManager.releaseResources();
4 changes: 2 additions & 2 deletions tests/plugin_manager_benchmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ suite(
}),

add("initHostProviderWith10Plugins", async () => {
const pluginManagerWithPlugins = await initPluginManagerWithPlugins(10, instance(mockPluginService), propsWithPlugins);;
const pluginManagerWithPlugins = await initPluginManagerWithPlugins(10, instance(mockPluginService), propsWithPlugins);
return async () =>
await pluginManagerWithPlugins.initHostProvider(
new HostInfoBuilder({ hostAvailabilityStrategy: new SimpleHostAvailabilityStrategy() }).withHost("host").build(),
Expand Down Expand Up @@ -325,7 +325,7 @@ suite(
}),

add("notifyConnectionChangedWith10Plugins", async () => {
const pluginManagerWithPlugins = await initPluginManagerWithPlugins(10, instance(mockPluginService), propsWithPlugins);;
const pluginManagerWithPlugins = await initPluginManagerWithPlugins(10, instance(mockPluginService), propsWithPlugins);
return async () =>
await pluginManagerWithPlugins.notifyConnectionChanged(new Set<HostChangeOptions>([HostChangeOptions.INITIAL_CONNECTION]), null);
}),
Expand Down
Loading