Skip to content

Commit

Permalink
docs: fastest response docs and examples (#362)
Browse files Browse the repository at this point in the history
  • Loading branch information
joyc-bq authored Jan 6, 2025
1 parent b9eedde commit 9e0cfe9
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 5 deletions.
8 changes: 4 additions & 4 deletions docs/using-the-nodejs-wrapper/Telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ Metrics can be one of 3 types: counters, gauges or histograms.

### EFM plugin

| Metric name | Metric type | Description |
| ---------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------- |
| efm.connections.aborted | Counter | Number of times a connection was aborted after being defined as unhealthy by an EFM monitoring thread |
| efm.hostUnhealthy.count.[INSTANCE] | Counter | Number of times a specific instance has been defined as unhealthy |
| Metric name | Metric type | Description |
| ---------------------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
| efm.connections.aborted | Counter | Number of times a connection was aborted after being defined as unhealthy by an EFM monitoring task |
| efm.hostUnhealthy.count.[INSTANCE] | Counter | Number of times a specific instance has been defined as unhealthy |

### Secrets Manager plugin

Expand Down
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.<br><br> :warning:**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 |

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 that monitors 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.

The host response time is measured at an interval set by the configuration parameter `responseMeasurementIntervalMs`, at which time the old cache expires and is updated with the current fastest host.

## 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.

> [!IMPORTANT]\
> **The `readerHostSelectorStrategy` parameter 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'.`
```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` |
| `responseMeasurementIntervalMs` | `number` | No | Interval in milliseconds between measuring response time to a database host. | `30_000` |

## Host Response Time Monitor

The Host Response Time Monitor measures the host response time in a separate monitoring task. If the monitoring task has not been called for a response time for 10 minutes, the task is stopped. When the topology changes, the new hosts will be added to monitoring.

The host response time monitoring task 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:

```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.**
### Sample Code

[PostgreSQL fastest response strategy sample code](../../../examples/aws_driver_example/fastest_response_strategy_postgres_example.ts)<br>
[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();

0 comments on commit 9e0cfe9

Please sign in to comment.