Skip to content

Commit 6ca4850

Browse files
committed
chore: set statement_timeout as /query params
1 parent 3d9fea4 commit 6ca4850

File tree

4 files changed

+58
-10
lines changed

4 files changed

+58
-10
lines changed

src/lib/PostgresMeta.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import { init } from './db.js'
2222
import { PostgresMetaResult, PoolConfig } from './types.js'
2323

2424
export default class PostgresMeta {
25-
query: (sql: string, trackQueryInSentry?: boolean) => Promise<PostgresMetaResult<any>>
25+
query: (
26+
sql: string,
27+
opts?: { statementQueryTimeout?: number; trackQueryInSentry?: boolean }
28+
) => Promise<PostgresMetaResult<any>>
2629
end: () => Promise<void>
2730
columnPrivileges: PostgresMetaColumnPrivileges
2831
columns: PostgresMetaColumns

src/lib/db.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ import pg from 'pg'
22
import * as Sentry from '@sentry/node'
33
import { parse as parseArray } from 'postgres-array'
44
import { PostgresMetaResult, PoolConfig } from './types.js'
5-
import { PG_STATEMENT_TIMEOUT_SECS } from '../server/constants.js'
6-
7-
const STATEMENT_TIMEOUT_QUERY_PREFIX = `SET statement_timeout='${PG_STATEMENT_TIMEOUT_SECS}s';`
85

96
pg.types.setTypeParser(pg.types.builtins.INT8, (x) => {
107
const asNumber = Number(x)
@@ -65,7 +62,10 @@ const poolerQueryHandleError = (pgpool: pg.Pool, sql: string): Promise<pg.QueryR
6562
}
6663

6764
export const init: (config: PoolConfig) => {
68-
query: (sql: string, trackQueryInSentry?: boolean) => Promise<PostgresMetaResult<any>>
65+
query: (
66+
sql: string,
67+
opts?: { statementQueryTimeout?: number; trackQueryInSentry?: boolean }
68+
) => Promise<PostgresMetaResult<any>>
6969
end: () => Promise<void>
7070
} = (config) => {
7171
return Sentry.startSpan({ op: 'db', name: 'db.init' }, () => {
@@ -106,7 +106,10 @@ export const init: (config: PoolConfig) => {
106106
let pool: pg.Pool | null = new pg.Pool(config)
107107

108108
return {
109-
async query(sql, trackQueryInSentry = true) {
109+
async query(
110+
sql,
111+
{ statementQueryTimeout, trackQueryInSentry } = { trackQueryInSentry: true }
112+
) {
110113
return Sentry.startSpan(
111114
// For metrics purposes, log the query that will be run if it's not an user provided query (with possibly sentitives infos)
112115
{
@@ -115,11 +118,14 @@ export const init: (config: PoolConfig) => {
115118
attributes: { sql: trackQueryInSentry ? sql : 'custom' },
116119
},
117120
async () => {
121+
const statementTimeoutQueryPrefix = statementQueryTimeout
122+
? `SET statement_timeout='${statementQueryTimeout}s';`
123+
: ''
118124
// node-postgres need a statement_timeout to kill the connection when timeout is reached
119125
// otherwise the query will keep running on the database even if query timeout was reached
120126
// This need to be added at query and not connection level because poolers (pgbouncer) doesn't
121127
// allow to set this parameter at connection time
122-
const sqlWithStatementTimeout = `${STATEMENT_TIMEOUT_QUERY_PREFIX}${sql}`
128+
const sqlWithStatementTimeout = `${statementTimeoutQueryPrefix}${sql}`
123129
try {
124130
if (!pool) {
125131
const pool = new pg.Pool(config)
@@ -156,8 +162,7 @@ export const init: (config: PoolConfig) => {
156162
if (error.position) {
157163
// error.position is 1-based
158164
// we also remove our `SET statement_timeout = 'XXs';\n` from the position
159-
const position =
160-
Number(error.position) - 1 - STATEMENT_TIMEOUT_QUERY_PREFIX.length
165+
const position = Number(error.position) - 1 - statementTimeoutQueryPrefix.length
161166
// we set the new error position
162167
error.position = `${position + 1}`
163168

src/server/routes/query.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ export default async (fastify: FastifyInstance) => {
1919
Body: {
2020
query: string
2121
}
22+
Querystring: {
23+
statementTimeoutSecs?: number
24+
}
2225
}>('/', async (request, reply) => {
26+
const statementTimeoutSecs = request.query.statementTimeoutSecs
2327
errorOnEmptyQuery(request)
2428
const config = createConnectionConfig(request)
2529
const pgMeta = new PostgresMeta(config)
26-
const { data, error } = await pgMeta.query(request.body.query, false)
30+
const { data, error } = await pgMeta.query(request.body.query, {
31+
trackQueryInSentry: true,
32+
statementQueryTimeout: statementTimeoutSecs,
33+
})
2734
await pgMeta.end()
2835
if (error) {
2936
request.log.error({ error, request: extractRequestForLogging(request) })

test/server/query-timeout.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('test query timeout', () => {
1313
const res = await app.inject({
1414
method: 'POST',
1515
path: '/query',
16+
query: `statementTimeoutSecs=${TIMEOUT - 2}`,
1617
payload: {
1718
query,
1819
},
@@ -36,4 +37,36 @@ describe('test query timeout', () => {
3637
},
3738
TIMEOUT * 1000
3839
)
40+
41+
test(
42+
'query without timeout parameter should not have timeout',
43+
async () => {
44+
const query = `SELECT pg_sleep(${TIMEOUT});`
45+
// Execute a query that will sleep for 10 seconds without specifying timeout
46+
const res = await app.inject({
47+
method: 'POST',
48+
path: '/query',
49+
payload: {
50+
query,
51+
},
52+
})
53+
54+
// Check that we get the proper timeout error response
55+
expect(res.statusCode).toBe(408) // Request Timeout
56+
expect(res.json()).toMatchObject({
57+
error: expect.stringContaining('Query read timeout'),
58+
})
59+
// wait one second
60+
await new Promise((resolve) => setTimeout(resolve, 1000))
61+
62+
// Verify that the connection has not been cleaned up sinice there is no statementTimetout
63+
const connectionsRes = await pgMeta.query(`
64+
SELECT * FROM pg_stat_activity where application_name = 'postgres-meta 0.0.0-automated' and query ILIKE '%${query}%';
65+
`)
66+
67+
// Should have no active connections except for our current query
68+
expect(connectionsRes.data).toHaveLength(1)
69+
},
70+
TIMEOUT * 1000
71+
)
3972
})

0 commit comments

Comments
 (0)