feat: Add query timeout support for MySql (#10846)

Add "enableQueryTimeout" option to MysqlConnectionOptions. When enabled the value of "maxQueryExecutionTime" will be passed to mysql driver as query timeout.

---------

Co-authored-by: Mike Guida <mike@mguida.com>
This commit is contained in:
iliagrvch 2025-04-15 21:17:04 +03:00 committed by GitHub
parent 45577df8b7
commit 046aebe696
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 129 additions and 3 deletions

View File

@ -162,6 +162,11 @@ Different RDBMS-es have their own specific options.
- `ssl` - object with SSL parameters or a string containing the name of the SSL profile.
See [SSL options](https://github.com/mysqljs/mysql#ssl-options).
- `enableQueryTimeout` - If a value is specified for maxQueryExecutionTime, in addition to generating a warning log when a
query exceeds this time limit,
the specified maxQueryExecutionTime value is also used as the timeout for the query.
For more information, check https://github.com/mysqljs/mysql?tab=readme-ov-file#timeouts
## `postgres` / `cockroachdb` data source options
- `url` - Connection url where the connection is performed. Please note that other data source options will override parameters set from url.
@ -242,6 +247,7 @@ Different RDBMS-es have their own specific options.
- `database` - Database name
## `mssql` data source options
Based on [tedious](https://tediousjs.github.io/node-mssql/) MSSQL implementation. See [SqlServerConnectionOptions.ts](..\src\driver\sqlserver\SqlServerConnectionOptions.ts) for details on exposed attributes.
- `url` - Connection url where the connection is performed. Please note that other data source options will override parameters set from url.
@ -547,10 +553,10 @@ The following TNS connection string will be used in the next explanations:
(SERVER=shared)))
)
```
- `sid` - The System Identifier (SID) identifies a specific database instance. For example, "sales".
- `serviceName` - The Service Name is an identifier of a database service. For example, `sales.us.example.com`.
## Data Source Options example
Here is a small example of data source options for mysql:

View File

@ -109,6 +109,13 @@ export interface MysqlConnectionOptions
*/
readonly connectorPackage?: "mysql" | "mysql2"
/**
* If a value is specified for maxQueryExecutionTime, in addition to generating a warning log when a query exceeds this time limit,
* the specified maxQueryExecutionTime value is also used as the timeout for the query.
* For more information, check https://github.com/mysqljs/mysql?tab=readme-ov-file#timeouts
*/
readonly enableQueryTimeout?: boolean
/**
* Replication setup.
*/

View File

@ -199,10 +199,17 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
query,
parameters,
)
const enableQueryTimeout =
this.driver.options.enableQueryTimeout
const maxQueryExecutionTime =
this.driver.options.maxQueryExecutionTime
const queryPayload =
enableQueryTimeout && maxQueryExecutionTime
? { sql: query, timeout: maxQueryExecutionTime }
: query
const queryStartTime = +new Date()
databaseConnection.query(
query,
queryPayload,
parameters,
async (err: any, raw: any) => {
// log slow queries if maxQueryExecution time is set

View File

@ -0,0 +1,106 @@
import { expect } from "chai"
import "reflect-metadata"
import { DataSource } from "../../../../../src"
import {
TestingOptions,
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../../../utils/test-utils"
describe("mysql driver > enableQueryTimeout connection option", () => {
let dataSources: DataSource[]
const commonConnectionOptions: TestingOptions = {
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchema: true,
enabledDrivers: ["mysql"],
}
const timeoutMs = 10
const longQueryTimeSec = 0.02
const shortQueryTimeSec = 0.005
describe("when enableQueryTimeout is true", () => {
before(async () => {
dataSources = await createTestingConnections({
...commonConnectionOptions,
driverSpecific: {
enableQueryTimeout: true,
maxQueryExecutionTime: timeoutMs,
},
})
})
beforeEach(() => reloadTestingDatabases(dataSources))
after(() => closeTestingConnections(dataSources))
it("should throw a query execution timeout error for the query when it exceeds the maxQueryExecutionTime", async () => {
await Promise.all(
dataSources.map(async (dataSource) => {
let errorThrown = false
try {
await dataSource.manager.query(
`SELECT SLEEP(${longQueryTimeSec})`,
)
} catch (err) {
errorThrown = true
expect(err).to.have.nested.property(
"driverError.code",
"PROTOCOL_SEQUENCE_TIMEOUT",
)
expect(err).to.have.nested.property(
"driverError.timeout",
timeoutMs,
)
}
expect(errorThrown).to.be.true
}),
)
})
it("should not throw a query execution timeout error for the query when it runs within the maxQueryExecutionTime", async () => {
await Promise.all(
dataSources.map(async (dataSource) => {
let errorThrown = false
try {
await dataSource.manager.query(
`SELECT SLEEP(${shortQueryTimeSec})`,
)
} catch (err) {
errorThrown = true
}
expect(errorThrown).to.be.false
}),
)
})
})
describe("when enableQueryTimeout is not provided", () => {
let datasources: DataSource[]
before(async () => {
datasources = await createTestingConnections({
...commonConnectionOptions,
driverSpecific: { maxQueryExecutionTime: timeoutMs },
})
})
after(() => closeTestingConnections(datasources))
it("should not throw a query execution timeout error", () => {
Promise.all(
datasources.map(async (dataSource) => {
let errorThrown = false
try {
await dataSource.manager.query(
`SELECT SLEEP(${longQueryTimeSec})`,
)
} catch (err) {
errorThrown = true
}
expect(errorThrown).to.be.false
}),
)
})
})
})