@dangao/bun-server 1.4.0 → 1.5.0
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/dist/core/application.d.ts +27 -1
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/server.d.ts +25 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/index.js +107 -2
- package/package.json +1 -1
- package/src/core/application.ts +89 -1
- package/src/core/server.ts +114 -2
- package/tests/core/graceful-shutdown.test.ts +321 -0
|
@@ -15,6 +15,16 @@ export interface ApplicationOptions {
|
|
|
15
15
|
* 主机名
|
|
16
16
|
*/
|
|
17
17
|
hostname?: string;
|
|
18
|
+
/**
|
|
19
|
+
* 优雅停机超时时间(毫秒)
|
|
20
|
+
* 默认 30 秒
|
|
21
|
+
*/
|
|
22
|
+
gracefulShutdownTimeout?: number;
|
|
23
|
+
/**
|
|
24
|
+
* 是否启用信号监听(SIGTERM、SIGINT)
|
|
25
|
+
* 默认 true
|
|
26
|
+
*/
|
|
27
|
+
enableSignalHandlers?: boolean;
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
30
|
* 应用主类
|
|
@@ -26,6 +36,7 @@ export declare class Application {
|
|
|
26
36
|
private readonly middlewarePipeline;
|
|
27
37
|
private readonly websocketRegistry;
|
|
28
38
|
private readonly extensions;
|
|
39
|
+
private signalHandlersInstalled;
|
|
29
40
|
constructor(options?: ApplicationOptions);
|
|
30
41
|
/**
|
|
31
42
|
* 注册全局中间件
|
|
@@ -46,9 +57,16 @@ export declare class Application {
|
|
|
46
57
|
*/
|
|
47
58
|
private initializeConfigCenter;
|
|
48
59
|
/**
|
|
49
|
-
*
|
|
60
|
+
* 停止应用(立即停止,不等待请求完成)
|
|
50
61
|
*/
|
|
51
62
|
stop(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* 优雅停机
|
|
65
|
+
* 停止接受新请求,等待正在处理的请求完成,然后关闭应用
|
|
66
|
+
* @param timeout - 超时时间(毫秒),默认使用配置的 gracefulShutdownTimeout 或 30000
|
|
67
|
+
* @returns Promise,在停机完成时 resolve
|
|
68
|
+
*/
|
|
69
|
+
gracefulShutdown(timeout?: number): Promise<void>;
|
|
52
70
|
/**
|
|
53
71
|
* 关闭所有扩展
|
|
54
72
|
*/
|
|
@@ -99,5 +117,13 @@ export declare class Application {
|
|
|
99
117
|
* @returns DI 容器
|
|
100
118
|
*/
|
|
101
119
|
getContainer(): import("..").Container;
|
|
120
|
+
/**
|
|
121
|
+
* 安装信号处理器(SIGTERM、SIGINT)
|
|
122
|
+
*/
|
|
123
|
+
private installSignalHandlers;
|
|
124
|
+
/**
|
|
125
|
+
* 移除信号处理器
|
|
126
|
+
*/
|
|
127
|
+
private removeSignalHandlers;
|
|
102
128
|
}
|
|
103
129
|
//# sourceMappingURL=application.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/core/application.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,UAAU,CAAC;AAMzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/core/application.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,UAAU,CAAC;AAMzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAO3C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,CAAY;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,uBAAuB,CAAkB;gBAE9B,OAAO,GAAE,kBAAuB;IAsBnD;;;OAGG;IACI,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAIxC;;OAEG;IACU,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCpE;;OAEG;YACW,oBAAoB;IAqBlC;;;OAGG;YACW,sBAAsB;IAyBpC;;OAEG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYlC;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9D;;OAEG;YACW,eAAe;IAkB7B;;;;OAIG;YACW,aAAa;IA8B3B;;;OAGG;IACI,kBAAkB,CAAC,eAAe,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAKtE;;;OAGG;IACI,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAgBrD;;;OAGG;IACI,wBAAwB,CAAC,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAIzE;;;OAGG;IACI,iBAAiB,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI;IAK/D;;;OAGG;IACI,SAAS,IAAI,SAAS,GAAG,SAAS;IAIzC;;;OAGG;YACW,gBAAgB;IAsB9B;;;OAGG;YACW,kBAAkB;IAwBhC;;;OAGG;IACI,YAAY;IAInB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA2B7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAS7B"}
|
package/dist/core/server.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface ServerOptions {
|
|
|
22
22
|
* WebSocket 网关注册表
|
|
23
23
|
*/
|
|
24
24
|
websocketRegistry?: WebSocketGatewayRegistry;
|
|
25
|
+
/**
|
|
26
|
+
* 优雅停机超时时间(毫秒)
|
|
27
|
+
* 默认 30 秒
|
|
28
|
+
*/
|
|
29
|
+
gracefulShutdownTimeout?: number;
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* 服务器封装类
|
|
@@ -30,15 +35,34 @@ export interface ServerOptions {
|
|
|
30
35
|
export declare class BunServer {
|
|
31
36
|
private server?;
|
|
32
37
|
private readonly options;
|
|
38
|
+
private activeRequests;
|
|
39
|
+
private isShuttingDown;
|
|
40
|
+
private shutdownPromise?;
|
|
41
|
+
private shutdownResolve?;
|
|
33
42
|
constructor(options: ServerOptions);
|
|
34
43
|
/**
|
|
35
44
|
* 启动服务器
|
|
36
45
|
*/
|
|
37
46
|
start(): void;
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
48
|
+
* 停止服务器(立即停止,不等待请求完成)
|
|
40
49
|
*/
|
|
41
50
|
stop(): void;
|
|
51
|
+
/**
|
|
52
|
+
* 优雅停机
|
|
53
|
+
* 停止接受新请求,等待正在处理的请求完成
|
|
54
|
+
* @param timeout - 超时时间(毫秒),默认使用配置的 gracefulShutdownTimeout 或 30000
|
|
55
|
+
* @returns Promise,在停机完成时 resolve
|
|
56
|
+
*/
|
|
57
|
+
gracefulShutdown(timeout?: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* 获取当前活跃请求数
|
|
60
|
+
*/
|
|
61
|
+
getActiveRequests(): number;
|
|
62
|
+
/**
|
|
63
|
+
* 检查是否正在关闭
|
|
64
|
+
*/
|
|
65
|
+
isShuttingDownState(): boolean;
|
|
42
66
|
/**
|
|
43
67
|
* 获取服务器实例
|
|
44
68
|
* @returns Bun Server 实例
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1D;;OAEG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1D;;OAEG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAE7C;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAa;gBAElB,OAAO,EAAE,aAAa;IAIzC;;OAEG;IACI,KAAK,IAAI,IAAI;IA2FpB;;OAEG;IACI,IAAI,IAAI,IAAI;IAWnB;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2C9D;;OAEG;IACI,iBAAiB,IAAI,MAAM;IAIlC;;OAEG;IACI,mBAAmB,IAAI,OAAO;IAIrC;;;OAGG;IACI,SAAS,IAAI,MAAM,CAAC,uBAAuB,CAAC,GAAG,SAAS;IAI/D;;;OAGG;IACI,SAAS,IAAI,OAAO;IAI3B;;OAEG;IACI,OAAO,IAAI,MAAM;IAIxB;;OAEG;IACI,WAAW,IAAI,MAAM,GAAG,SAAS;CAGzC"}
|
package/dist/index.js
CHANGED
|
@@ -3235,6 +3235,10 @@ import { LoggerManager } from "@dangao/logsmith";
|
|
|
3235
3235
|
class BunServer {
|
|
3236
3236
|
server;
|
|
3237
3237
|
options;
|
|
3238
|
+
activeRequests = 0;
|
|
3239
|
+
isShuttingDown = false;
|
|
3240
|
+
shutdownPromise;
|
|
3241
|
+
shutdownResolve;
|
|
3238
3242
|
constructor(options) {
|
|
3239
3243
|
this.options = options;
|
|
3240
3244
|
}
|
|
@@ -3243,10 +3247,17 @@ class BunServer {
|
|
|
3243
3247
|
throw new Error("Server is already running");
|
|
3244
3248
|
}
|
|
3245
3249
|
const logger = LoggerManager.getLogger();
|
|
3250
|
+
this.activeRequests = 0;
|
|
3251
|
+
this.isShuttingDown = false;
|
|
3252
|
+
this.shutdownPromise = undefined;
|
|
3253
|
+
this.shutdownResolve = undefined;
|
|
3246
3254
|
this.server = Bun.serve({
|
|
3247
3255
|
port: this.options.port ?? 3000,
|
|
3248
3256
|
hostname: this.options.hostname,
|
|
3249
3257
|
fetch: (request, server) => {
|
|
3258
|
+
if (this.isShuttingDown) {
|
|
3259
|
+
return new Response("Server is shutting down", { status: 503 });
|
|
3260
|
+
}
|
|
3250
3261
|
const upgradeHeader = request.headers.get("upgrade");
|
|
3251
3262
|
if (this.options.websocketRegistry && upgradeHeader && upgradeHeader.toLowerCase() === "websocket") {
|
|
3252
3263
|
const url = new URL(request.url);
|
|
@@ -3261,8 +3272,23 @@ class BunServer {
|
|
|
3261
3272
|
}
|
|
3262
3273
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
3263
3274
|
}
|
|
3275
|
+
this.activeRequests++;
|
|
3264
3276
|
const context = new Context(request);
|
|
3265
|
-
|
|
3277
|
+
const responsePromise = this.options.fetch(context);
|
|
3278
|
+
if (responsePromise instanceof Promise) {
|
|
3279
|
+
responsePromise.finally(() => {
|
|
3280
|
+
this.activeRequests--;
|
|
3281
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
3282
|
+
this.shutdownResolve();
|
|
3283
|
+
}
|
|
3284
|
+
}).catch(() => {});
|
|
3285
|
+
} else {
|
|
3286
|
+
this.activeRequests--;
|
|
3287
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
3288
|
+
this.shutdownResolve();
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
return responsePromise;
|
|
3266
3292
|
},
|
|
3267
3293
|
websocket: {
|
|
3268
3294
|
open: (ws) => {
|
|
@@ -3285,9 +3311,45 @@ class BunServer {
|
|
|
3285
3311
|
const logger = LoggerManager.getLogger();
|
|
3286
3312
|
this.server.stop();
|
|
3287
3313
|
this.server = undefined;
|
|
3314
|
+
this.isShuttingDown = false;
|
|
3315
|
+
this.activeRequests = 0;
|
|
3288
3316
|
logger.info("Server stopped");
|
|
3289
3317
|
}
|
|
3290
3318
|
}
|
|
3319
|
+
async gracefulShutdown(timeout) {
|
|
3320
|
+
if (!this.server || this.isShuttingDown) {
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
const logger = LoggerManager.getLogger();
|
|
3324
|
+
const shutdownTimeout = timeout ?? this.options.gracefulShutdownTimeout ?? 30000;
|
|
3325
|
+
logger.info(`Starting graceful shutdown (timeout: ${shutdownTimeout}ms, active requests: ${this.activeRequests})`);
|
|
3326
|
+
this.isShuttingDown = true;
|
|
3327
|
+
if (this.activeRequests === 0) {
|
|
3328
|
+
this.stop();
|
|
3329
|
+
logger.info("Graceful shutdown completed (no active requests)");
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
if (!this.shutdownPromise) {
|
|
3333
|
+
this.shutdownPromise = new Promise((resolve) => {
|
|
3334
|
+
this.shutdownResolve = resolve;
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
3338
|
+
setTimeout(() => {
|
|
3339
|
+
logger.warn(`Graceful shutdown timeout (${shutdownTimeout}ms), forcing shutdown`);
|
|
3340
|
+
resolve();
|
|
3341
|
+
}, shutdownTimeout);
|
|
3342
|
+
});
|
|
3343
|
+
await Promise.race([this.shutdownPromise, timeoutPromise]);
|
|
3344
|
+
this.stop();
|
|
3345
|
+
logger.info(`Graceful shutdown completed (remaining active requests: ${this.activeRequests})`);
|
|
3346
|
+
}
|
|
3347
|
+
getActiveRequests() {
|
|
3348
|
+
return this.activeRequests;
|
|
3349
|
+
}
|
|
3350
|
+
isShuttingDownState() {
|
|
3351
|
+
return this.isShuttingDown;
|
|
3352
|
+
}
|
|
3291
3353
|
getServer() {
|
|
3292
3354
|
return this.server;
|
|
3293
3355
|
}
|
|
@@ -3840,12 +3902,15 @@ ConfigModule = __legacyDecorateClassTS([
|
|
|
3840
3902
|
], ConfigModule);
|
|
3841
3903
|
|
|
3842
3904
|
// src/core/application.ts
|
|
3905
|
+
import { LoggerManager as LoggerManager8 } from "@dangao/logsmith";
|
|
3906
|
+
|
|
3843
3907
|
class Application {
|
|
3844
3908
|
server;
|
|
3845
3909
|
options;
|
|
3846
3910
|
middlewarePipeline;
|
|
3847
3911
|
websocketRegistry;
|
|
3848
3912
|
extensions = [];
|
|
3913
|
+
signalHandlersInstalled = false;
|
|
3849
3914
|
constructor(options = {}) {
|
|
3850
3915
|
this.options = options;
|
|
3851
3916
|
this.middlewarePipeline = new MiddlewarePipeline([createErrorHandlingMiddleware()]);
|
|
@@ -3874,10 +3939,14 @@ class Application {
|
|
|
3874
3939
|
port: finalPort,
|
|
3875
3940
|
hostname: finalHostname,
|
|
3876
3941
|
fetch: this.handleRequest.bind(this),
|
|
3877
|
-
websocketRegistry: this.websocketRegistry
|
|
3942
|
+
websocketRegistry: this.websocketRegistry,
|
|
3943
|
+
gracefulShutdownTimeout: this.options.gracefulShutdownTimeout
|
|
3878
3944
|
};
|
|
3879
3945
|
this.server = new BunServer(serverOptions);
|
|
3880
3946
|
this.server.start();
|
|
3947
|
+
if (this.options.enableSignalHandlers !== false) {
|
|
3948
|
+
this.installSignalHandlers();
|
|
3949
|
+
}
|
|
3881
3950
|
await this.registerServices(finalPort, finalHostname);
|
|
3882
3951
|
}
|
|
3883
3952
|
async initializeExtensions() {
|
|
@@ -3905,10 +3974,19 @@ class Application {
|
|
|
3905
3974
|
}
|
|
3906
3975
|
}
|
|
3907
3976
|
async stop() {
|
|
3977
|
+
this.removeSignalHandlers();
|
|
3908
3978
|
await this.deregisterServices();
|
|
3909
3979
|
await this.closeExtensions();
|
|
3910
3980
|
this.server?.stop();
|
|
3911
3981
|
}
|
|
3982
|
+
async gracefulShutdown(timeout) {
|
|
3983
|
+
this.removeSignalHandlers();
|
|
3984
|
+
await this.deregisterServices();
|
|
3985
|
+
await this.closeExtensions();
|
|
3986
|
+
if (this.server) {
|
|
3987
|
+
await this.server.gracefulShutdown(timeout);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3912
3990
|
async closeExtensions() {
|
|
3913
3991
|
const container = this.getContainer();
|
|
3914
3992
|
for (let i = this.extensions.length - 1;i >= 0; i--) {
|
|
@@ -3995,6 +4073,33 @@ class Application {
|
|
|
3995
4073
|
getContainer() {
|
|
3996
4074
|
return ControllerRegistry.getInstance().getContainer();
|
|
3997
4075
|
}
|
|
4076
|
+
installSignalHandlers() {
|
|
4077
|
+
if (this.signalHandlersInstalled) {
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
4080
|
+
const logger = LoggerManager8.getLogger();
|
|
4081
|
+
const shutdownHandler = async (signal) => {
|
|
4082
|
+
logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
4083
|
+
try {
|
|
4084
|
+
await this.gracefulShutdown();
|
|
4085
|
+
process.exit(0);
|
|
4086
|
+
} catch (error) {
|
|
4087
|
+
logger.error(`Error during graceful shutdown:`, error);
|
|
4088
|
+
process.exit(1);
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
process.on("SIGTERM", () => shutdownHandler("SIGTERM"));
|
|
4092
|
+
process.on("SIGINT", () => shutdownHandler("SIGINT"));
|
|
4093
|
+
this.signalHandlersInstalled = true;
|
|
4094
|
+
}
|
|
4095
|
+
removeSignalHandlers() {
|
|
4096
|
+
if (!this.signalHandlersInstalled) {
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
process.removeAllListeners("SIGTERM");
|
|
4100
|
+
process.removeAllListeners("SIGINT");
|
|
4101
|
+
this.signalHandlersInstalled = false;
|
|
4102
|
+
}
|
|
3998
4103
|
}
|
|
3999
4104
|
|
|
4000
4105
|
// src/index.ts
|
package/package.json
CHANGED
package/src/core/application.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { InterceptorRegistry, INTERCEPTOR_REGISTRY_TOKEN } from '../interceptor'
|
|
|
16
16
|
import { CONFIG_SERVICE_TOKEN } from '../config/types';
|
|
17
17
|
import { ConfigService } from '../config/service';
|
|
18
18
|
import { ConfigModule } from '../config/config-module';
|
|
19
|
+
import { LoggerManager } from '@dangao/logsmith';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* 应用配置选项
|
|
@@ -30,6 +31,18 @@ export interface ApplicationOptions {
|
|
|
30
31
|
* 主机名
|
|
31
32
|
*/
|
|
32
33
|
hostname?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 优雅停机超时时间(毫秒)
|
|
37
|
+
* 默认 30 秒
|
|
38
|
+
*/
|
|
39
|
+
gracefulShutdownTimeout?: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 是否启用信号监听(SIGTERM、SIGINT)
|
|
43
|
+
* 默认 true
|
|
44
|
+
*/
|
|
45
|
+
enableSignalHandlers?: boolean;
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
/**
|
|
@@ -42,6 +55,7 @@ export class Application {
|
|
|
42
55
|
private readonly middlewarePipeline: MiddlewarePipeline;
|
|
43
56
|
private readonly websocketRegistry: WebSocketGatewayRegistry;
|
|
44
57
|
private readonly extensions: ApplicationExtension[] = [];
|
|
58
|
+
private signalHandlersInstalled: boolean = false;
|
|
45
59
|
|
|
46
60
|
public constructor(options: ApplicationOptions = {}) {
|
|
47
61
|
this.options = options;
|
|
@@ -95,11 +109,17 @@ export class Application {
|
|
|
95
109
|
hostname: finalHostname,
|
|
96
110
|
fetch: this.handleRequest.bind(this),
|
|
97
111
|
websocketRegistry: this.websocketRegistry,
|
|
112
|
+
gracefulShutdownTimeout: this.options.gracefulShutdownTimeout,
|
|
98
113
|
};
|
|
99
114
|
|
|
100
115
|
this.server = new BunServer(serverOptions);
|
|
101
116
|
this.server.start();
|
|
102
117
|
|
|
118
|
+
// 安装信号处理器(如果启用)
|
|
119
|
+
if (this.options.enableSignalHandlers !== false) {
|
|
120
|
+
this.installSignalHandlers();
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
// 自动注册服务到注册中心(如果使用了 @ServiceRegistry 装饰器)
|
|
104
124
|
await this.registerServices(finalPort, finalHostname);
|
|
105
125
|
}
|
|
@@ -158,9 +178,12 @@ export class Application {
|
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
/**
|
|
161
|
-
*
|
|
181
|
+
* 停止应用(立即停止,不等待请求完成)
|
|
162
182
|
*/
|
|
163
183
|
public async stop(): Promise<void> {
|
|
184
|
+
// 移除信号处理器
|
|
185
|
+
this.removeSignalHandlers();
|
|
186
|
+
|
|
164
187
|
// 自动注销服务(如果使用了 @ServiceRegistry 装饰器)
|
|
165
188
|
await this.deregisterServices();
|
|
166
189
|
|
|
@@ -169,6 +192,28 @@ export class Application {
|
|
|
169
192
|
this.server?.stop();
|
|
170
193
|
}
|
|
171
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 优雅停机
|
|
197
|
+
* 停止接受新请求,等待正在处理的请求完成,然后关闭应用
|
|
198
|
+
* @param timeout - 超时时间(毫秒),默认使用配置的 gracefulShutdownTimeout 或 30000
|
|
199
|
+
* @returns Promise,在停机完成时 resolve
|
|
200
|
+
*/
|
|
201
|
+
public async gracefulShutdown(timeout?: number): Promise<void> {
|
|
202
|
+
// 移除信号处理器
|
|
203
|
+
this.removeSignalHandlers();
|
|
204
|
+
|
|
205
|
+
// 自动注销服务(如果使用了 @ServiceRegistry 装饰器)
|
|
206
|
+
await this.deregisterServices();
|
|
207
|
+
|
|
208
|
+
// 关闭所有扩展(包括数据库连接等)
|
|
209
|
+
await this.closeExtensions();
|
|
210
|
+
|
|
211
|
+
// 优雅关闭服务器
|
|
212
|
+
if (this.server) {
|
|
213
|
+
await this.server.gracefulShutdown(timeout);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
/**
|
|
173
218
|
* 关闭所有扩展
|
|
174
219
|
*/
|
|
@@ -340,5 +385,48 @@ export class Application {
|
|
|
340
385
|
public getContainer() {
|
|
341
386
|
return ControllerRegistry.getInstance().getContainer();
|
|
342
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 安装信号处理器(SIGTERM、SIGINT)
|
|
391
|
+
*/
|
|
392
|
+
private installSignalHandlers(): void {
|
|
393
|
+
if (this.signalHandlersInstalled) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const logger = LoggerManager.getLogger();
|
|
398
|
+
|
|
399
|
+
const shutdownHandler = async (signal: string) => {
|
|
400
|
+
logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
401
|
+
try {
|
|
402
|
+
await this.gracefulShutdown();
|
|
403
|
+
process.exit(0);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
logger.error(`Error during graceful shutdown:`, error);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// 监听 SIGTERM(通常由进程管理器发送)
|
|
411
|
+
process.on('SIGTERM', () => shutdownHandler('SIGTERM'));
|
|
412
|
+
|
|
413
|
+
// 监听 SIGINT(通常由 Ctrl+C 触发)
|
|
414
|
+
process.on('SIGINT', () => shutdownHandler('SIGINT'));
|
|
415
|
+
|
|
416
|
+
this.signalHandlersInstalled = true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 移除信号处理器
|
|
421
|
+
*/
|
|
422
|
+
private removeSignalHandlers(): void {
|
|
423
|
+
if (!this.signalHandlersInstalled) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
process.removeAllListeners('SIGTERM');
|
|
428
|
+
process.removeAllListeners('SIGINT');
|
|
429
|
+
this.signalHandlersInstalled = false;
|
|
430
|
+
}
|
|
343
431
|
}
|
|
344
432
|
|
package/src/core/server.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface ServerOptions {
|
|
|
27
27
|
* WebSocket 网关注册表
|
|
28
28
|
*/
|
|
29
29
|
websocketRegistry?: WebSocketGatewayRegistry;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 优雅停机超时时间(毫秒)
|
|
33
|
+
* 默认 30 秒
|
|
34
|
+
*/
|
|
35
|
+
gracefulShutdownTimeout?: number;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -36,6 +42,10 @@ export interface ServerOptions {
|
|
|
36
42
|
export class BunServer {
|
|
37
43
|
private server?: Server<WebSocketConnectionData>;
|
|
38
44
|
private readonly options: ServerOptions;
|
|
45
|
+
private activeRequests: number = 0;
|
|
46
|
+
private isShuttingDown: boolean = false;
|
|
47
|
+
private shutdownPromise?: Promise<void>;
|
|
48
|
+
private shutdownResolve?: () => void;
|
|
39
49
|
|
|
40
50
|
public constructor(options: ServerOptions) {
|
|
41
51
|
this.options = options;
|
|
@@ -51,6 +61,12 @@ export class BunServer {
|
|
|
51
61
|
|
|
52
62
|
const logger = LoggerManager.getLogger();
|
|
53
63
|
|
|
64
|
+
// 重置状态
|
|
65
|
+
this.activeRequests = 0;
|
|
66
|
+
this.isShuttingDown = false;
|
|
67
|
+
this.shutdownPromise = undefined;
|
|
68
|
+
this.shutdownResolve = undefined;
|
|
69
|
+
|
|
54
70
|
this.server = Bun.serve({
|
|
55
71
|
port: this.options.port ?? 3000,
|
|
56
72
|
hostname: this.options.hostname,
|
|
@@ -58,6 +74,11 @@ export class BunServer {
|
|
|
58
74
|
request: Request,
|
|
59
75
|
server: Server<WebSocketConnectionData>,
|
|
60
76
|
): Response | Promise<Response> | undefined => {
|
|
77
|
+
// 如果正在关闭,拒绝新请求
|
|
78
|
+
if (this.isShuttingDown) {
|
|
79
|
+
return new Response("Server is shutting down", { status: 503 });
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
const upgradeHeader = request.headers.get("upgrade");
|
|
62
83
|
if (
|
|
63
84
|
this.options.websocketRegistry &&
|
|
@@ -77,8 +98,34 @@ export class BunServer {
|
|
|
77
98
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
78
99
|
}
|
|
79
100
|
|
|
101
|
+
// 增加活跃请求计数
|
|
102
|
+
this.activeRequests++;
|
|
103
|
+
|
|
80
104
|
const context = new Context(request);
|
|
81
|
-
|
|
105
|
+
const responsePromise = this.options.fetch(context);
|
|
106
|
+
|
|
107
|
+
// 处理响应完成后的清理
|
|
108
|
+
if (responsePromise instanceof Promise) {
|
|
109
|
+
responsePromise
|
|
110
|
+
.finally(() => {
|
|
111
|
+
this.activeRequests--;
|
|
112
|
+
// 如果正在关闭且没有活跃请求,触发关闭完成
|
|
113
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
114
|
+
this.shutdownResolve();
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.catch(() => {
|
|
118
|
+
// 错误已在中间件中处理,这里只负责计数
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
// 同步响应
|
|
122
|
+
this.activeRequests--;
|
|
123
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
124
|
+
this.shutdownResolve();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return responsePromise;
|
|
82
129
|
},
|
|
83
130
|
websocket: {
|
|
84
131
|
open: (ws) => {
|
|
@@ -99,17 +146,82 @@ export class BunServer {
|
|
|
99
146
|
}
|
|
100
147
|
|
|
101
148
|
/**
|
|
102
|
-
*
|
|
149
|
+
* 停止服务器(立即停止,不等待请求完成)
|
|
103
150
|
*/
|
|
104
151
|
public stop(): void {
|
|
105
152
|
if (this.server) {
|
|
106
153
|
const logger = LoggerManager.getLogger();
|
|
107
154
|
this.server.stop();
|
|
108
155
|
this.server = undefined;
|
|
156
|
+
this.isShuttingDown = false;
|
|
157
|
+
this.activeRequests = 0;
|
|
109
158
|
logger.info("Server stopped");
|
|
110
159
|
}
|
|
111
160
|
}
|
|
112
161
|
|
|
162
|
+
/**
|
|
163
|
+
* 优雅停机
|
|
164
|
+
* 停止接受新请求,等待正在处理的请求完成
|
|
165
|
+
* @param timeout - 超时时间(毫秒),默认使用配置的 gracefulShutdownTimeout 或 30000
|
|
166
|
+
* @returns Promise,在停机完成时 resolve
|
|
167
|
+
*/
|
|
168
|
+
public async gracefulShutdown(timeout?: number): Promise<void> {
|
|
169
|
+
if (!this.server || this.isShuttingDown) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const logger = LoggerManager.getLogger();
|
|
174
|
+
const shutdownTimeout = timeout ?? this.options.gracefulShutdownTimeout ?? 30000;
|
|
175
|
+
|
|
176
|
+
logger.info(`Starting graceful shutdown (timeout: ${shutdownTimeout}ms, active requests: ${this.activeRequests})`);
|
|
177
|
+
|
|
178
|
+
// 标记为正在关闭,停止接受新请求
|
|
179
|
+
this.isShuttingDown = true;
|
|
180
|
+
|
|
181
|
+
// 如果没有活跃请求,立即关闭
|
|
182
|
+
if (this.activeRequests === 0) {
|
|
183
|
+
this.stop();
|
|
184
|
+
logger.info("Graceful shutdown completed (no active requests)");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 创建关闭 Promise
|
|
189
|
+
if (!this.shutdownPromise) {
|
|
190
|
+
this.shutdownPromise = new Promise<void>((resolve) => {
|
|
191
|
+
this.shutdownResolve = resolve;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 设置超时
|
|
196
|
+
const timeoutPromise = new Promise<void>((resolve) => {
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
logger.warn(`Graceful shutdown timeout (${shutdownTimeout}ms), forcing shutdown`);
|
|
199
|
+
resolve();
|
|
200
|
+
}, shutdownTimeout);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 等待所有请求完成或超时
|
|
204
|
+
await Promise.race([this.shutdownPromise, timeoutPromise]);
|
|
205
|
+
|
|
206
|
+
// 停止服务器
|
|
207
|
+
this.stop();
|
|
208
|
+
logger.info(`Graceful shutdown completed (remaining active requests: ${this.activeRequests})`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 获取当前活跃请求数
|
|
213
|
+
*/
|
|
214
|
+
public getActiveRequests(): number {
|
|
215
|
+
return this.activeRequests;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 检查是否正在关闭
|
|
220
|
+
*/
|
|
221
|
+
public isShuttingDownState(): boolean {
|
|
222
|
+
return this.isShuttingDown;
|
|
223
|
+
}
|
|
224
|
+
|
|
113
225
|
/**
|
|
114
226
|
* 获取服务器实例
|
|
115
227
|
* @returns Bun Server 实例
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { describe, expect, test, afterEach, beforeEach } from 'bun:test';
|
|
2
|
+
import { Application } from '../../src/core/application';
|
|
3
|
+
import { Controller, ControllerRegistry } from '../../src/controller/controller';
|
|
4
|
+
import { GET } from '../../src/router/decorators';
|
|
5
|
+
import { Param } from '../../src/controller/decorators';
|
|
6
|
+
import { getTestPort } from '../utils/test-port';
|
|
7
|
+
import { RouteRegistry } from '../../src/router/registry';
|
|
8
|
+
|
|
9
|
+
describe('Graceful Shutdown', () => {
|
|
10
|
+
let app: Application;
|
|
11
|
+
let port: number;
|
|
12
|
+
|
|
13
|
+
beforeEach((done) => {
|
|
14
|
+
port = getTestPort();
|
|
15
|
+
done();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async (done) => {
|
|
19
|
+
if (app) {
|
|
20
|
+
// 确保清理,使用 stop 而不是 gracefulShutdown 以避免测试间干扰
|
|
21
|
+
try {
|
|
22
|
+
const server = app.getServer();
|
|
23
|
+
if (server?.isRunning()) {
|
|
24
|
+
await app.stop();
|
|
25
|
+
// 等待一小段时间确保端口释放
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// 忽略错误,确保清理完成
|
|
30
|
+
}
|
|
31
|
+
app = undefined as any;
|
|
32
|
+
}
|
|
33
|
+
RouteRegistry.getInstance().clear();
|
|
34
|
+
ControllerRegistry.getInstance().clear();
|
|
35
|
+
done();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should reject new requests during shutdown', async () => {
|
|
39
|
+
@Controller('/api')
|
|
40
|
+
class TestController {
|
|
41
|
+
@GET('/test')
|
|
42
|
+
public test() {
|
|
43
|
+
return { message: 'ok' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@GET('/slow')
|
|
47
|
+
public async slow() {
|
|
48
|
+
// 模拟慢请求,确保在停机过程中有活跃请求
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
50
|
+
return { message: 'completed' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
app = new Application({ port, enableSignalHandlers: false });
|
|
55
|
+
app.registerController(TestController);
|
|
56
|
+
await app.listen();
|
|
57
|
+
|
|
58
|
+
const server = app.getServer();
|
|
59
|
+
expect(server).toBeDefined();
|
|
60
|
+
|
|
61
|
+
// 先发送一个慢请求,确保在停机过程中有活跃请求
|
|
62
|
+
const slowRequestPromise = fetch(`http://localhost:${port}/api/slow`);
|
|
63
|
+
|
|
64
|
+
// 等待请求开始处理
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
66
|
+
|
|
67
|
+
// 开始优雅停机(此时有活跃请求,服务器不会立即关闭)
|
|
68
|
+
const shutdownPromise = app.gracefulShutdown(5000);
|
|
69
|
+
|
|
70
|
+
// 等待一小段时间确保 shutdown 状态已设置
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
72
|
+
|
|
73
|
+
// 尝试发送新请求,应该被拒绝
|
|
74
|
+
const response = await fetch(`http://localhost:${port}/api/test`);
|
|
75
|
+
expect(response.status).toBe(503);
|
|
76
|
+
expect(await response.text()).toBe('Server is shutting down');
|
|
77
|
+
|
|
78
|
+
// 等待慢请求完成
|
|
79
|
+
await slowRequestPromise;
|
|
80
|
+
|
|
81
|
+
// 等待停机完成
|
|
82
|
+
await shutdownPromise;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('should wait for active requests to complete', async () => {
|
|
86
|
+
let requestCompleted = false;
|
|
87
|
+
|
|
88
|
+
@Controller('/api')
|
|
89
|
+
class TestController {
|
|
90
|
+
@GET('/slow')
|
|
91
|
+
public async slow() {
|
|
92
|
+
// 模拟慢请求
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
94
|
+
requestCompleted = true;
|
|
95
|
+
return { message: 'completed' };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
app = new Application({ port, enableSignalHandlers: false });
|
|
100
|
+
app.registerController(TestController);
|
|
101
|
+
await app.listen();
|
|
102
|
+
|
|
103
|
+
// 发送一个慢请求
|
|
104
|
+
const requestPromise = fetch(`http://localhost:${port}/api/slow`);
|
|
105
|
+
|
|
106
|
+
// 等待请求开始处理
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
108
|
+
|
|
109
|
+
// 开始优雅停机
|
|
110
|
+
const shutdownPromise = app.gracefulShutdown(5000);
|
|
111
|
+
|
|
112
|
+
// 等待请求完成
|
|
113
|
+
const response = await requestPromise;
|
|
114
|
+
expect(response.status).toBe(200);
|
|
115
|
+
const data = (await response.json()) as { message: string };
|
|
116
|
+
expect(data.message).toBe('completed');
|
|
117
|
+
expect(requestCompleted).toBe(true);
|
|
118
|
+
|
|
119
|
+
// 等待停机完成
|
|
120
|
+
await shutdownPromise;
|
|
121
|
+
|
|
122
|
+
const server = app.getServer();
|
|
123
|
+
expect(server?.isRunning()).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should force shutdown after timeout', async () => {
|
|
127
|
+
@Controller('/api')
|
|
128
|
+
class TestController {
|
|
129
|
+
@GET('/very-slow')
|
|
130
|
+
public async verySlow() {
|
|
131
|
+
// 模拟非常慢的请求(超过超时时间)
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
133
|
+
return { message: 'completed' };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
app = new Application({ port, enableSignalHandlers: false });
|
|
138
|
+
app.registerController(TestController);
|
|
139
|
+
await app.listen();
|
|
140
|
+
|
|
141
|
+
// 发送一个非常慢的请求
|
|
142
|
+
const requestPromise = fetch(`http://localhost:${port}/api/very-slow`);
|
|
143
|
+
|
|
144
|
+
// 等待请求开始处理
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
146
|
+
|
|
147
|
+
// 开始优雅停机,设置较短的超时时间
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
await app.gracefulShutdown(500);
|
|
150
|
+
const shutdownDuration = Date.now() - startTime;
|
|
151
|
+
|
|
152
|
+
// 应该在大约 500ms 后强制关闭(允许一些误差)
|
|
153
|
+
expect(shutdownDuration).toBeGreaterThanOrEqual(450);
|
|
154
|
+
expect(shutdownDuration).toBeLessThan(1000);
|
|
155
|
+
|
|
156
|
+
const server = app.getServer();
|
|
157
|
+
expect(server?.isRunning()).toBe(false);
|
|
158
|
+
|
|
159
|
+
// 请求可能仍在进行,但不应该影响服务器状态
|
|
160
|
+
try {
|
|
161
|
+
await requestPromise;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// 请求可能失败,这是预期的
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should handle multiple concurrent requests during shutdown', async () => {
|
|
168
|
+
const completedRequests: number[] = [];
|
|
169
|
+
|
|
170
|
+
@Controller('/api')
|
|
171
|
+
class TestController {
|
|
172
|
+
@GET('/concurrent/:id')
|
|
173
|
+
public async concurrent(@Param('id') id: string) {
|
|
174
|
+
// 模拟异步处理
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
176
|
+
completedRequests.push(Number.parseInt(id));
|
|
177
|
+
return { id, message: 'completed' };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
app = new Application({ port, enableSignalHandlers: false });
|
|
182
|
+
app.registerController(TestController);
|
|
183
|
+
await app.listen();
|
|
184
|
+
|
|
185
|
+
// 发送多个并发请求
|
|
186
|
+
const requests = Promise.all([
|
|
187
|
+
fetch(`http://localhost:${port}/api/concurrent/1`),
|
|
188
|
+
fetch(`http://localhost:${port}/api/concurrent/2`),
|
|
189
|
+
fetch(`http://localhost:${port}/api/concurrent/3`),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
// 等待请求开始处理
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
194
|
+
|
|
195
|
+
// 开始优雅停机
|
|
196
|
+
const shutdownPromise = app.gracefulShutdown(5000);
|
|
197
|
+
|
|
198
|
+
// 等待所有请求完成
|
|
199
|
+
const responses = await requests;
|
|
200
|
+
expect(responses.length).toBe(3);
|
|
201
|
+
|
|
202
|
+
for (const response of responses) {
|
|
203
|
+
expect(response.status).toBe(200);
|
|
204
|
+
const data = (await response.json()) as { message: string };
|
|
205
|
+
expect(data.message).toBe('completed');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 所有请求应该都完成了
|
|
209
|
+
expect(completedRequests.sort()).toEqual([1, 2, 3]);
|
|
210
|
+
|
|
211
|
+
// 等待停机完成
|
|
212
|
+
await shutdownPromise;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('should handle graceful shutdown with no active requests', async () => {
|
|
216
|
+
@Controller('/api')
|
|
217
|
+
class TestController {
|
|
218
|
+
@GET('/test')
|
|
219
|
+
public test() {
|
|
220
|
+
return { message: 'ok' };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
app = new Application({ port, enableSignalHandlers: false });
|
|
225
|
+
app.registerController(TestController);
|
|
226
|
+
await app.listen();
|
|
227
|
+
|
|
228
|
+
const server = app.getServer();
|
|
229
|
+
expect(server?.isRunning()).toBe(true);
|
|
230
|
+
|
|
231
|
+
// 没有活跃请求时,应该立即关闭
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
await app.gracefulShutdown(5000);
|
|
234
|
+
const shutdownDuration = Date.now() - startTime;
|
|
235
|
+
|
|
236
|
+
// 应该几乎立即关闭(允许一些处理时间)
|
|
237
|
+
expect(shutdownDuration).toBeLessThan(100);
|
|
238
|
+
|
|
239
|
+
expect(server?.isRunning()).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('should track active requests correctly', async () => {
|
|
243
|
+
@Controller('/api')
|
|
244
|
+
class TestController {
|
|
245
|
+
@GET('/slow')
|
|
246
|
+
public async slow() {
|
|
247
|
+
// 使用慢请求确保在检查时请求仍在处理中
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
249
|
+
return { message: 'ok' };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
app = new Application({ port, enableSignalHandlers: false });
|
|
254
|
+
app.registerController(TestController);
|
|
255
|
+
await app.listen();
|
|
256
|
+
|
|
257
|
+
const server = app.getServer();
|
|
258
|
+
expect(server).toBeDefined();
|
|
259
|
+
|
|
260
|
+
// 发送慢请求
|
|
261
|
+
const requestPromise = fetch(`http://localhost:${port}/api/slow`);
|
|
262
|
+
|
|
263
|
+
// 等待请求开始处理(但不要等到完成)
|
|
264
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
265
|
+
|
|
266
|
+
// 检查活跃请求数(应该大于 0,因为请求还在处理中)
|
|
267
|
+
const activeRequests = server?.getActiveRequests() ?? 0;
|
|
268
|
+
expect(activeRequests).toBeGreaterThan(0);
|
|
269
|
+
|
|
270
|
+
// 等待请求完成
|
|
271
|
+
await requestPromise;
|
|
272
|
+
|
|
273
|
+
// 等待一小段时间确保计数更新
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
275
|
+
|
|
276
|
+
// 活跃请求数应该回到 0
|
|
277
|
+
const finalActiveRequests = server?.getActiveRequests() ?? 0;
|
|
278
|
+
expect(finalActiveRequests).toBe(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('should use custom graceful shutdown timeout', async () => {
|
|
282
|
+
@Controller('/api')
|
|
283
|
+
class TestController {
|
|
284
|
+
@GET('/slow')
|
|
285
|
+
public async slow() {
|
|
286
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
287
|
+
return { message: 'completed' };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
app = new Application({
|
|
292
|
+
port,
|
|
293
|
+
enableSignalHandlers: false,
|
|
294
|
+
gracefulShutdownTimeout: 2000,
|
|
295
|
+
});
|
|
296
|
+
app.registerController(TestController);
|
|
297
|
+
await app.listen();
|
|
298
|
+
|
|
299
|
+
// 发送请求
|
|
300
|
+
const requestPromise = fetch(`http://localhost:${port}/api/slow`);
|
|
301
|
+
|
|
302
|
+
// 等待请求开始处理
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
304
|
+
|
|
305
|
+
// 使用自定义超时时间(比配置的短)
|
|
306
|
+
const startTime = Date.now();
|
|
307
|
+
await app.gracefulShutdown(100);
|
|
308
|
+
const shutdownDuration = Date.now() - startTime;
|
|
309
|
+
|
|
310
|
+
// 应该在大约 100ms 后强制关闭
|
|
311
|
+
expect(shutdownDuration).toBeGreaterThanOrEqual(80);
|
|
312
|
+
expect(shutdownDuration).toBeLessThan(200);
|
|
313
|
+
|
|
314
|
+
// 等待请求完成(可能失败)
|
|
315
|
+
try {
|
|
316
|
+
await requestPromise;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// 请求可能失败,这是预期的
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|