@pxp-nd/core 1.2.89 → 1.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -1
- package/dist/interfaces.d.ts +5 -0
- package/dist/lib/pxp/Controller.d.ts +4 -2
- package/dist/lib/pxp/Controller.js +57 -13
- package/dist/lib/pxp/Decorators.d.ts +3 -1
- package/dist/lib/pxp/Decorators.js +6 -1
- package/dist/lib/pxp/utils/DbTimeouts.d.ts +31 -0
- package/dist/lib/pxp/utils/DbTimeouts.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,2 +1,63 @@
|
|
|
1
1
|
# PXP-ND core
|
|
2
|
-
- Core module for pxp-nd
|
|
2
|
+
- Core module for pxp-nd
|
|
3
|
+
|
|
4
|
+
## 1.2.90
|
|
5
|
+
|
|
6
|
+
Release de resiliencia de conexiones a base de datos. **Sin configurar nada, el comportamiento es idéntico a 1.2.89**: todas las opciones nuevas son opt-in.
|
|
7
|
+
|
|
8
|
+
### `db.acquireTimeoutMs` (opcional)
|
|
9
|
+
|
|
10
|
+
Tiempo máximo, en milisegundos, que una ruta no-readonly espera para obtener una conexión del pool antes de fallar.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
new PxpApp({
|
|
14
|
+
// ...
|
|
15
|
+
db: { acquireTimeoutMs: 5000 },
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Si vence, la request responde **503** con el mensaje `Database acquire timeout after N ms`.
|
|
20
|
+
- Solo se dispara cuando el pool está agotado; con conexiones libres no agrega latencia.
|
|
21
|
+
- `undefined` o `0` = comportamiento legacy (espera indefinida).
|
|
22
|
+
|
|
23
|
+
### Decorador `@Timeout(ms)` y opción de ruta `timeoutMs`
|
|
24
|
+
|
|
25
|
+
Límite de duración por ruta. Se puede usar el decorador o la opción en la definición de la ruta:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
@Post('/save')
|
|
29
|
+
@Timeout(30000)
|
|
30
|
+
async save(params: any) { /* ... */ }
|
|
31
|
+
|
|
32
|
+
// o bien
|
|
33
|
+
@Post('/save', { timeoutMs: 30000 })
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Al vencer el tiempo:
|
|
37
|
+
- La request responde **503**.
|
|
38
|
+
- La conexión física se destruye, por lo que MySQL hace rollback de la transacción abierta.
|
|
39
|
+
- El handler puede seguir ejecutándose en segundo plano, pero ya sin acceso a la base de datos.
|
|
40
|
+
- Se registra la línea de log `[pxp-core] timeout-abandoned <transactionCode>`.
|
|
41
|
+
|
|
42
|
+
Sin decorador ni `timeoutMs`, la ruta no tiene límite (igual que 1.2.89).
|
|
43
|
+
|
|
44
|
+
### Fix: fuga de conexiones si falla `startTransaction()`
|
|
45
|
+
|
|
46
|
+
Antes, si `startTransaction()` lanzaba un error, la conexión obtenida del pool nunca se liberaba y con el tiempo el pool quedaba agotado. Ahora la conexión se libera siempre.
|
|
47
|
+
|
|
48
|
+
### Clon del controlador por request (`controllerForRequest`)
|
|
49
|
+
|
|
50
|
+
Portado desde 1.2.89: cada request trabaja sobre un clon del controlador, de modo que requests concurrentes ya no se pisan `user`, `headers` ni `transactionCode`.
|
|
51
|
+
|
|
52
|
+
### Proyectos afectados
|
|
53
|
+
|
|
54
|
+
Dependen de core con `^1.2.x`, así que tomarán esta versión en su próximo `npm install`:
|
|
55
|
+
|
|
56
|
+
- tempo
|
|
57
|
+
- conductor-nd
|
|
58
|
+
- go-pxp-nd
|
|
59
|
+
- Api_Boa_ND
|
|
60
|
+
- electrica360_nd
|
|
61
|
+
- cleaners-pxp-nd
|
|
62
|
+
|
|
63
|
+
Sin configurar `db.acquireTimeoutMs` ni `@Timeout`/`timeoutMs`, ninguno de ellos cambia de comportamiento respecto a 1.2.89.
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -49,5 +49,10 @@ export interface IConfigPxpApp {
|
|
|
49
49
|
expressJsonConfig?: IConfigExpress;
|
|
50
50
|
expressUrlEncodedConfig?: IConfigExpress;
|
|
51
51
|
permissionFunction?: boolean | ((user: any, transaction: string) => Promise<boolean>);
|
|
52
|
+
/** Database safety options. All optional; when omitted the behaviour is identical to <=1.2.89. */
|
|
53
|
+
db?: {
|
|
54
|
+
/** Max ms to wait for a pool connection before a non-readonly route fails with 503. Undefined/0 = wait forever (legacy). */
|
|
55
|
+
acquireTimeoutMs?: number;
|
|
56
|
+
};
|
|
52
57
|
}
|
|
53
58
|
export {};
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* Last modified : 2020-10-13 15:36:31
|
|
13
13
|
* Last modified : 2021-03-10 15:36:31 - Favio Figueroa
|
|
14
14
|
* Last modified : 2021-05-01 18:35:31 - Favio Figueroa
|
|
15
|
+
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 per-request controller clone (from 1.2.89), startTransaction inside try
|
|
16
|
+
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 acquire timeout + @Timeout decorator
|
|
15
17
|
*/
|
|
16
18
|
import { EntityManager } from 'typeorm';
|
|
17
19
|
import { Request, Response, NextFunction } from 'express';
|
|
@@ -37,8 +39,8 @@ export declare class Controller implements ControllerInterface {
|
|
|
37
39
|
private basicReadOnly;
|
|
38
40
|
constructor(module: string, Entity?: any, config?: IConfigPxpApp, io?: any);
|
|
39
41
|
private initializeRoutes;
|
|
40
|
-
genericMethodWrapper(params: Record<string, unknown>, req: Request, next: NextFunction, res: Response, methodName: string, dbsettings: string, readonly: boolean, permission: boolean, log: boolean, logConfig: {}, isHtml: boolean, isFile: boolean): Promise<void>;
|
|
41
|
-
ormMethodWrapper(params: Record<string, unknown>, req: Request | any, next: NextFunction, res: Response, methodName: string, readonly: boolean, permission?: boolean, log?: boolean, logConfig?: {}, isHtml?: boolean, isFile?: boolean): Promise<void>;
|
|
42
|
+
genericMethodWrapper(params: Record<string, unknown>, req: Request, next: NextFunction, res: Response, methodName: string, dbsettings: string, readonly: boolean, permission: boolean, log: boolean, logConfig: {}, isHtml: boolean, isFile: boolean, timeoutMs?: number): Promise<void>;
|
|
43
|
+
ormMethodWrapper(params: Record<string, unknown>, req: Request | any, next: NextFunction, res: Response, methodName: string, readonly: boolean, permission?: boolean, log?: boolean, logConfig?: {}, isHtml?: boolean, isFile?: boolean, timeoutMs?: number): Promise<void>;
|
|
42
44
|
list(params: Record<string, unknown>): Promise<unknown>;
|
|
43
45
|
add(params: Record<string, unknown>, manager: EntityManager): Promise<unknown>;
|
|
44
46
|
edit(params: Record<string, unknown>, manager: EntityManager): Promise<unknown>;
|
|
@@ -37,6 +37,8 @@ exports.Controller = void 0;
|
|
|
37
37
|
* Last modified : 2020-10-13 15:36:31
|
|
38
38
|
* Last modified : 2021-03-10 15:36:31 - Favio Figueroa
|
|
39
39
|
* Last modified : 2021-05-01 18:35:31 - Favio Figueroa
|
|
40
|
+
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 per-request controller clone (from 1.2.89), startTransaction inside try
|
|
41
|
+
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 acquire timeout + @Timeout decorator
|
|
40
42
|
*/
|
|
41
43
|
const typeorm_1 = require("typeorm");
|
|
42
44
|
const class_validator_1 = require("class-validator");
|
|
@@ -51,6 +53,7 @@ const ParseParams_1 = require("./middlewares/ParseParams");
|
|
|
51
53
|
const isReportMiddleware_1 = require("./middlewares/isReportMiddleware");
|
|
52
54
|
const pdf_1 = require("../reports/pdf");
|
|
53
55
|
const xlsx_1 = require("../reports/xlsx");
|
|
56
|
+
const DbTimeouts_1 = require("./utils/DbTimeouts");
|
|
54
57
|
// import * as entities from '@pxp-nd/entities';
|
|
55
58
|
// const pxpEntities: any = entities;
|
|
56
59
|
class Controller {
|
|
@@ -128,6 +131,8 @@ class Controller {
|
|
|
128
131
|
// get type return
|
|
129
132
|
let ishtml = Reflect.getMetadata('ishtml', this.constructor) || {};
|
|
130
133
|
let isfile = Reflect.getMetadata('isfile', this.constructor) || {};
|
|
134
|
+
// get request timeouts (@Timeout)
|
|
135
|
+
const timeouts = Reflect.getMetadata('timeout', this.constructor) || {};
|
|
131
136
|
const defaultConfig = Reflect.getMetadata('optionsRoute', this.constructor) || {};
|
|
132
137
|
// define basic routes
|
|
133
138
|
if (this.modelString !== '') {
|
|
@@ -146,6 +151,8 @@ class Controller {
|
|
|
146
151
|
const isHtmlMethod = ishtml[route.methodName] !== undefined ? ishtml[route.methodName] : (defaultConfig[route.methodName] ? defaultConfig[route.methodName].ishtml : false);
|
|
147
152
|
// is file
|
|
148
153
|
const isFileMethod = isfile[route.methodName] !== undefined ? isfile[route.methodName] : (defaultConfig[route.methodName] ? defaultConfig[route.methodName].isfile : false);
|
|
154
|
+
// request timeout (ms), undefined = no timeout
|
|
155
|
+
const timeoutMethod = timeouts[route.methodName] !== undefined ? timeouts[route.methodName] : (defaultConfig[route.methodName] ? defaultConfig[route.methodName].timeoutMs : undefined);
|
|
149
156
|
if (readOnlyMethod === null || readOnlyMethod === undefined) {
|
|
150
157
|
throw new index_1.PxpError(500, 'ReadOnly decorator was not defined for ' +
|
|
151
158
|
route.methodName +
|
|
@@ -175,13 +182,13 @@ class Controller {
|
|
|
175
182
|
.toLowerCase();
|
|
176
183
|
controllerForRequest.validated = false;
|
|
177
184
|
try {
|
|
178
|
-
await index_1.__(controllerForRequest.genericMethodWrapper(params, req, next, res, route.methodName, methodDbSettings, readOnlyMethod, false, logKey, logValue, isHtmlMethod, isFileMethod));
|
|
185
|
+
await index_1.__(controllerForRequest.genericMethodWrapper(params, req, next, res, route.methodName, methodDbSettings, readOnlyMethod, false, logKey, logValue, isHtmlMethod, isFileMethod, timeoutMethod));
|
|
179
186
|
}
|
|
180
187
|
catch (ex) {
|
|
181
188
|
const now = new Date();
|
|
182
189
|
const iniAt = req.start;
|
|
183
190
|
const endsAt = now.valueOf() - iniAt.valueOf();
|
|
184
|
-
res.logId = await index_1.__(index_1.insertLog('nouser', 'mac', req.ip, 'error', ex.tecMessage,
|
|
191
|
+
res.logId = await index_1.__(index_1.insertLog('nouser', 'mac', req.ip, 'error', ex.tecMessage, controllerForRequest.module, controllerForRequest.transactionCode, '', // query
|
|
185
192
|
params, ex.stack, ex.statusCode, endsAt, logValue));
|
|
186
193
|
index_1.errorMiddleware(ex, req, res);
|
|
187
194
|
}
|
|
@@ -209,13 +216,13 @@ class Controller {
|
|
|
209
216
|
.toLowerCase();
|
|
210
217
|
controllerForRequest.validated = false;
|
|
211
218
|
try {
|
|
212
|
-
await index_1.__(controllerForRequest.genericMethodWrapper(params, req, next, res, route.methodName, methodDbSettings, readOnlyMethod, permission[route.methodName], logKey, logValue, isHtmlMethod, isFileMethod));
|
|
219
|
+
await index_1.__(controllerForRequest.genericMethodWrapper(params, req, next, res, route.methodName, methodDbSettings, readOnlyMethod, permission[route.methodName], logKey, logValue, isHtmlMethod, isFileMethod, timeoutMethod));
|
|
213
220
|
}
|
|
214
221
|
catch (ex) {
|
|
215
222
|
const now = new Date();
|
|
216
223
|
const iniAt = req.start;
|
|
217
224
|
const endsAt = now.valueOf() - iniAt.valueOf();
|
|
218
|
-
res.logId = (await index_1.__(index_1.insertLog(controllerForRequest.user && controllerForRequest.user.username ? controllerForRequest.user.username : 'nouser', 'mac', req.ip, 'error', ex.tecMessage,
|
|
225
|
+
res.logId = (await index_1.__(index_1.insertLog(controllerForRequest.user && controllerForRequest.user.username ? controllerForRequest.user.username : 'nouser', 'mac', req.ip, 'error', ex.tecMessage, controllerForRequest.module, controllerForRequest.transactionCode, '', // query
|
|
219
226
|
params, ex.stack, ex.statusCode, endsAt, logValue)));
|
|
220
227
|
index_1.errorMiddleware(ex, req, res);
|
|
221
228
|
}
|
|
@@ -223,9 +230,9 @@ class Controller {
|
|
|
223
230
|
}
|
|
224
231
|
});
|
|
225
232
|
}
|
|
226
|
-
async genericMethodWrapper(params, req, next, res, methodName, dbsettings, readonly, permission = true, log = true, logConfig = {}, isHtml, isFile) {
|
|
233
|
+
async genericMethodWrapper(params, req, next, res, methodName, dbsettings, readonly, permission = true, log = true, logConfig = {}, isHtml, isFile, timeoutMs) {
|
|
227
234
|
if (dbsettings === 'Orm') {
|
|
228
|
-
await index_1.__(this.ormMethodWrapper(params, req, next, res, methodName, readonly, permission, log, logConfig, isHtml, isFile));
|
|
235
|
+
await index_1.__(this.ormMethodWrapper(params, req, next, res, methodName, readonly, permission, log, logConfig, isHtml, isFile, timeoutMs));
|
|
229
236
|
}
|
|
230
237
|
else if (dbsettings === 'Procedure') {
|
|
231
238
|
await index_1.__(this.procedureMethodWrapper(params, next, res, methodName, readonly, permission, log, logConfig));
|
|
@@ -234,7 +241,8 @@ class Controller {
|
|
|
234
241
|
await this.sqlMethodWrapper(params, next, res, methodName, readonly, permission, log, logConfig);
|
|
235
242
|
}
|
|
236
243
|
}
|
|
237
|
-
async ormMethodWrapper(params, req, next, res, methodName, readonly, permission = true, log = true, logConfig = {}, isHtml = false, isFile = false) {
|
|
244
|
+
async ormMethodWrapper(params, req, next, res, methodName, readonly, permission = true, log = true, logConfig = {}, isHtml = false, isFile = false, timeoutMs) {
|
|
245
|
+
var _a;
|
|
238
246
|
let metResponse;
|
|
239
247
|
if (permission) {
|
|
240
248
|
if (this.user) {
|
|
@@ -250,19 +258,55 @@ class Controller {
|
|
|
250
258
|
else {
|
|
251
259
|
const connection = typeorm_1.getConnection(process.env.DB_WRITE_CONNECTION_NAME);
|
|
252
260
|
const queryRunner = connection.createQueryRunner();
|
|
253
|
-
// establish real database connection using our new query runner
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
// establish real database connection using our new query runner.
|
|
262
|
+
// `db` is added to IConfigPxpApp by a later change; without it this is a plain connect().
|
|
263
|
+
const acquireTimeoutMs = (_a = this.config.db) === null || _a === void 0 ? void 0 : _a.acquireTimeoutMs;
|
|
264
|
+
await index_1.__(DbTimeouts_1.connectWithTimeout(queryRunner, acquireTimeoutMs, this.transactionCode));
|
|
265
|
+
let timedOut = false;
|
|
266
|
+
let timer;
|
|
256
267
|
try {
|
|
257
|
-
|
|
268
|
+
// Start the transaction inside try so a failure here still releases the connection
|
|
269
|
+
await index_1.__(queryRunner.startTransaction());
|
|
270
|
+
const work = Promise.resolve(eval(`this.${methodName}(params, queryRunner.manager, res, req)`));
|
|
271
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
272
|
+
metResponse = await Promise.race([
|
|
273
|
+
work,
|
|
274
|
+
new Promise((_, reject) => {
|
|
275
|
+
timer = setTimeout(() => {
|
|
276
|
+
timedOut = true;
|
|
277
|
+
reject(new DbTimeouts_1.DbTimeoutError('request', timeoutMs, this.transactionCode));
|
|
278
|
+
}, timeoutMs);
|
|
279
|
+
})
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
metResponse = await work;
|
|
284
|
+
}
|
|
258
285
|
await queryRunner.commitTransaction();
|
|
259
286
|
}
|
|
260
287
|
catch (err) {
|
|
261
|
-
|
|
288
|
+
// On request timeout skip rollback: it would queue behind the abandoned statement and delay the 503 (destroy rolls back server-side).
|
|
289
|
+
if (!timedOut && queryRunner.isTransactionActive) {
|
|
290
|
+
try {
|
|
291
|
+
await queryRunner.rollbackTransaction();
|
|
292
|
+
}
|
|
293
|
+
catch (_) {
|
|
294
|
+
// keep the original error; the connection is released or destroyed below
|
|
295
|
+
}
|
|
296
|
+
}
|
|
262
297
|
throw err;
|
|
263
298
|
}
|
|
264
299
|
finally {
|
|
265
|
-
|
|
300
|
+
if (timer)
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
if (timedOut) {
|
|
303
|
+
// The handler may keep running in background; its connection is destroyed so it cannot commit.
|
|
304
|
+
console.warn(`[pxp-core] timeout-abandoned ${this.transactionCode}`);
|
|
305
|
+
await DbTimeouts_1.destroyQueryRunner(queryRunner);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
await index_1.__(queryRunner.release());
|
|
309
|
+
}
|
|
266
310
|
}
|
|
267
311
|
}
|
|
268
312
|
if (log) {
|
|
@@ -8,6 +8,7 @@ export declare interface IOptionsRoute {
|
|
|
8
8
|
dbSettings?: 'Procedure' | 'Orm' | 'Query';
|
|
9
9
|
authentication?: boolean;
|
|
10
10
|
log?: boolean | ILog;
|
|
11
|
+
timeoutMs?: number;
|
|
11
12
|
}
|
|
12
13
|
declare const Get: (path?: string, options?: IOptionsRoute) => (target: any, propertyKey: string) => void;
|
|
13
14
|
declare const Post: (path?: string, options?: IOptionsRoute) => (target: any, propertyKey: string) => void;
|
|
@@ -19,9 +20,10 @@ declare const Model: (model: string) => ClassDecorator;
|
|
|
19
20
|
declare const StoredProcedure: (storedProcedure: string) => ClassDecorator;
|
|
20
21
|
declare const Authentication: (authentication?: boolean) => (target: any, propertyKey: string) => void;
|
|
21
22
|
declare const ReadOnly: (ronly?: boolean) => (target: any, propertyKey: string) => void;
|
|
23
|
+
declare const Timeout: (ms: number) => (target: any, propertyKey: string) => void;
|
|
22
24
|
declare const IsHtml: (ishtml?: boolean) => (target: any, propertyKey: string) => void;
|
|
23
25
|
declare const IsFile: (isFile?: boolean) => (target: any, propertyKey: string) => void;
|
|
24
26
|
declare const DbSettings: (modelType: 'Procedure' | 'Orm' | 'Query') => (target: any, propertyKey: string) => void;
|
|
25
27
|
declare const Permission: (permission?: boolean) => (target: any, propertyKey: string) => void;
|
|
26
28
|
declare const Log: (log?: boolean, config?: Record<string, unknown>) => (target: any, propertyKey: string) => void;
|
|
27
|
-
export { Get, Post, Put, Delete, Patch, Route, Authentication, Log, Permission, DbSettings, ReadOnly, IsHtml, IsFile, Model, StoredProcedure };
|
|
29
|
+
export { Get, Post, Put, Delete, Patch, Route, Authentication, Log, Permission, DbSettings, ReadOnly, Timeout, IsHtml, IsFile, Model, StoredProcedure };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StoredProcedure = exports.Model = exports.IsFile = exports.IsHtml = exports.ReadOnly = exports.DbSettings = exports.Permission = exports.Log = exports.Authentication = exports.Route = exports.Patch = exports.Delete = exports.Put = exports.Post = exports.Get = void 0;
|
|
3
|
+
exports.StoredProcedure = exports.Model = exports.IsFile = exports.IsHtml = exports.Timeout = exports.ReadOnly = exports.DbSettings = exports.Permission = exports.Log = exports.Authentication = exports.Route = exports.Patch = exports.Delete = exports.Put = exports.Post = exports.Get = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Kplian Ltda 2020
|
|
6
6
|
*
|
|
@@ -13,6 +13,7 @@ exports.StoredProcedure = exports.Model = exports.IsFile = exports.IsHtml = expo
|
|
|
13
13
|
*
|
|
14
14
|
* Created at : 2020-06-13 18:09:48
|
|
15
15
|
* Last modified : 2020-09-17 18:29:51
|
|
16
|
+
* Last modified : 2026-10-08 - Favio Figueroa - EF-23 acquire timeout + @Timeout decorator
|
|
16
17
|
*/
|
|
17
18
|
const RouteDefinition_1 = require("./RouteDefinition");
|
|
18
19
|
;
|
|
@@ -102,6 +103,10 @@ const Authentication = (authentication = true) => (target, propertyKey) => setPr
|
|
|
102
103
|
exports.Authentication = Authentication;
|
|
103
104
|
const ReadOnly = (ronly = true) => (target, propertyKey) => setProperty(target, propertyKey)(ronly, 'readonly');
|
|
104
105
|
exports.ReadOnly = ReadOnly;
|
|
106
|
+
// Request timeout (ms) for a write transaction: on expiry the request fails with 503,
|
|
107
|
+
// the transaction is rolled back and the physical connection is destroyed.
|
|
108
|
+
const Timeout = (ms) => (target, propertyKey) => setProperty(target, propertyKey)(ms, 'timeout');
|
|
109
|
+
exports.Timeout = Timeout;
|
|
105
110
|
const IsHtml = (ishtml = false) => (target, propertyKey) => setProperty(target, propertyKey)(ishtml, 'ishtml');
|
|
106
111
|
exports.IsHtml = IsHtml;
|
|
107
112
|
const IsFile = (isFile = false) => (target, propertyKey) => setProperty(target, propertyKey)(isFile, 'isfile');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kplian Ltda 2020
|
|
3
|
+
*
|
|
4
|
+
* MIT
|
|
5
|
+
*
|
|
6
|
+
* Database timeout helpers.
|
|
7
|
+
*
|
|
8
|
+
* @summary Connection acquire timeout, request timeout error and physical connection destroy.
|
|
9
|
+
* @author Favio Figueroa
|
|
10
|
+
*
|
|
11
|
+
* Created at : 2026-10-08 - Favio Figueroa - EF-23 acquire timeout + @Timeout decorator
|
|
12
|
+
*/
|
|
13
|
+
import { QueryRunner } from 'typeorm';
|
|
14
|
+
import { PxpError } from '../PxpError';
|
|
15
|
+
export declare class DbTimeoutError extends PxpError {
|
|
16
|
+
kind: 'acquire' | 'request';
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
transactionCode: string | undefined;
|
|
19
|
+
constructor(kind: 'acquire' | 'request', ms: number, transactionCode?: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Destroys the physical driver connection behind the query runner (so it never
|
|
23
|
+
* goes back to the pool with an open transaction) and then releases the runner.
|
|
24
|
+
* mysql2 removes a destroyed connection from its pool on its own.
|
|
25
|
+
*/
|
|
26
|
+
export declare function destroyQueryRunner(queryRunner: QueryRunner): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Acquires the query runner connection with an upper bound.
|
|
29
|
+
* When ms is undefined or <= 0 this is exactly `await queryRunner.connect()`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function connectWithTimeout(queryRunner: QueryRunner, ms: number | undefined, transactionCode?: string): Promise<void>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connectWithTimeout = exports.destroyQueryRunner = exports.DbTimeoutError = void 0;
|
|
4
|
+
const PxpError_1 = require("../PxpError");
|
|
5
|
+
class DbTimeoutError extends PxpError_1.PxpError {
|
|
6
|
+
constructor(kind, ms, transactionCode) {
|
|
7
|
+
super(503, `Database ${kind} timeout after ${ms} ms`);
|
|
8
|
+
this.kind = kind;
|
|
9
|
+
this.timeoutMs = ms;
|
|
10
|
+
this.transactionCode = transactionCode;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.DbTimeoutError = DbTimeoutError;
|
|
14
|
+
/**
|
|
15
|
+
* Destroys the physical driver connection behind the query runner (so it never
|
|
16
|
+
* goes back to the pool with an open transaction) and then releases the runner.
|
|
17
|
+
* mysql2 removes a destroyed connection from its pool on its own.
|
|
18
|
+
*/
|
|
19
|
+
async function destroyQueryRunner(queryRunner) {
|
|
20
|
+
const databaseConnection = queryRunner.databaseConnection;
|
|
21
|
+
if (databaseConnection && typeof databaseConnection.destroy === 'function') {
|
|
22
|
+
databaseConnection.destroy();
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
await queryRunner.release();
|
|
26
|
+
}
|
|
27
|
+
catch (_) {
|
|
28
|
+
// the connection was already destroyed; release failures are expected here
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.destroyQueryRunner = destroyQueryRunner;
|
|
32
|
+
/**
|
|
33
|
+
* Acquires the query runner connection with an upper bound.
|
|
34
|
+
* When ms is undefined or <= 0 this is exactly `await queryRunner.connect()`.
|
|
35
|
+
*/
|
|
36
|
+
async function connectWithTimeout(queryRunner, ms, transactionCode) {
|
|
37
|
+
if (!ms || ms <= 0) {
|
|
38
|
+
await queryRunner.connect();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let timer;
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
const connectPromise = queryRunner.connect();
|
|
44
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
45
|
+
timer = setTimeout(() => {
|
|
46
|
+
timedOut = true;
|
|
47
|
+
reject(new DbTimeoutError('acquire', ms, transactionCode));
|
|
48
|
+
}, ms);
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (timedOut) {
|
|
55
|
+
// A late connection must be destroyed, not leaked into the pool.
|
|
56
|
+
connectPromise.then(() => destroyQueryRunner(queryRunner)).catch(() => { });
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (timer)
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.connectWithTimeout = connectWithTimeout;
|