@abtnode/util 1.16.49-beta-20250815-032308-7bcf0b85 → 1.16.49-beta-20250819-084933-3bcbd851

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.
@@ -1,6 +1,6 @@
1
1
  const { CustomError } = require('@blocklet/error');
2
2
 
3
- const pm2 = require('./async-pm2');
3
+ const pm2 = require('./pm2/async-pm2');
4
4
 
5
5
  const noop = () => {};
6
6
 
@@ -29,6 +29,9 @@ pm2.start = (opts, cb) => {
29
29
  } else {
30
30
  opts.node_args = nodeArgs.join(' ');
31
31
  }
32
+ if (opts.env && opts.name) {
33
+ opts.env.RELOAD_PM2_APP_NAME = opts.name;
34
+ }
32
35
 
33
36
  return originStart(opts, cb);
34
37
  };
@@ -0,0 +1,26 @@
1
+ const RpcClient = require('@arcblock/event-hub/lib/rpc');
2
+ const pm2 = require('./async-pm2');
3
+ const md5 = require('../md5');
4
+
5
+ let rpcClient = null;
6
+
7
+ async function fetchPm2(pm2Config, ABT_NODE_SK, { timeoutMs = 120000 } = {}) {
8
+ if (!rpcClient) {
9
+ rpcClient = new RpcClient(Number(process.env.ABT_NODE_EVENT_PORT) || 40407, '127.0.0.1');
10
+ }
11
+
12
+ if (process.env.NODE_ENV === 'test') {
13
+ await pm2.startAsync(pm2Config);
14
+ return 'ok';
15
+ }
16
+
17
+ const pwd = md5(`${ABT_NODE_SK}-fetch-pm2`);
18
+ const data = await rpcClient.rpc(
19
+ 'pm2/start',
20
+ { pm2Config, pwd },
21
+ { timeoutMs, errorPrefix: 'blocklet start failed' }
22
+ );
23
+ return data;
24
+ }
25
+
26
+ module.exports = fetchPm2;
@@ -0,0 +1,21 @@
1
+ const os = require('os');
2
+
3
+ const getDaemonInstanceCount = () => {
4
+ return 1;
5
+ };
6
+
7
+ const getServiceInstanceCount = () => {
8
+ // unit: GB
9
+ const memorySize = Math.floor(os.totalmem() / 1024 / 1024 / 1024) || 1;
10
+
11
+ const cpuLength = os.cpus().length || 1;
12
+
13
+ const customConfig = +process.env.ABT_NODE_MAX_CLUSTER_SIZE || Number.MAX_SAFE_INTEGER;
14
+
15
+ return Math.max(Math.min(memorySize, cpuLength, customConfig), 1);
16
+ };
17
+
18
+ module.exports = {
19
+ getDaemonInstanceCount,
20
+ getServiceInstanceCount,
21
+ };
@@ -0,0 +1,137 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ /* eslint-disable consistent-return */
3
+ /* eslint-disable no-promise-executor-return */
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+ const pm2 = require('./async-pm2');
8
+
9
+ function getTempEnvPath(name) {
10
+ const envPath = path.join(process.env.ABT_NODE_DATA_DIR || os.tmpdir(), 'tmp', `blocklet-${name}.temp-env.json`);
11
+ const dir = path.dirname(envPath);
12
+ if (!fs.existsSync(dir)) {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+ return envPath;
16
+ }
17
+
18
+ function loadReloadEnv() {
19
+ if (!process.env.RELOAD_PM2_APP_NAME) {
20
+ return;
21
+ }
22
+ const tempEnvPath = getTempEnvPath(process.env.RELOAD_PM2_APP_NAME);
23
+ if (!fs.existsSync(tempEnvPath)) {
24
+ return;
25
+ }
26
+ try {
27
+ const env = JSON.parse(fs.readFileSync(tempEnvPath, 'utf8'));
28
+ Object.assign(process.env, env);
29
+ } catch {
30
+ //
31
+ }
32
+
33
+ try {
34
+ fs.unlinkSync(tempEnvPath);
35
+ } catch {
36
+ //
37
+ }
38
+ }
39
+
40
+ // 等单个 pm_id online
41
+ function waitProcOnlineById(id, timeout = 20_000) {
42
+ const start = Date.now();
43
+ return new Promise((resolve, reject) => {
44
+ (function poll() {
45
+ pm2.describe(id, (err, list) => {
46
+ if (err) return reject(err);
47
+ const info = Array.isArray(list) ? list[0] : list;
48
+ if (info?.pm2_env?.status === 'online') return resolve();
49
+ if (Date.now() - start > timeout) return reject(new Error('proc not online in time'));
50
+ setTimeout(poll, 120);
51
+ });
52
+ })();
53
+ });
54
+ }
55
+
56
+ /** 等待该 app 的 cluster 全 online */
57
+ function waitSomeInstancesOnline(name, instances, timeoutMs = 20_000) {
58
+ const start = Date.now();
59
+ return new Promise((resolve, reject) => {
60
+ (function poll() {
61
+ pm2.list((err, list) => {
62
+ if (err) return reject(err);
63
+ const procs = list.filter((p) => p.name === name);
64
+ const online = procs.filter((p) => p.pm2_env?.status === 'online').length;
65
+ if (online >= instances) return resolve();
66
+ if (Date.now() - start > timeoutMs) return reject(new Error('instances not online in time'));
67
+ setTimeout(poll, 120);
68
+ });
69
+ })();
70
+ });
71
+ }
72
+
73
+ async function rollingRestartWithEnv(config) {
74
+ // eslint-disable-next-line camelcase
75
+ const { name, listen_timeout = 20_000 } = config;
76
+ const procs = await new Promise((resolve, reject) => {
77
+ pm2.list((err, list) => (err ? reject(err) : resolve(list)));
78
+ });
79
+
80
+ // 固定顺序
81
+ const targets = procs.filter((p) => p.name === name).sort((a, b) => a.pm_id - b.pm_id);
82
+
83
+ for (const p of targets) {
84
+ await new Promise((resolve, reject) => {
85
+ pm2.reload(p.pm_id, {}, (err) => (err ? reject(err) : resolve()));
86
+ });
87
+ // 等这个 pm_id 回到 online(而不是 name 级别)
88
+ await waitProcOnlineById(p.pm_id, listen_timeout);
89
+ // 给切换一点冗余,避免瞬时影响
90
+ await new Promise((r) => setTimeout(r, 500));
91
+ }
92
+ }
93
+
94
+ async function pm2ReloadWithEnv(config) {
95
+ const { name, instances } = config;
96
+ if (instances > 1) {
97
+ await rollingRestartWithEnv(config);
98
+ } else {
99
+ await new Promise((resolve, reject) => {
100
+ pm2.reload(name, {}, (err) => (err ? reject(err) : resolve()));
101
+ });
102
+ }
103
+ }
104
+
105
+ async function isProcessRunningByPm2(name) {
106
+ const isHasRunning = await new Promise((resolve) => {
107
+ pm2.list((err, list) => {
108
+ if (err) return resolve(false);
109
+ const proc = list.find((p) => p.name === name);
110
+ resolve(!!proc);
111
+ });
112
+ });
113
+
114
+ return isHasRunning;
115
+ }
116
+
117
+ async function pm2StartOrReload(config = {}) {
118
+ const { name } = config;
119
+ const tempEnvPath = getTempEnvPath(name);
120
+ fs.writeFileSync(tempEnvPath, JSON.stringify(config.env));
121
+
122
+ const isHasRunning = await isProcessRunningByPm2(name);
123
+
124
+ if (isHasRunning) {
125
+ await pm2ReloadWithEnv(config);
126
+ } else {
127
+ await pm2.startAsync(config);
128
+ }
129
+ await waitSomeInstancesOnline(name, config.instances);
130
+ }
131
+
132
+ module.exports = {
133
+ loadReloadEnv,
134
+ isProcessRunningByPm2,
135
+ waitSomeInstancesOnline,
136
+ pm2StartOrReload,
137
+ };
@@ -0,0 +1,356 @@
1
+ /* eslint-disable no-continue */
2
+ /* eslint-disable no-empty */
3
+ const INSTALLED = Symbol.for('graceful.shutdown.installed');
4
+
5
+ function setupGracefulShutdown(server, opts = {}) {
6
+ if (server[INSTALLED]) return;
7
+ server[INSTALLED] = true;
8
+
9
+ const {
10
+ killTimeout = 10000, // 总体兜底退出时间(ms)
11
+ socketEndTimeout = 5000, // 等待所有 TCP 连接优雅结束的最大时间(ms)
12
+ wsServers = [], // 可传入 ws 或 socket.io 实例,用于优雅关闭
13
+ logger = console, // 日志对象
14
+ setConnectionCloseOnShutdown = true, // h1 关停时设置 Connection: close / shouldKeepAlive=false
15
+ tuneKeepAlive = true, // 关停时温和缩短 keepAliveTimeout
16
+ http503OnShutdown = false, // 关停窗口是否对新请求/新流立即 503(默认 false:保持 200)
17
+ http503DelayMs = 2000, // 503 生效延迟(ms),给新进程接管留时间
18
+ quietEndDelay = 250, // 关停后延迟一小段再 end 空闲 socket(降低竞态)
19
+ // ---- HTTP/2 相关可选项 ----
20
+ enableHttp2Support = false, // 若服务包含 HTTP/2,建议保持 true
21
+ http2RefuseNewStreamsOnShutdown = true, // 关停窗口拒绝“新建流”(用 503 响应)
22
+ // ---- KA 调优参数(温和缩短)----
23
+ kaGentleMs = 600, // 关停期将 keepAliveTimeout 降到该值(建议 300–800ms)
24
+ kaPhaseDelayMs = 500, // 进入关停后,延迟一小段时间再缩短 KA
25
+ } = opts;
26
+
27
+ let isShuttingDown = false;
28
+ let shutdownStartedAt = 0;
29
+ let readySent = false;
30
+
31
+ if (!logger.info) logger.info = logger.log;
32
+
33
+ const sendReadyOnce = () => {
34
+ if (readySent) return;
35
+ readySent = true;
36
+ process.send?.('ready');
37
+ };
38
+
39
+ if (typeof server.listening === 'boolean' && server.listening) {
40
+ setImmediate(sendReadyOnce);
41
+ } else {
42
+ server.on('listening', sendReadyOnce);
43
+ }
44
+
45
+ // --------- HTTP/1.1 连接与请求跟踪 ----------
46
+ const sockets = new Set();
47
+ const metaMap = new WeakMap(); // socket -> { active: number, _ended?: boolean }
48
+
49
+ server.on('connection', (socket) => {
50
+ sockets.add(socket);
51
+ metaMap.set(socket, { active: 0 });
52
+ socket.on('close', () => sockets.delete(socket));
53
+ });
54
+
55
+ server.prependListener('request', (req, res) => {
56
+ const { socket } = req;
57
+ const meta = metaMap.get(socket);
58
+ if (meta) meta.active++;
59
+
60
+ const isH2 = req.httpVersionMajor >= 2;
61
+
62
+ if (isShuttingDown) {
63
+ // HTTP/1.x:仅设置 shouldKeepAlive=false(Node 会自动输出 Connection: close)
64
+ if (!isH2 && setConnectionCloseOnShutdown && !res.headersSent) {
65
+ try {
66
+ res.shouldKeepAlive = false;
67
+ // 下面这句可选;shouldKeepAlive=false 已足够,保留以兼容旧栈
68
+ res.setHeader?.('Connection', 'close');
69
+ } catch {
70
+ //
71
+ }
72
+ }
73
+
74
+ const should503 = http503OnShutdown && (http503DelayMs <= 0 || Date.now() - shutdownStartedAt >= http503DelayMs);
75
+
76
+ if (should503) {
77
+ res.statusCode = 503;
78
+ res.end('server reloading');
79
+ return;
80
+ }
81
+ }
82
+
83
+ const done = () => {
84
+ res.removeListener('finish', done);
85
+ res.removeListener('close', done);
86
+
87
+ const m = metaMap.get(socket);
88
+ if (!m) return;
89
+ m.active = Math.max(0, m.active - 1);
90
+
91
+ // 关停期且该 socket 已无在途请求 → 优雅收尾
92
+ if (isShuttingDown && m.active === 0 && !m._ended) {
93
+ m._ended = true;
94
+ try {
95
+ socket.end();
96
+ } catch {
97
+ //
98
+ }
99
+ }
100
+ };
101
+
102
+ res.on('finish', done); // 正常完成
103
+ res.on('close', done); // 客户端提前断开
104
+ });
105
+
106
+ // --------- WebSocket / Socket.IO 关闭 ----------
107
+ function closeWebSockets() {
108
+ for (const s of wsServers) {
109
+ if (s && s.clients && typeof s.clients.forEach === 'function') {
110
+ try {
111
+ s.clients.forEach((client) => {
112
+ // 1001: going away
113
+ if (client.readyState === 1 /* OPEN */) client.close(1001, 'Server restart');
114
+ });
115
+ setTimeout(
116
+ () => {
117
+ s.clients.forEach((client) => {
118
+ if (client.readyState !== 3 /* CLOSED */) client.terminate?.();
119
+ });
120
+ },
121
+ Math.min(socketEndTimeout, killTimeout - 100)
122
+ ).unref?.();
123
+ } catch (e) {
124
+ logger.error('[shutdown] close ws failed:', e);
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (s && typeof s.close === 'function' && s.sockets && s.sockets.sockets) {
130
+ try {
131
+ const socketsMap = s.sockets.sockets;
132
+ if (typeof socketsMap.forEach === 'function') {
133
+ socketsMap.forEach((sock) => {
134
+ try {
135
+ sock.disconnect(true);
136
+ } catch {
137
+ //
138
+ }
139
+ });
140
+ } else if (Symbol.iterator in Object(socketsMap)) {
141
+ for (const [, sock] of socketsMap) {
142
+ try {
143
+ sock.disconnect(true);
144
+ } catch {
145
+ //
146
+ }
147
+ }
148
+ }
149
+ s.close(); // 停止接受新连接并关闭现有
150
+ } catch (e) {
151
+ logger.error('[shutdown] close socket.io failed:', e);
152
+ }
153
+ continue;
154
+ }
155
+
156
+ try {
157
+ s?.broadcastClose?.('Server restart');
158
+ s?.terminateAll?.();
159
+ } catch {
160
+ //
161
+ }
162
+ }
163
+ }
164
+
165
+ // --------- HTTP/2 会话与流管理 ----------
166
+ const h2Sessions = new Set();
167
+ const h2Meta = new WeakMap(); // session -> { active: number, closed?: boolean }
168
+
169
+ let NGHTTP2_NO_ERROR;
170
+ try {
171
+ if (enableHttp2Support) {
172
+ // eslint-disable-next-line global-require
173
+ const http2 = require('http2');
174
+ NGHTTP2_NO_ERROR = http2.constants.NGHTTP2_NO_ERROR;
175
+
176
+ // 仅 http2 服务器会触发
177
+ server.on?.('session', (session) => {
178
+ h2Sessions.add(session);
179
+ h2Meta.set(session, { active: 0 });
180
+
181
+ session.on('close', () => h2Sessions.delete(session));
182
+
183
+ // 统计活跃流, Http2Stream
184
+ session.on('stream', (stream) => {
185
+ const meta = h2Meta.get(session);
186
+ if (meta) meta.active++;
187
+
188
+ // 关停窗口内:可选择拒绝“新建流”
189
+ if (isShuttingDown && http2RefuseNewStreamsOnShutdown) {
190
+ const should503 =
191
+ http503OnShutdown && (http503DelayMs <= 0 || Date.now() - shutdownStartedAt >= http503DelayMs);
192
+ try {
193
+ // 用 503 语义清晰,Node 没有直接暴露 REFUSED_STREAM 常量
194
+ stream.respond({ ':status': should503 ? 503 : 200 });
195
+ if (should503) stream.end('server reloading');
196
+ else stream.end(); // 若不打 503,可立即结束,以促使客户端重建到新实例
197
+ return;
198
+ } catch {
199
+ //
200
+ }
201
+ }
202
+
203
+ const onDone = () => {
204
+ stream.removeListener('close', onDone);
205
+ stream.removeListener('aborted', onDone);
206
+ const m = h2Meta.get(session);
207
+ if (!m) return;
208
+ m.active = Math.max(0, m.active - 1);
209
+ if (isShuttingDown && m.active === 0 && !m.closed) {
210
+ m.closed = true;
211
+ try {
212
+ session.close();
213
+ } catch {
214
+ //
215
+ }
216
+ }
217
+ };
218
+ stream.on('close', onDone);
219
+ stream.on('aborted', onDone);
220
+ });
221
+ });
222
+ }
223
+ } catch (e) {
224
+ // 环境不支持 http2,忽略
225
+ NGHTTP2_NO_ERROR = undefined;
226
+ }
227
+
228
+ // --------- h1 TCP socket 收尾 ----------
229
+ function drainTcpSockets() {
230
+ sockets.forEach((socket) => {
231
+ const meta = metaMap.get(socket) || { active: 0 };
232
+ if (meta.active === 0 && !meta._ended) {
233
+ meta._ended = true;
234
+ try {
235
+ socket.end();
236
+ } catch {
237
+ //
238
+ }
239
+ }
240
+ });
241
+
242
+ // 兜底:到点仍未关闭的强制 destroy(避免泄露)
243
+ const hardKillAfter = Math.min(socketEndTimeout, Math.max(0, killTimeout - 200));
244
+ setTimeout(() => {
245
+ sockets.forEach((socket) => {
246
+ try {
247
+ socket.destroy();
248
+ } catch {
249
+ //
250
+ }
251
+ });
252
+ }, hardKillAfter).unref?.();
253
+ }
254
+
255
+ function gracefulExit() {
256
+ if (isShuttingDown) return;
257
+ isShuttingDown = true;
258
+ shutdownStartedAt = Date.now();
259
+ logger.info('[shutdown] starting graceful shutdown');
260
+
261
+ // (A) HTTP/1.x:温和缩短 keep-alive(可选)
262
+ if (tuneKeepAlive) {
263
+ setTimeout(
264
+ () => {
265
+ try {
266
+ const ka = Math.max(50, Number(kaGentleMs) || 600); // 安全下限 50ms
267
+ if ('keepAliveTimeout' in server) server.keepAliveTimeout = ka;
268
+ if ('headersTimeout' in server) server.headersTimeout = Math.max(server.headersTimeout || 0, ka + 1000);
269
+ // 不设置 requestTimeout(保持 0 / 无限),避免掐在途
270
+ } catch {
271
+ //
272
+ }
273
+ },
274
+ Math.max(0, Number(kaPhaseDelayMs) || 0)
275
+ ).unref?.();
276
+ }
277
+
278
+ // (B) HTTP/2:向现有会话发 GOAWAY,阻止新流(让已有流跑完)
279
+ if (enableHttp2Support && NGHTTP2_NO_ERROR != null) {
280
+ for (const session of h2Sessions) {
281
+ try {
282
+ session.goaway(NGHTTP2_NO_ERROR);
283
+ } catch {
284
+ //
285
+ }
286
+ }
287
+ }
288
+
289
+ // 1) 先优雅关闭 WS/Socket.IO
290
+ closeWebSockets();
291
+
292
+ // 2) 停止接收新的 TCP 连接/HTTP 请求/HTTP2 会话
293
+ if (typeof server.close === 'function') {
294
+ server.close(() => {
295
+ logger.log('[shutdown] http server closed, exiting 0');
296
+ process.exit(0);
297
+ });
298
+ }
299
+
300
+ // 3) 稍等一会儿再去收尾(减少与在途请求竞争)
301
+ setTimeout(() => {
302
+ drainTcpSockets(); // h1
303
+
304
+ // h2:对“已无活跃流”的会话关闭
305
+ if (enableHttp2Support) {
306
+ for (const session of h2Sessions) {
307
+ const meta = h2Meta.get(session) || { active: 0 };
308
+ if (meta.active === 0 && !meta.closed) {
309
+ meta.closed = true;
310
+ try {
311
+ session.close();
312
+ } catch {
313
+ //
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }, quietEndDelay).unref?.();
319
+
320
+ // 4) h2 兜底:到点仍未关闭的会话直接 destroy,避免泄露
321
+ const hardKillAfter = Math.min(socketEndTimeout, Math.max(0, killTimeout - 200));
322
+ setTimeout(() => {
323
+ if (enableHttp2Support) {
324
+ for (const session of h2Sessions) {
325
+ try {
326
+ session.destroy();
327
+ } catch {
328
+ //
329
+ }
330
+ }
331
+ }
332
+ }, hardKillAfter).unref?.();
333
+
334
+ // 5) 最终兜底:进程仍未退出则强退
335
+ setTimeout(() => {
336
+ logger.error('[shutdown] timeout reached, forcing exit 1');
337
+ process.exit(1);
338
+ }, killTimeout).unref?.();
339
+ }
340
+
341
+ process.on('message', (msg) => {
342
+ if (msg === 'shutdown') gracefulExit();
343
+ });
344
+ process.on('SIGINT', gracefulExit);
345
+ process.on('SIGTERM', gracefulExit);
346
+ process.on('uncaughtException', (err) => {
347
+ logger.error('uncaughtException:', err);
348
+ gracefulExit();
349
+ });
350
+ process.on('unhandledRejection', (reason) => {
351
+ logger.error('unhandledRejection:', reason);
352
+ gracefulExit();
353
+ });
354
+ }
355
+
356
+ module.exports = { setupGracefulShutdown };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.49-beta-20250815-032308-7bcf0b85",
6
+ "version": "1.16.49-beta-20250819-084933-3bcbd851",
7
7
  "description": "ArcBlock's JavaScript utility",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -18,18 +18,19 @@
18
18
  "author": "polunzh <polunzh@gmail.com> (http://github.com/polunzh)",
19
19
  "license": "Apache-2.0",
20
20
  "dependencies": {
21
- "@abtnode/constant": "1.16.49-beta-20250815-032308-7bcf0b85",
22
- "@abtnode/db-cache": "1.16.49-beta-20250815-032308-7bcf0b85",
23
- "@arcblock/did": "1.21.2",
21
+ "@abtnode/constant": "1.16.49-beta-20250819-084933-3bcbd851",
22
+ "@abtnode/db-cache": "1.16.49-beta-20250819-084933-3bcbd851",
23
+ "@arcblock/did": "1.22.2",
24
+ "@arcblock/event-hub": "1.22.2",
24
25
  "@arcblock/pm2": "^6.0.12",
25
- "@blocklet/constant": "1.16.49-beta-20250815-032308-7bcf0b85",
26
+ "@blocklet/constant": "1.16.49-beta-20250819-084933-3bcbd851",
26
27
  "@blocklet/error": "^0.2.5",
27
- "@blocklet/meta": "1.16.49-beta-20250815-032308-7bcf0b85",
28
+ "@blocklet/meta": "1.16.49-beta-20250819-084933-3bcbd851",
28
29
  "@blocklet/xss": "^0.2.5",
29
- "@ocap/client": "1.21.2",
30
- "@ocap/mcrypto": "1.21.2",
31
- "@ocap/util": "1.21.2",
32
- "@ocap/wallet": "1.21.2",
30
+ "@ocap/client": "1.22.2",
31
+ "@ocap/mcrypto": "1.22.2",
32
+ "@ocap/util": "1.22.2",
33
+ "@ocap/wallet": "1.22.2",
33
34
  "archiver": "^7.0.1",
34
35
  "axios": "^1.7.9",
35
36
  "axios-mock-adapter": "^2.1.0",
@@ -90,5 +91,5 @@
90
91
  "fs-extra": "^11.2.0",
91
92
  "jest": "^29.7.0"
92
93
  },
93
- "gitHead": "75dd236eb42d14a5093c1364e51625412aa91790"
94
+ "gitHead": "b8bbf2279e9a29bd58eac7b66e1db16e851a183c"
94
95
  }