AwsMySQLPoolClient runs `SELECT @@innodb_read_only` twice before every query, and `verifyOpenedConnectionType` has no effect

Đang mở
#717 2 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
45/100
Loại issue
Lỗi
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
mysql, node.js, typescript
Lĩnh vực
backend, databases

Hướng nghiên cứu

Start with mysql/lib/client.ts and trace AwsMySQLPoolClient.query() through connect(), then read common/lib/plugins/aurora_initial_connection_strategy_plugin.ts and common/lib/internal_pooled_connection_provider.ts. The issue’s reproduction script can confirm the extra role-check queries; common/lib/wrapper_property.ts defines the option that currently appears unused. Done means role verification follows a supported configuration or connection-lifecycle policy, without repeated checks on every pooled query.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

bug
Describe the bug

When using AwsMySQLPoolClient as documented (Using the Connection Pool), every pool.query() sends SELECT @@innodb_read_only as is_reader to the database before the actual query. With the initialConnection plugin (a default plugin since 2.1.1) and a reader cluster endpoint, it is sent twice per query.

The physical connection is reused from the internal mysql2 pool, but the role is verified again on every checkout. There is no way to turn this off: verifyOpenedConnectionType is defined in WrapperProperties but not read anywhere.

This became visible after adding DB spans via the mysql2 tracing channel. In a test environment:

  • is_reader queries were about 63% of the total DB span time; the application's own queries were the rest.
  • Each application query gets 2 extra round trips (1–3 ms per request).
  • Reader QPS is about 3x the actual application QPS.
  • Locally, pool.query() used about 1.5 ms more CPU per query than plain mysql2, with all plugins disabled. That is the cost of building a new client and plugin chain per query. With plugins enabled it is likely higher.

The same symptom was reported for the JDBC wrapper in aws/aws-advanced-jdbc-wrapper#1861 and reworked in JDBC 3.1.0 (aws/aws-advanced-jdbc-wrapper#1655), which made verifyOpenedConnectionType / endpointSubstitutionRole effective. The Python wrapper also has these options. The Node.js wrapper does not seem to have this change yet.

Expected Behavior

One of the following:

  • The host role is verified once per physical connection (or once per instance pool, since InternalPooledConnectionProvider already keeps one pool per instance), not on every pool.query().
  • verifyOpenedConnectionType: "none" disables the verification on checkout, as in the JDBC and Python wrappers.
What plugins are used? What other connection properties were set?

plugins include initialConnection and failover2 / dialect: aurora-mysql / host: reader cluster endpoint / AwsPoolConfig with default-like settings

Current Behavior

Trace of a single request that runs one application query (reader cluster endpoint, initialConnection enabled):

GET /items/:id                                   4.1 ms
 └─ handler
     ├─ SELECT @@innodb_read_only as is_reader   0.73 ms   ← wrapper
     ├─ SELECT @@innodb_read_only as is_reader   0.41 ms   ← wrapper
     └─ SELECT ... FROM items WHERE ...          0.87 ms   ← application

These are three separate queries on the wire (different start times and durations), not a duplicated span.

Local measurement with the script below (300 sequential select 1, same database, no plugins, host is not a cluster endpoint so initialConnection is skipped):

Method wall / query CPU / query queries sent / query is_reader / query
A. mysql2 createPool().query() 27.0 ms 0.73 ms 1 0
B. AwsMySQLPoolClient.query() 55.1 ms 2.28 ms 2 1
C. AwsMySQLPoolClient.getConnection() once, then conn.query() 27.4 ms 0.66 ms 1 0

(Wall time is dominated by a ~27 ms network round trip in this local setup.)

Reproduction Steps
// repro.mjs
// DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, PLUGINS (e.g. "initialConnection")
// Use a reader cluster endpoint + PLUGINS=initialConnection to see 2 checks per query.
import dc from "node:diagnostics_channel";
import { performance } from "node:perf_hooks";
import mysql from "mysql2/promise";
import { AwsPoolConfig, PluginManager } from "aws-advanced-nodejs-wrapper";
import { AwsMySQLPoolClient } from "aws-advanced-nodejs-wrapper/mysql";

const e = process.env;
const conn = { host: e.DB_HOST, port: Number(e.DB_PORT ?? 3306), user: e.DB_USER, password: e.DB_PASSWORD, database: e.DB_NAME };

// Count what mysql2 actually sends (mysql2 >= 3.20 tracing channels).
let isReader = 0, total = 0;
for (const name of ["mysql2:query", "mysql2:execute"]) {
  dc.tracingChannel(name).subscribe({
    start(m) { total++; if (String(m.query).includes("innodb_read_only")) isReader++; },
    end() {}, asyncStart() {}, asyncEnd() {}, error() {},
  });
}

