@pxp-nd/core 1.2.88 → 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 +61 -15
- 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 +
|
|
@@ -164,7 +171,8 @@ class Controller {
|
|
|
164
171
|
// Execute our method for this path and pass our express request and response object.
|
|
165
172
|
// const params = { ...req.query, ...req.body, ...req.params };
|
|
166
173
|
const params = Object.assign(Object.assign({}, req.files), req.paramasMerge);
|
|
167
|
-
const controllerForRequest = Object.
|
|
174
|
+
const controllerForRequest = Object.create(Object.getPrototypeOf(this));
|
|
175
|
+
Object.assign(controllerForRequest, this);
|
|
168
176
|
controllerForRequest.user = req.user || null;
|
|
169
177
|
controllerForRequest.headers = req.headers;
|
|
170
178
|
controllerForRequest.pxpParams = req.pxpParams;
|
|
@@ -174,13 +182,13 @@ class Controller {
|
|
|
174
182
|
.toLowerCase();
|
|
175
183
|
controllerForRequest.validated = false;
|
|
176
184
|
try {
|
|
177
|
-
await index_1.__(
|
|
185
|
+
await index_1.__(controllerForRequest.genericMethodWrapper(params, req, next, res, route.methodName, methodDbSettings, readOnlyMethod, false, logKey, logValue, isHtmlMethod, isFileMethod, timeoutMethod));
|
|
178
186
|
}
|
|
179
187
|
catch (ex) {
|
|
180
188
|
const now = new Date();
|
|
181
189
|
const iniAt = req.start;
|
|
182
190
|
const endsAt = now.valueOf() - iniAt.valueOf();
|
|
183
|
-
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
|
|
184
192
|
params, ex.stack, ex.statusCode, endsAt, logValue));
|
|
185
193
|
index_1.errorMiddleware(ex, req, res);
|
|
186
194
|
}
|
|
@@ -197,7 +205,8 @@ class Controller {
|
|
|
197
205
|
], async (req, res, next) => {
|
|
198
206
|
// Execute our method for this path and pass our express request and response object.
|
|
199
207
|
const params = Object.assign(Object.assign({}, req.files), req.paramasMerge);
|
|
200
|
-
const controllerForRequest = Object.
|
|
208
|
+
const controllerForRequest = Object.create(Object.getPrototypeOf(this));
|
|
209
|
+
Object.assign(controllerForRequest, this);
|
|
201
210
|
controllerForRequest.user = req.user || null;
|
|
202
211
|
controllerForRequest.headers = req.headers;
|
|
203
212
|
controllerForRequest.pxpParams = req.pxpParams;
|
|
@@ -207,13 +216,13 @@ class Controller {
|
|
|
207
216
|
.toLowerCase();
|
|
208
217
|
controllerForRequest.validated = false;
|
|
209
218
|
try {
|
|
210
|
-
await index_1.__(
|
|
219
|
+
await index_1.__(controllerForRequest.genericMethodWrapper(params, req, next, res, route.methodName, methodDbSettings, readOnlyMethod, permission[route.methodName], logKey, logValue, isHtmlMethod, isFileMethod, timeoutMethod));
|
|
211
220
|
}
|
|
212
221
|
catch (ex) {
|
|
213
222
|
const now = new Date();
|
|
214
223
|
const iniAt = req.start;
|
|
215
224
|
const endsAt = now.valueOf() - iniAt.valueOf();
|
|
216
|
-
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
|
|
217
226
|
params, ex.stack, ex.statusCode, endsAt, logValue)));
|
|
218
227
|
index_1.errorMiddleware(ex, req, res);
|
|
219
228
|
}
|
|
@@ -221,9 +230,9 @@ class Controller {
|
|
|
221
230
|
}
|
|
222
231
|
});
|
|
223
232
|
}
|
|
224
|
-
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) {
|
|
225
234
|
if (dbsettings === 'Orm') {
|
|
226
|
-
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));
|
|
227
236
|
}
|
|
228
237
|
else if (dbsettings === 'Procedure') {
|
|
229
238
|
await index_1.__(this.procedureMethodWrapper(params, next, res, methodName, readonly, permission, log, logConfig));
|
|
@@ -232,7 +241,8 @@ class Controller {
|
|
|
232
241
|
await this.sqlMethodWrapper(params, next, res, methodName, readonly, permission, log, logConfig);
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
|
-
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;
|
|
236
246
|
let metResponse;
|
|
237
247
|
if (permission) {
|
|
238
248
|
if (this.user) {
|
|
@@ -248,19 +258,55 @@ class Controller {
|
|
|
248
258
|
else {
|
|
249
259
|
const connection = typeorm_1.getConnection(process.env.DB_WRITE_CONNECTION_NAME);
|
|
250
260
|
const queryRunner = connection.createQueryRunner();
|
|
251
|
-
// establish real database connection using our new query runner
|
|
252
|
-
|
|
253
|
-
|
|
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;
|
|
254
267
|
try {
|
|
255
|
-
|
|
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
|
+
}
|
|
256
285
|
await queryRunner.commitTransaction();
|
|
257
286
|
}
|
|
258
287
|
catch (err) {
|
|
259
|
-
|
|
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
|
+
}
|
|
260
297
|
throw err;
|
|
261
298
|
}
|
|
262
299
|
finally {
|
|
263
|
-
|
|
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
|
+
}
|
|
264
310
|
}
|
|
265
311
|
}
|
|
266
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;
|