@abtnode/core 1.17.3-beta-20251123-232619-53258789 → 1.17.3-beta-20251126-121502-d0926972
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/lib/blocklet/manager/config-synchronizer.js +45 -11
- package/lib/blocklet/manager/disk.js +38 -3
- package/lib/blocklet/migration-dist/migration.cjs +459 -456
- package/lib/cert.js +10 -1
- package/lib/states/blocklet.js +177 -30
- package/lib/util/blocklet.js +193 -33
- package/lib/util/docker/ensure-docker-postgres.js +2 -1
- package/lib/util/install-external-dependencies.js +1 -1
- package/package.json +38 -38
package/lib/cert.js
CHANGED
|
@@ -118,7 +118,16 @@ class Cert extends EventEmitter {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
update(data) {
|
|
121
|
-
|
|
121
|
+
if (data.certificate && data.privateKey) {
|
|
122
|
+
Cert.fixCertificate(data);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return this.manager.update(data.id, {
|
|
126
|
+
name: data.name,
|
|
127
|
+
public: data.public,
|
|
128
|
+
certificate: data.certificate,
|
|
129
|
+
privateKey: data.privateKey,
|
|
130
|
+
});
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
async remove({ id }) {
|
package/lib/states/blocklet.js
CHANGED
|
@@ -25,7 +25,7 @@ const {
|
|
|
25
25
|
BLOCKLET_DEFAULT_PORT_NAME,
|
|
26
26
|
BlockletGroup,
|
|
27
27
|
} = require('@blocklet/constant');
|
|
28
|
-
const {
|
|
28
|
+
const { isPortTaken } = require('@abtnode/util/lib/port');
|
|
29
29
|
const { verifyVault } = require('@blocklet/meta/lib/security');
|
|
30
30
|
const { APP_STRUCT_VERSION } = require('@abtnode/constant');
|
|
31
31
|
|
|
@@ -42,6 +42,97 @@ const lock = new DBCache(() => ({
|
|
|
42
42
|
...getAbtNodeRedisAndSQLiteUrl(),
|
|
43
43
|
}));
|
|
44
44
|
|
|
45
|
+
// Lock for port assignment to prevent race conditions in multi-process environment
|
|
46
|
+
const portAssignLock = new DBCache(() => ({
|
|
47
|
+
prefix: 'blocklet-port-assign-lock',
|
|
48
|
+
ttl: 1000 * 30, // 30 seconds timeout
|
|
49
|
+
...getAbtNodeRedisAndSQLiteUrl(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 统一的端口分配方法
|
|
54
|
+
* 无论是首次分配还是刷新端口,都使用这个方法
|
|
55
|
+
* @param {Object} options
|
|
56
|
+
* @param {Array<string>} options.wantedPorts - 需要分配的端口名称列表
|
|
57
|
+
* @param {Object} options.blackListPorts - 黑名单端口(已占用的端口)
|
|
58
|
+
* @param {Array<number>} options.portRange - 端口范围,默认 [10000, 31000]
|
|
59
|
+
* @param {number} options.defaultPort - 回退时的起始端口
|
|
60
|
+
* @returns {Promise<Object>} 分配的端口对象 { portName: portNumber }
|
|
61
|
+
*/
|
|
62
|
+
async function assignPortsWithLock({
|
|
63
|
+
wantedPorts,
|
|
64
|
+
blackListPorts = [],
|
|
65
|
+
portRange = [10000, 31000],
|
|
66
|
+
defaultPort = 5555,
|
|
67
|
+
}) {
|
|
68
|
+
const lockName = 'blocklet-port-assign';
|
|
69
|
+
|
|
70
|
+
// ⚠️ 关键修复:使用 DBCache 锁确保端口分配的原子性
|
|
71
|
+
// 在多进程环境下,防止多个进程同时分配端口导致冲突
|
|
72
|
+
await portAssignLock.acquire(lockName);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const assignedPorts = {};
|
|
76
|
+
const usedPorts = [...blackListPorts];
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < wantedPorts.length; i++) {
|
|
79
|
+
const wantedPort = wantedPorts[i];
|
|
80
|
+
|
|
81
|
+
let found = false;
|
|
82
|
+
let assignedPort = null;
|
|
83
|
+
let attempts = 0;
|
|
84
|
+
const maxAttempts = 100;
|
|
85
|
+
|
|
86
|
+
while (!found && attempts < maxAttempts) {
|
|
87
|
+
// 在 [10000, 31000] 范围内随机选择端口
|
|
88
|
+
const randomPort = Math.floor(Math.random() * (portRange[1] - portRange[0])) + portRange[0];
|
|
89
|
+
|
|
90
|
+
if (!usedPorts.includes(randomPort)) {
|
|
91
|
+
// eslint-disable-next-line no-await-in-loop
|
|
92
|
+
const isTaken = await isPortTaken(randomPort);
|
|
93
|
+
if (!isTaken) {
|
|
94
|
+
assignedPort = randomPort;
|
|
95
|
+
usedPorts.push(assignedPort);
|
|
96
|
+
found = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
attempts++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 如果随机方式失败,回退到累加方式
|
|
103
|
+
if (!found) {
|
|
104
|
+
logger.warn('Random port assignment failed, falling back to sequential assignment', {
|
|
105
|
+
wantedPort,
|
|
106
|
+
attempts,
|
|
107
|
+
});
|
|
108
|
+
let port = defaultPort + 1;
|
|
109
|
+
// eslint-disable-next-line no-await-in-loop
|
|
110
|
+
let fallbackPort = await detectPort(Number(port));
|
|
111
|
+
while (usedPorts.includes(fallbackPort)) {
|
|
112
|
+
port = fallbackPort + 1;
|
|
113
|
+
while (usedPorts.includes(port)) {
|
|
114
|
+
port++;
|
|
115
|
+
}
|
|
116
|
+
// eslint-disable-next-line no-await-in-loop
|
|
117
|
+
fallbackPort = await detectPort(Number(port));
|
|
118
|
+
}
|
|
119
|
+
assignedPort = fallbackPort;
|
|
120
|
+
usedPorts.push(assignedPort);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// set wantedPort to assignedPort
|
|
124
|
+
assignedPorts[wantedPort] = assignedPort;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return assignedPorts;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
logger.error('Failed to assign ports with lock', { error: err });
|
|
130
|
+
throw err;
|
|
131
|
+
} finally {
|
|
132
|
+
await portAssignLock.releaseLock(lockName);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
45
136
|
const isHex = (str) => /^0x[0-9a-f]+$/i.test(str);
|
|
46
137
|
const getMaxPort = (ports = {}) => Math.max(...Object.values(ports).map(Number));
|
|
47
138
|
|
|
@@ -436,31 +527,29 @@ class BlockletState extends BaseState {
|
|
|
436
527
|
wantedPorts.push(BLOCKLET_DEFAULT_PORT_NAME);
|
|
437
528
|
}
|
|
438
529
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
for (let i = 0; i < wantedPorts.length; i++) {
|
|
443
|
-
const wantedPort = wantedPorts[i];
|
|
444
|
-
|
|
445
|
-
if (typeof alreadyAssigned[wantedPort] === 'undefined') {
|
|
446
|
-
// find assignedPort not exist in occupiedInternalPorts
|
|
447
|
-
let assignedPort = await detectPort(Number(port));
|
|
448
|
-
while (occupiedInternalPorts.has(assignedPort)) {
|
|
449
|
-
port = assignedPort + 1;
|
|
450
|
-
while (occupiedInternalPorts.has(port)) {
|
|
451
|
-
port++;
|
|
452
|
-
}
|
|
453
|
-
assignedPort = await detectPort(Number(port));
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// set wantedPort to assignedPort
|
|
457
|
-
assignedPorts[wantedPort] = assignedPort;
|
|
530
|
+
// 过滤出需要分配的端口(排除已分配的)
|
|
531
|
+
const portsToAssign = wantedPorts.filter((port) => typeof alreadyAssigned[port] === 'undefined');
|
|
458
532
|
|
|
459
|
-
|
|
460
|
-
|
|
533
|
+
if (portsToAssign.length === 0) {
|
|
534
|
+
return alreadyAssigned;
|
|
461
535
|
}
|
|
462
536
|
|
|
463
|
-
|
|
537
|
+
const blackListPorts = [
|
|
538
|
+
...Array.from(occupiedExternalPorts.keys()),
|
|
539
|
+
...Array.from(occupiedInternalPorts.keys()),
|
|
540
|
+
...Object.values(alreadyAssigned),
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
// 使用统一的端口分配方法(包含锁机制)
|
|
544
|
+
const newPorts = await assignPortsWithLock({
|
|
545
|
+
wantedPorts: portsToAssign,
|
|
546
|
+
blackListPorts,
|
|
547
|
+
portRange: [10000, 31000],
|
|
548
|
+
defaultPort: Math.max(Number(this.defaultPort), defaultPort),
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// 合并已分配的端口和新分配的端口
|
|
552
|
+
return Object.assign({}, alreadyAssigned, newPorts);
|
|
464
553
|
} catch (err) {
|
|
465
554
|
logger.error('Failed to assign port to blocklet', { error: err });
|
|
466
555
|
throw err;
|
|
@@ -469,6 +558,7 @@ class BlockletState extends BaseState {
|
|
|
469
558
|
|
|
470
559
|
/**
|
|
471
560
|
* refresh ports for blocklet if occupied during starting workflow
|
|
561
|
+
* @returns {Promise<{refreshed: boolean, componentDids: string[], isInitialAssignment?: boolean}>} 返回是否真正刷新了端口,isInitialAssignment 表示是否是首次分配(非冲突刷新)
|
|
472
562
|
*/
|
|
473
563
|
async refreshBlockletPorts(did, componentDids = [], isGreen = false) {
|
|
474
564
|
const blocklet = await this.getBlocklet(did);
|
|
@@ -477,22 +567,79 @@ class BlockletState extends BaseState {
|
|
|
477
567
|
}
|
|
478
568
|
|
|
479
569
|
const { occupiedExternalPorts, occupiedInternalPorts } = await this._getOccupiedPorts();
|
|
570
|
+
const blackListPorts = [...Array.from(occupiedExternalPorts.keys()), ...Array.from(occupiedInternalPorts.keys())];
|
|
571
|
+
|
|
572
|
+
const actuallyRefreshedDids = [];
|
|
573
|
+
let hasInitialAssignment = false; // 标记是否有首次分配(非冲突刷新)
|
|
480
574
|
|
|
481
575
|
await forEachComponentV2(blocklet, async (component) => {
|
|
482
576
|
if (!shouldSkipComponent(component.meta.did, componentDids)) {
|
|
483
577
|
let oldPorts = component[isGreen ? 'greenPorts' : 'ports'];
|
|
484
|
-
|
|
578
|
+
const hasOldPorts = oldPorts && Object.keys(oldPorts).length > 0;
|
|
579
|
+
|
|
580
|
+
// 如果是 green 环境且 greenPorts 为空,需要分配新端口(基于 ports 的端口名)
|
|
581
|
+
// 这是首次分配,不是冲突刷新
|
|
582
|
+
if (isGreen && !hasOldPorts) {
|
|
583
|
+
// 从 ports 获取端口名,分配新的 green 端口
|
|
584
|
+
const basePorts = component.ports || {};
|
|
585
|
+
if (Object.keys(basePorts).length > 0) {
|
|
586
|
+
const wantedPorts = Object.keys(basePorts);
|
|
587
|
+
const newPorts = await assignPortsWithLock({
|
|
588
|
+
wantedPorts,
|
|
589
|
+
blackListPorts: [...blackListPorts, ...Object.values(basePorts)],
|
|
590
|
+
portRange: [10000, 31000],
|
|
591
|
+
defaultPort: this.defaultPort,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
component.greenPorts = newPorts;
|
|
595
|
+
actuallyRefreshedDids.push(component.meta.did);
|
|
596
|
+
hasInitialAssignment = true; // 标记为首次分配
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 如果 oldPorts 为空,回退到 ports
|
|
602
|
+
if (!hasOldPorts) {
|
|
485
603
|
oldPorts = component.ports;
|
|
486
604
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
605
|
+
|
|
606
|
+
// 检查旧端口是否真的被占用
|
|
607
|
+
let needReassign = false;
|
|
608
|
+
for (const port of Object.values(oldPorts)) {
|
|
609
|
+
const isTaken = await isPortTaken(port);
|
|
610
|
+
if (isTaken) {
|
|
611
|
+
needReassign = true;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 只有旧端口被占用时才分配新端口
|
|
617
|
+
if (needReassign) {
|
|
618
|
+
const wantedPorts = Object.keys(oldPorts);
|
|
619
|
+
const newPorts = await assignPortsWithLock({
|
|
620
|
+
wantedPorts,
|
|
621
|
+
blackListPorts: [...blackListPorts, ...Object.values(oldPorts)],
|
|
622
|
+
portRange: [10000, 31000],
|
|
623
|
+
defaultPort: this.defaultPort,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
component[isGreen ? 'greenPorts' : 'ports'] = newPorts;
|
|
627
|
+
actuallyRefreshedDids.push(component.meta.did);
|
|
628
|
+
}
|
|
490
629
|
}
|
|
491
630
|
});
|
|
492
631
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
632
|
+
if (actuallyRefreshedDids.length > 0) {
|
|
633
|
+
await this.updateBlocklet(did, {
|
|
634
|
+
children: blocklet.children,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
refreshed: actuallyRefreshedDids.length > 0,
|
|
640
|
+
componentDids: actuallyRefreshedDids,
|
|
641
|
+
isInitialAssignment: hasInitialAssignment, // 返回是否是首次分配
|
|
642
|
+
};
|
|
496
643
|
}
|
|
497
644
|
|
|
498
645
|
async getServices() {
|
package/lib/util/blocklet.js
CHANGED
|
@@ -136,6 +136,92 @@ const parseDockerName = require('./docker/parse-docker-name');
|
|
|
136
136
|
const { createDockerNetwork } = require('./docker/docker-network');
|
|
137
137
|
const { ensureBun } = require('./ensure-bun');
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Get actual listening port from Docker container or process
|
|
141
|
+
* @param {string} processId - PM2 process ID
|
|
142
|
+
* @param {object} blocklet - Blocklet object with meta and env
|
|
143
|
+
* @returns {Promise<number|null>} Actual port number or null if not found
|
|
144
|
+
*/
|
|
145
|
+
const getActualListeningPort = async (processId, blocklet) => {
|
|
146
|
+
try {
|
|
147
|
+
// eslint-disable-next-line no-use-before-define
|
|
148
|
+
const info = await getProcessInfo(processId, { timeout: 3_000 });
|
|
149
|
+
const dockerName = info.pm2_env?.env?.dockerName;
|
|
150
|
+
|
|
151
|
+
if (dockerName) {
|
|
152
|
+
// For Docker containers, get actual port from docker inspect
|
|
153
|
+
try {
|
|
154
|
+
// Get port mapping from docker inspect
|
|
155
|
+
const inspectCmd = `docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} {{end}}' ${dockerName}`;
|
|
156
|
+
const portMappings = await promiseSpawn(inspectCmd, { mute: true });
|
|
157
|
+
|
|
158
|
+
if (portMappings) {
|
|
159
|
+
const ports = portMappings.trim().split(/\s+/).filter(Boolean);
|
|
160
|
+
for (const portMapping of ports) {
|
|
161
|
+
const port = parseInt(portMapping.split('/')[0], 10);
|
|
162
|
+
if (port && !Number.isNaN(port)) {
|
|
163
|
+
try {
|
|
164
|
+
const portCmd = `docker port ${dockerName} ${portMapping}`;
|
|
165
|
+
const hostPortOutput = await promiseSpawn(portCmd, { mute: true });
|
|
166
|
+
const match = hostPortOutput.match(/:(\d+)$/);
|
|
167
|
+
if (match) {
|
|
168
|
+
const actualPort = parseInt(match[1], 10);
|
|
169
|
+
if (actualPort && !Number.isNaN(actualPort)) {
|
|
170
|
+
logger.info('Got actual Docker port from container', {
|
|
171
|
+
processId,
|
|
172
|
+
dockerName,
|
|
173
|
+
containerPort: port,
|
|
174
|
+
hostPort: actualPort,
|
|
175
|
+
});
|
|
176
|
+
return actualPort;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
logger.debug('Failed to get port from docker port command', { error: err.message });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fallback: try to get from NetworkSettings.Ports directly
|
|
187
|
+
const inspectPortsCmd = `docker inspect --format='{{json .NetworkSettings.Ports}}' ${dockerName}`;
|
|
188
|
+
const portsJson = await promiseSpawn(inspectPortsCmd, { mute: true });
|
|
189
|
+
if (portsJson) {
|
|
190
|
+
const ports = JSON.parse(portsJson);
|
|
191
|
+
// Find the primary port (usually BLOCKLET_PORT)
|
|
192
|
+
const webInterface = (blocklet?.meta?.interfaces || []).find(
|
|
193
|
+
(x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB || x.type === BLOCKLET_INTERFACE_TYPE_DOCKER
|
|
194
|
+
);
|
|
195
|
+
const expectedContainerPort = webInterface?.containerPort || webInterface?.port;
|
|
196
|
+
|
|
197
|
+
if (expectedContainerPort) {
|
|
198
|
+
const portKey = `${expectedContainerPort}/tcp`;
|
|
199
|
+
if (ports[portKey] && ports[portKey][0]) {
|
|
200
|
+
const hostPort = parseInt(ports[portKey][0].HostPort, 10);
|
|
201
|
+
if (hostPort && !Number.isNaN(hostPort)) {
|
|
202
|
+
logger.info('Got actual Docker port from NetworkSettings', {
|
|
203
|
+
processId,
|
|
204
|
+
dockerName,
|
|
205
|
+
containerPort: expectedContainerPort,
|
|
206
|
+
hostPort,
|
|
207
|
+
});
|
|
208
|
+
return hostPort;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
logger.debug('Failed to get Docker port mapping', { error: error.message, processId, dockerName });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
logger.debug('Failed to get actual listening port', { error: error.message, processId });
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
139
225
|
/**
|
|
140
226
|
* get blocklet engine info, default is node
|
|
141
227
|
* @param {object} meta blocklet meta
|
|
@@ -149,6 +235,13 @@ const startLock = new DBCache(() => ({
|
|
|
149
235
|
ttl: 1000 * 60 * 3,
|
|
150
236
|
}));
|
|
151
237
|
|
|
238
|
+
// Lock for port assignment to prevent race conditions in multi-process environment
|
|
239
|
+
const portAssignLock = new DBCache(() => ({
|
|
240
|
+
...getAbtNodeRedisAndSQLiteUrl(),
|
|
241
|
+
prefix: 'blocklet-port-assign-lock',
|
|
242
|
+
ttl: 1000 * 30, // 30 seconds timeout
|
|
243
|
+
}));
|
|
244
|
+
|
|
152
245
|
const blockletCache = new DBCache(() => ({
|
|
153
246
|
prefix: 'blocklet-state',
|
|
154
247
|
ttl: BLOCKLET_CACHE_TTL,
|
|
@@ -1122,18 +1215,46 @@ const _checkProcessHealthy = async (
|
|
|
1122
1215
|
throw new Error('process not start within 10s');
|
|
1123
1216
|
}
|
|
1124
1217
|
|
|
1218
|
+
// ⚠️ 关键修复:优先从进程实际监听的端口获取
|
|
1219
|
+
// 对于 Docker 容器,从 Docker 端口映射获取实际端口
|
|
1220
|
+
// 这样可以避免端口刷新后,健康检查使用错误的端口
|
|
1221
|
+
const actualPort = await getActualListeningPort(processId, blocklet);
|
|
1222
|
+
|
|
1223
|
+
// 端口优先级:实际端口 > pm2 环境变量端口 > 数据库中的端口
|
|
1125
1224
|
const port =
|
|
1225
|
+
actualPort ||
|
|
1126
1226
|
envPort ||
|
|
1127
1227
|
findInterfacePortByName({ meta, ports: isGreen ? greenPorts : ports }, (webInterface || dockerInterface).name);
|
|
1228
|
+
|
|
1128
1229
|
if (logToTerminal) {
|
|
1129
1230
|
// eslint-disable-next-line no-console
|
|
1130
|
-
|
|
1131
|
-
|
|
1231
|
+
logger.info(
|
|
1232
|
+
// eslint-disable-next-line no-nested-ternary
|
|
1233
|
+
`Checking endpoint healthy for ${meta.title}, port: ${port}${actualPort ? ' (actual)' : envPort ? ' (from pm2 env)' : ' (from db)'}, minConsecutiveTime: ${
|
|
1132
1234
|
minConsecutiveTime / 1000
|
|
1133
1235
|
}s, timeout: ${timeout / 1000}s`
|
|
1134
1236
|
);
|
|
1135
1237
|
}
|
|
1136
1238
|
|
|
1239
|
+
if (
|
|
1240
|
+
actualPort &&
|
|
1241
|
+
actualPort !== envPort &&
|
|
1242
|
+
actualPort !==
|
|
1243
|
+
(isGreen
|
|
1244
|
+
? greenPorts?.[webInterface?.port || dockerInterface?.port]
|
|
1245
|
+
: ports?.[webInterface?.port || dockerInterface?.port])
|
|
1246
|
+
) {
|
|
1247
|
+
logger.info('Port mismatch detected, using actual port for health check', {
|
|
1248
|
+
processId,
|
|
1249
|
+
appDid,
|
|
1250
|
+
actualPort,
|
|
1251
|
+
envPort,
|
|
1252
|
+
dbPort: isGreen
|
|
1253
|
+
? greenPorts?.[webInterface?.port || dockerInterface?.port]
|
|
1254
|
+
: ports?.[webInterface?.port || dockerInterface?.port],
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1137
1258
|
try {
|
|
1138
1259
|
await ensureEndpointHealthy({
|
|
1139
1260
|
port,
|
|
@@ -2440,50 +2561,89 @@ const ensurePortsShape = (_states, portsA, portsB) => {
|
|
|
2440
2561
|
|
|
2441
2562
|
const ensureAppPortsNotOccupied = async ({ blocklet, componentDids: inputDids, states, manager, isGreen = false }) => {
|
|
2442
2563
|
const { did } = blocklet.meta;
|
|
2443
|
-
const
|
|
2564
|
+
const lockName = `port-check-${did}`;
|
|
2444
2565
|
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2566
|
+
// ⚠️ 关键修复:使用 DBCache 锁确保端口分配的原子性
|
|
2567
|
+
// 在多进程环境下,防止多个进程同时检查同一个端口
|
|
2568
|
+
await portAssignLock.acquire(lockName);
|
|
2448
2569
|
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2570
|
+
try {
|
|
2571
|
+
const occupiedDids = new Set();
|
|
2572
|
+
|
|
2573
|
+
await forEachComponentV2(blocklet, async (b) => {
|
|
2574
|
+
try {
|
|
2575
|
+
if (shouldSkipComponent(b.meta.did, inputDids)) return;
|
|
2576
|
+
|
|
2577
|
+
if (!b.greenPorts) {
|
|
2578
|
+
occupiedDids.add(b.meta.did);
|
|
2579
|
+
b.greenPorts = {};
|
|
2580
|
+
}
|
|
2581
|
+
const { ports = {}, greenPorts } = b;
|
|
2582
|
+
ensurePortsShape(states, ports, greenPorts);
|
|
2583
|
+
|
|
2584
|
+
const targetPorts = isGreen ? greenPorts : ports;
|
|
2455
2585
|
|
|
2456
|
-
|
|
2586
|
+
let currentOccupied = false;
|
|
2587
|
+
for (const port of Object.values(targetPorts)) {
|
|
2588
|
+
currentOccupied = await isPortTaken(port);
|
|
2589
|
+
if (currentOccupied) {
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2457
2593
|
|
|
2458
|
-
let currentOccupied = false;
|
|
2459
|
-
for (const port of Object.values(targetPorts)) {
|
|
2460
|
-
currentOccupied = await isPortTaken(port);
|
|
2461
2594
|
if (currentOccupied) {
|
|
2462
|
-
|
|
2595
|
+
occupiedDids.add(b.meta.did);
|
|
2463
2596
|
}
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
logger.error('Failed to check ports occupied', { error, blockletDid: b.meta.did, isGreen });
|
|
2464
2599
|
}
|
|
2600
|
+
});
|
|
2465
2601
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2602
|
+
if (occupiedDids.size === 0) {
|
|
2603
|
+
logger.info('No occupied ports detected, no refresh needed', { did, isGreen });
|
|
2604
|
+
return blocklet;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
const componentDids = Array.from(occupiedDids);
|
|
2608
|
+
const {
|
|
2609
|
+
refreshed,
|
|
2610
|
+
componentDids: actuallyRefreshedDids,
|
|
2611
|
+
isInitialAssignment,
|
|
2612
|
+
} = await states.blocklet.refreshBlockletPorts(did, componentDids, isGreen);
|
|
2613
|
+
|
|
2614
|
+
// 只有真正刷新了端口才打印日志和更新环境
|
|
2615
|
+
if (refreshed && actuallyRefreshedDids.length > 0) {
|
|
2616
|
+
// 区分首次分配和冲突刷新,使用不同的日志信息
|
|
2617
|
+
if (isInitialAssignment) {
|
|
2618
|
+
logger.info('Assigned green ports for blue-green deployment', {
|
|
2619
|
+
did,
|
|
2620
|
+
componentDids: actuallyRefreshedDids,
|
|
2621
|
+
isGreen,
|
|
2622
|
+
});
|
|
2623
|
+
} else {
|
|
2624
|
+
logger.info('Refreshed component ports due to conflict', {
|
|
2625
|
+
did,
|
|
2626
|
+
componentDids: actuallyRefreshedDids,
|
|
2627
|
+
isGreen,
|
|
2628
|
+
});
|
|
2468
2629
|
}
|
|
2469
|
-
} catch (error) {
|
|
2470
|
-
logger.error('Failed to check ports occupied', { error, blockletDid: b.meta.did, isGreen });
|
|
2471
|
-
}
|
|
2472
|
-
});
|
|
2473
2630
|
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
return blocklet;
|
|
2477
|
-
}
|
|
2631
|
+
await manager._updateBlockletEnvironment(did);
|
|
2632
|
+
const newBlocklet = await manager.ensureBlocklet(did);
|
|
2478
2633
|
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
logger.info('Refreshed component ports due to conflict', { did, componentDids });
|
|
2634
|
+
return newBlocklet;
|
|
2635
|
+
}
|
|
2482
2636
|
|
|
2483
|
-
|
|
2484
|
-
|
|
2637
|
+
logger.info('Ports were detected as occupied but not actually occupied during refresh, no refresh needed', {
|
|
2638
|
+
did,
|
|
2639
|
+
componentDids,
|
|
2640
|
+
isGreen,
|
|
2641
|
+
});
|
|
2485
2642
|
|
|
2486
|
-
|
|
2643
|
+
return blocklet;
|
|
2644
|
+
} finally {
|
|
2645
|
+
await portAssignLock.releaseLock(lockName);
|
|
2646
|
+
}
|
|
2487
2647
|
};
|
|
2488
2648
|
|
|
2489
2649
|
const getComponentNamesWithVersion = (app = {}, componentDids = []) => {
|
|
@@ -62,6 +62,7 @@ async function _ensureDockerPostgres(dataDir, name = 'abtnode-postgres', port =
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const postgresVersion = process.env.ABT_NODE_POSTGRES_VERSION || '17.5';
|
|
65
|
+
const postgresMaxConnections = process.env.ABT_NODE_POSTGRES_MAX_CONNECTIONS || '500';
|
|
65
66
|
|
|
66
67
|
const runCmd = [
|
|
67
68
|
'docker run -d',
|
|
@@ -74,7 +75,7 @@ async function _ensureDockerPostgres(dataDir, name = 'abtnode-postgres', port =
|
|
|
74
75
|
'-e POSTGRES_USER=postgres',
|
|
75
76
|
'-e POSTGRES_DB=postgres',
|
|
76
77
|
`postgres:${postgresVersion}`,
|
|
77
|
-
|
|
78
|
+
`-c max_connections=${postgresMaxConnections}`,
|
|
78
79
|
].join(' ');
|
|
79
80
|
|
|
80
81
|
const url = `postgresql://postgres:postgres@localhost:${port}/postgres`;
|
|
@@ -33,7 +33,7 @@ async function installExternalDependencies({ appDir, forceInstall = false, nodeI
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (!(await isSameOs(appDir, isUseDocker))) {
|
|
36
|
-
fs.removeSync(path.join(appDir, 'node_modules'));
|
|
36
|
+
fs.removeSync(path.join(appDir, 'node_modules'), { recursive: true });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const isNeedInstall = forceInstall || externals.some((dependency) => !isDependencyInstalled(appDir, dependency));
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.17.3-beta-
|
|
6
|
+
"version": "1.17.3-beta-20251126-121502-d0926972",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -17,46 +17,46 @@
|
|
|
17
17
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@abtnode/analytics": "1.17.3-beta-
|
|
21
|
-
"@abtnode/auth": "1.17.3-beta-
|
|
22
|
-
"@abtnode/certificate-manager": "1.17.3-beta-
|
|
23
|
-
"@abtnode/constant": "1.17.3-beta-
|
|
24
|
-
"@abtnode/cron": "1.17.3-beta-
|
|
25
|
-
"@abtnode/db-cache": "1.17.3-beta-
|
|
26
|
-
"@abtnode/docker-utils": "1.17.3-beta-
|
|
27
|
-
"@abtnode/logger": "1.17.3-beta-
|
|
28
|
-
"@abtnode/models": "1.17.3-beta-
|
|
29
|
-
"@abtnode/queue": "1.17.3-beta-
|
|
30
|
-
"@abtnode/rbac": "1.17.3-beta-
|
|
31
|
-
"@abtnode/router-provider": "1.17.3-beta-
|
|
32
|
-
"@abtnode/static-server": "1.17.3-beta-
|
|
33
|
-
"@abtnode/timemachine": "1.17.3-beta-
|
|
34
|
-
"@abtnode/util": "1.17.3-beta-
|
|
35
|
-
"@aigne/aigne-hub": "^0.10.
|
|
36
|
-
"@arcblock/did": "^1.27.
|
|
37
|
-
"@arcblock/did-connect-js": "^1.27.
|
|
38
|
-
"@arcblock/did-ext": "^1.27.
|
|
20
|
+
"@abtnode/analytics": "1.17.3-beta-20251126-121502-d0926972",
|
|
21
|
+
"@abtnode/auth": "1.17.3-beta-20251126-121502-d0926972",
|
|
22
|
+
"@abtnode/certificate-manager": "1.17.3-beta-20251126-121502-d0926972",
|
|
23
|
+
"@abtnode/constant": "1.17.3-beta-20251126-121502-d0926972",
|
|
24
|
+
"@abtnode/cron": "1.17.3-beta-20251126-121502-d0926972",
|
|
25
|
+
"@abtnode/db-cache": "1.17.3-beta-20251126-121502-d0926972",
|
|
26
|
+
"@abtnode/docker-utils": "1.17.3-beta-20251126-121502-d0926972",
|
|
27
|
+
"@abtnode/logger": "1.17.3-beta-20251126-121502-d0926972",
|
|
28
|
+
"@abtnode/models": "1.17.3-beta-20251126-121502-d0926972",
|
|
29
|
+
"@abtnode/queue": "1.17.3-beta-20251126-121502-d0926972",
|
|
30
|
+
"@abtnode/rbac": "1.17.3-beta-20251126-121502-d0926972",
|
|
31
|
+
"@abtnode/router-provider": "1.17.3-beta-20251126-121502-d0926972",
|
|
32
|
+
"@abtnode/static-server": "1.17.3-beta-20251126-121502-d0926972",
|
|
33
|
+
"@abtnode/timemachine": "1.17.3-beta-20251126-121502-d0926972",
|
|
34
|
+
"@abtnode/util": "1.17.3-beta-20251126-121502-d0926972",
|
|
35
|
+
"@aigne/aigne-hub": "^0.10.10",
|
|
36
|
+
"@arcblock/did": "^1.27.12",
|
|
37
|
+
"@arcblock/did-connect-js": "^1.27.12",
|
|
38
|
+
"@arcblock/did-ext": "^1.27.12",
|
|
39
39
|
"@arcblock/did-motif": "^1.1.14",
|
|
40
|
-
"@arcblock/did-util": "^1.27.
|
|
41
|
-
"@arcblock/event-hub": "^1.27.
|
|
42
|
-
"@arcblock/jwt": "^1.27.
|
|
40
|
+
"@arcblock/did-util": "^1.27.12",
|
|
41
|
+
"@arcblock/event-hub": "^1.27.12",
|
|
42
|
+
"@arcblock/jwt": "^1.27.12",
|
|
43
43
|
"@arcblock/pm2-events": "^0.0.5",
|
|
44
|
-
"@arcblock/validator": "^1.27.
|
|
45
|
-
"@arcblock/vc": "^1.27.
|
|
46
|
-
"@blocklet/constant": "1.17.3-beta-
|
|
47
|
-
"@blocklet/did-space-js": "^1.2.
|
|
48
|
-
"@blocklet/env": "1.17.3-beta-
|
|
44
|
+
"@arcblock/validator": "^1.27.12",
|
|
45
|
+
"@arcblock/vc": "^1.27.12",
|
|
46
|
+
"@blocklet/constant": "1.17.3-beta-20251126-121502-d0926972",
|
|
47
|
+
"@blocklet/did-space-js": "^1.2.6",
|
|
48
|
+
"@blocklet/env": "1.17.3-beta-20251126-121502-d0926972",
|
|
49
49
|
"@blocklet/error": "^0.3.3",
|
|
50
|
-
"@blocklet/meta": "1.17.3-beta-
|
|
51
|
-
"@blocklet/resolver": "1.17.3-beta-
|
|
52
|
-
"@blocklet/sdk": "1.17.3-beta-
|
|
53
|
-
"@blocklet/server-js": "1.17.3-beta-
|
|
54
|
-
"@blocklet/store": "1.17.3-beta-
|
|
55
|
-
"@blocklet/theme": "^3.2.
|
|
50
|
+
"@blocklet/meta": "1.17.3-beta-20251126-121502-d0926972",
|
|
51
|
+
"@blocklet/resolver": "1.17.3-beta-20251126-121502-d0926972",
|
|
52
|
+
"@blocklet/sdk": "1.17.3-beta-20251126-121502-d0926972",
|
|
53
|
+
"@blocklet/server-js": "1.17.3-beta-20251126-121502-d0926972",
|
|
54
|
+
"@blocklet/store": "1.17.3-beta-20251126-121502-d0926972",
|
|
55
|
+
"@blocklet/theme": "^3.2.10",
|
|
56
56
|
"@fidm/x509": "^1.2.1",
|
|
57
|
-
"@ocap/mcrypto": "^1.27.
|
|
58
|
-
"@ocap/util": "^1.27.
|
|
59
|
-
"@ocap/wallet": "^1.27.
|
|
57
|
+
"@ocap/mcrypto": "^1.27.12",
|
|
58
|
+
"@ocap/util": "^1.27.12",
|
|
59
|
+
"@ocap/wallet": "^1.27.12",
|
|
60
60
|
"@slack/webhook": "^7.0.6",
|
|
61
61
|
"archiver": "^7.0.1",
|
|
62
62
|
"axios": "^1.7.9",
|
|
@@ -116,5 +116,5 @@
|
|
|
116
116
|
"express": "^4.18.2",
|
|
117
117
|
"unzipper": "^0.10.11"
|
|
118
118
|
},
|
|
119
|
-
"gitHead": "
|
|
119
|
+
"gitHead": "7039cacaad2a14a9573371e24e57cbbd6b6525c8"
|
|
120
120
|
}
|