const N = 300;
async function run(label, q) {
  for (let i = 0; i < 20; i++) await q();
  isReader = 0; total = 0;
  const cpu0 = process.cpuUsage(); const t0 = performance.now();
  for (let i = 0; i < N; i++) await q();
  const ms = performance.now() - t0; const cpu = process.cpuUsage(cpu0);
  console.log(`${label}: wall/q=${(ms / N).toFixed(3)}ms cpu/q=${((cpu.user + cpu.system) / 1000 / N).toFixed(3)}ms queries/q=${(total / N).toFixed(2)} is_reader/q=${(isReader / N).toFixed(2)}`);
}

const plain = mysql.createPool({ ...conn, connectionLimit: 5 });
await run("A mysql2 pool", () => plain.query("select 1"));
await plain.end();

const pool = new AwsMySQLPoolClient(
  { ...conn, plugins: e.PLUGINS ?? "", dialect: "aurora-mysql" },
  new AwsPoolConfig({ maxConnections: 5 }),
);
await run("B wrapper pool.query", () => pool.query("select 1"));
const c = await pool.getConnection();
await run("C wrapper getConnection reuse", () => c.query("select 1"));
await c.release();
await pool.end();
await PluginManager.releaseResources();

Only B sends more than one query per call: is_reader/q=1, or 2 with a reader cluster endpoint and initialConnection.

Adding verifyOpenedConnectionType: "none" to the client config does not change B in the local setup (still queries/q=2.00 is_reader/q=1.00). The check from the initial connection strategy plugin cannot be disabled this way either, because the plugin does not read the property (see below).

Possible Solution

Code paths (tag 3.0.1):

  1. mysql/lib/client.ts:642 — AwsMySQLPoolClient.query() creates a new AwsMySQLPooledConnection per query and calls connect(), then query(), then end().
  2. mysql/lib/client.ts:70-97 — connect() always calls pluginManager.connect(hostInfo, props, true, null) (isInitialConnection = true) and, for topology-aware dialects, calls getHostRole() on the new client → 1st is_reader.
  3. common/lib/plugins/aurora_initial_connection_strategy_plugin.ts:205 — for a reader cluster endpoint, getVerifiedReaderClient() also calls getHostRole() → 2nd is_reader.
  4. common/lib/internal_pooled_connection_provider.ts:118 — physical connections come from a per-instance mysql2 pool, so every connection in that pool points to the same instance.
  5. common/lib/wrapper_property.ts:751 — VERIFY_OPENED_CONNECTION_ROLE (verifyOpenedConnectionType) is defined, but git grep VERIFY_OPENED_CONNECTION_ROLE finds no other usage.

The behavior is the same in 2.0.0, 2.0.1, 2.1.0, 2.1.1, 3.0.0 and 3.0.1.

Ideas:

  • Port the JDBC 3.1.0 change: make verifyOpenedConnectionType (and endpointSubstitutionRole) effective in the initial connection strategy plugin.
  • Verify the role once per physical connection. mysql2's pool emits 'connection' once per new physical connection, separately from 'acquire' on every checkout. Alternatively, cache the verified role per instance pool, since failover is detected separately by the failover plugins.
  • Avoid running the full connect pipeline for a connection that is already pooled and verified, which would also remove the per-query CPU cost.

Questions:

  1. Is verifying the role on every pool.query() intended?
  2. Is there a supported configuration today to avoid it while keeping initialConnection and failover2?
  3. If not, is the JDBC 3.1.0 behavior planned for the Node.js wrapper?
Additional Information/Context
  • In the Java, Python and Go wrappers, the pool (e.g. HikariCP, SQLAlchemy QueuePool, database/sql) keeps wrapper connection objects, so the role is verified once when a connection is created.
  • The pool was released with 2.0.0 as the resolution of #490 ("Please let us know if you encountered any issues when trying out the feature").
  • A possible workaround is pooling long-lived AwsMySQLClient instances in the application, as HikariCP does with JDBC wrapper connections. Measurement C above shows this removes the extra queries. We would prefer a supported option.
The AWS Advanced NodeJs Wrapper version used

3.0.1 (same code path since 2.0.0)

Node version used

v24.22.2 (mysql2 3.24.6)

Operating System and version

Linux; also reproduced on macOS

Ngôn ngữ chính
TypeScript
Star
38
Fork
17
Merge trung bình
3 ngày 11 giờ
Pull request đã merge (30 ngày)
4

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của aws/aws-advanced-nodejs-wrapper

Tất cả issue của aws/aws-advanced-nodejs-wrapper

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.