@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.
@@ -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;AAM3C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;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;gBAEtC,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;IA4BpE;;OAEG;YACW,oBAAoB;IAqBlC;;;OAGG;YACW,sBAAsB;IAyBpC;;OAEG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IASlC;;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;CAGpB"}
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"}
@@ -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;CAC9C;AAED;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;gBAErB,OAAO,EAAE,aAAa;IAIzC;;OAEG;IACI,KAAK,IAAI,IAAI;IAsDpB;;OAEG;IACI,IAAI,IAAI,IAAI;IASnB;;;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"}
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
- return this.options.fetch(context);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dangao/bun-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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
 
@@ -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
- return this.options.fetch(context);
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
+ });