llm-simple-router 0.9.26 → 0.9.27

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.
@@ -11,6 +11,8 @@ const GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-
11
11
  const GITEE_CONFIG_BASE = 'https://gitee.com/zzzzswszzzz/llm-simple-router/raw/main/router/config';
12
12
  const CHECK_INTERVAL_MS = 60 * 60 * 1000; // eslint-disable-line no-magic-numbers
13
13
  const JSON_INDENT = 2;
14
+ const RESTART_FORCE_EXIT_MS = 3_000;
15
+ const RESTART_RESPONSE_FLUSH_MS = 300;
14
16
  // 模块级单例:checker、configDir 和定时器
15
17
  let checker = null;
16
18
  let configDir = '';
@@ -87,13 +89,12 @@ export const adminUpgradeRoutes = (app, options, done) => {
87
89
  // 先回复客户端,再执行重启(否则客户端收不到响应)
88
90
  reply.send({ ok: true, method });
89
91
  // 给响应发送窗口
90
- await new Promise((resolve) => setTimeout(resolve, 300)); // eslint-disable-line no-magic-numbers
92
+ await new Promise((resolve) => setTimeout(resolve, RESTART_RESPONSE_FLUSH_MS));
91
93
  try {
92
94
  req.log.info({ method, managed }, 'Restarting server...');
93
- // 优雅关闭(释放端口、等待活跃请求完成)
94
- await options.closeFn();
95
95
  if (!managed) {
96
- // 无进程管理器(npx / 手动 node):自 spawn 新进程
96
+ // 无进程管理器(npx / 手动 node):先 spawn 新进程,再关闭旧进程。
97
+ // 必须在 closeFn 之前 spawn,否则 closeFn 可能因活跃 SSE 连接卡住导致新进程永远不启动。
97
98
  const binPath = resolveRestartBinPath();
98
99
  const args = process.argv.slice(2); // eslint-disable-line no-magic-numbers
99
100
  req.log.info({ binPath, args }, 'Spawning new process before exit');
@@ -102,15 +103,28 @@ export const adminUpgradeRoutes = (app, options, done) => {
102
103
  stdio: 'ignore',
103
104
  env: { ...process.env },
104
105
  });
106
+ child.on('error', (err) => {
107
+ req.log.error({ err, binPath }, 'Failed to spawn new process');
108
+ });
105
109
  child.unref();
106
110
  }
111
+ // 强制退出兜底:即使 closeFn 卡住(如活跃代理 SSE 流),也能确保进程退出。
112
+ const forceExitTimer = setTimeout(() => {
113
+ req.log.warn('Graceful shutdown timed out during restart, forcing exit');
114
+ process.exit(0);
115
+ }, RESTART_FORCE_EXIT_MS);
116
+ forceExitTimer.unref();
117
+ // 尝试优雅关闭(closeFn 内部有 2s 优雅等待 + closeAllConnections 兜底)
118
+ await options.closeFn();
119
+ clearTimeout(forceExitTimer);
107
120
  req.log.info('Exiting current process');
108
121
  process.exit(0);
109
122
  }
110
123
  catch (err) {
111
- // 重启失败时记录错误,保持服务运行
124
+ // 优雅关闭失败时也必须退出:新进程已经 spawn,旧进程必须让出端口
112
125
  const msg = err instanceof Error ? err.message : String(err);
113
126
  req.log.error({ err }, `Restart failed: ${msg}`);
127
+ process.exit(1);
114
128
  }
115
129
  });
116
130
  app.post('/admin/api/upgrade/sync-config', async (req, reply) => {
package/dist/index.js CHANGED
@@ -294,7 +294,18 @@ export async function buildApp(options) {
294
294
  proxyAgentFactory.invalidateAll();
295
295
  const sessionTracker = container.resolve(SERVICE_KEYS.sessionTracker);
296
296
  sessionTracker.stop();
297
+ // 等待活跃代理请求自然完成,超时后强制关闭所有连接。
298
+ // 先调用 app.close() 停止接受新连接并等待现有连接结束,
299
+ // 如果 2 秒内未完成则调用 closeAllConnections() 强制断开,防止 SSE 长连接导致无限等待。
300
+ const CLOSE_GRACE_PERIOD_MS = 2_000;
301
+ const forceClose = typeof app.server.closeAllConnections === 'function'
302
+ ? setTimeout(() => app.server.closeAllConnections(), CLOSE_GRACE_PERIOD_MS)
303
+ : null;
304
+ if (forceClose)
305
+ forceClose.unref();
297
306
  await app.close();
307
+ if (forceClose)
308
+ clearTimeout(forceClose);
298
309
  db.close();
299
310
  };
300
311
  // 文件压缩和清理任务(仅非 :memory: 模式)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-simple-router",
3
- "version": "0.9.26",
3
+ "version": "0.9.27",
4
4
  "description": "LLM API proxy router with OpenAI/Anthropic support, model mapping, retry strategies, and admin dashboard",
5
5
  "license": "MIT",
6
6
  "author": "ZZzzswszzZZ",