@abtnode/core 1.17.4-beta-20251203-225234-75da41dd → 1.17.4-beta-20251204-152224-243ff54f
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/api/team.js +5 -1
- package/lib/blocklet/manager/disk.js +3 -3
- package/lib/blocklet/manager/ensure-blocklet-running.js +372 -205
- package/lib/blocklet/storage/backup/routing-rule.js +2 -1
- package/lib/migrations/1.5.15-site.js +2 -1
- package/lib/router/manager.js +1 -1
- package/lib/util/blocklet.js +1 -1
- package/lib/util/check-dns.js +3 -3
- package/lib/validators/org.js +1 -1
- package/package.json +24 -24
package/lib/api/team.js
CHANGED
|
@@ -3062,7 +3062,11 @@ class TeamAPI extends EventEmitter {
|
|
|
3062
3062
|
|
|
3063
3063
|
return result;
|
|
3064
3064
|
} catch (err) {
|
|
3065
|
-
logger.error('Failed to create org',
|
|
3065
|
+
logger.error('Failed to create org', err, {
|
|
3066
|
+
teamDid,
|
|
3067
|
+
name: rest.name,
|
|
3068
|
+
userDid: rest.ownerDid || context.user.did || '',
|
|
3069
|
+
});
|
|
3066
3070
|
throw err;
|
|
3067
3071
|
}
|
|
3068
3072
|
}
|
|
@@ -45,7 +45,7 @@ const {
|
|
|
45
45
|
EVENTS,
|
|
46
46
|
USER_PROFILE_SYNC_FIELDS,
|
|
47
47
|
} = require('@abtnode/constant');
|
|
48
|
-
|
|
48
|
+
const { BLOCKLET_SITE_GROUP_SUFFIX } = require('@abtnode/constant');
|
|
49
49
|
const { getBlockletEngine } = require('@blocklet/meta/lib/engine');
|
|
50
50
|
const {
|
|
51
51
|
isDeletableBlocklet,
|
|
@@ -245,7 +245,7 @@ const { installExternalDependencies } = require('../../util/install-external-dep
|
|
|
245
245
|
const { dockerExecChown } = require('../../util/docker/docker-exec-chown');
|
|
246
246
|
const checkDockerRunHistory = require('../../util/docker/check-docker-run-history');
|
|
247
247
|
const { shouldJobBackoff } = require('../../util/env');
|
|
248
|
-
const ensureBlockletRunning = require('./ensure-blocklet-running');
|
|
248
|
+
const { ensureBlockletRunning } = require('./ensure-blocklet-running');
|
|
249
249
|
|
|
250
250
|
const { transformNotification } = require('../../util/notification');
|
|
251
251
|
const { generateUserUpdateData } = require('../../util/user');
|
|
@@ -1916,7 +1916,7 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
1916
1916
|
if (!aliasDomainSite) {
|
|
1917
1917
|
return null;
|
|
1918
1918
|
}
|
|
1919
|
-
targetDid = (aliasDomainSite.domain || '').replace(
|
|
1919
|
+
targetDid = (aliasDomainSite.domain || '').replace(BLOCKLET_SITE_GROUP_SUFFIX, '');
|
|
1920
1920
|
}
|
|
1921
1921
|
|
|
1922
1922
|
if (!targetDid) {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
1
2
|
const logger = require('@abtnode/logger')('@abtnode/core:blocklet-status-checker');
|
|
2
3
|
const pAll = require('p-all');
|
|
3
4
|
const { BlockletStatus } = require('@blocklet/constant');
|
|
4
5
|
const sleep = require('@abtnode/util/lib/sleep');
|
|
5
6
|
const { getDisplayName } = require('@blocklet/meta/lib/util');
|
|
6
|
-
const { isValid } = require('@arcblock/did');
|
|
7
|
-
|
|
8
7
|
const states = require('../../states');
|
|
9
8
|
const { isBlockletPortHealthy, shouldCheckHealthy } = require('../../util/blocklet');
|
|
10
9
|
|
|
11
|
-
const inProgressStatuses = [
|
|
10
|
+
const inProgressStatuses = [
|
|
11
|
+
BlockletStatus.stopping,
|
|
12
|
+
BlockletStatus.restarting,
|
|
13
|
+
BlockletStatus.waiting,
|
|
14
|
+
BlockletStatus.starting,
|
|
15
|
+
BlockletStatus.downloading,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Restart queue concurrency, 这个改大,容易 blocklet 超时导致启动失败
|
|
19
|
+
const RESTART_CONCURRENCY = 2;
|
|
12
20
|
|
|
13
21
|
class EnsureBlockletRunning {
|
|
14
22
|
canRunEnsureBlockletRunning = false;
|
|
@@ -22,11 +30,9 @@ class EnsureBlockletRunning {
|
|
|
22
30
|
|
|
23
31
|
minCheckInterval = 30_000;
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
everyBlockletDoingInterval = 5000;
|
|
33
|
+
preCheckInterval = 1000;
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
everyBlockletCheckInterval = 2000;
|
|
30
36
|
|
|
31
37
|
highLoadCpu = +process.env.ABT_NODE_ENSURE_RUNNING_HIGH_LOAD_CPU || 0.85;
|
|
32
38
|
|
|
@@ -34,27 +40,41 @@ class EnsureBlockletRunning {
|
|
|
34
40
|
|
|
35
41
|
highLoadDisk = +process.env.ABT_NODE_ENSURE_RUNNING_HIGH_LOAD_DISK || 0.95;
|
|
36
42
|
|
|
37
|
-
//
|
|
38
|
-
|
|
43
|
+
// 各个状态的超时阈值(毫秒)
|
|
44
|
+
// 如果是首次调用(whenCycleCheck 为 false),这些值应该是 0
|
|
45
|
+
stoppingTimeout = +process.env.ABT_NODE_ENSURE_RUNNING_STOPPING_TIMEOUT || 60 * 1000;
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
restartingTimeout = +process.env.ABT_NODE_ENSURE_RUNNING_RESTARTING_TIMEOUT || 6 * 60 * 1000;
|
|
48
|
+
|
|
49
|
+
waitingTimeout = +process.env.ABT_NODE_ENSURE_RUNNING_WAITING_TIMEOUT || 60 * 1000;
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
downloadingTimeout = +process.env.ABT_NODE_ENSURE_RUNNING_DOWNLOADING_TIMEOUT || 3 * 60 * 1000;
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
startingTimeout = +process.env.ABT_NODE_ENSURE_RUNNING_STARTING_TIMEOUT || 6 * 60 * 1000;
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
runningBlocklets = {};
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
rootBlockletsInfo = {};
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
progressBlockletsTime = {};
|
|
51
60
|
|
|
52
61
|
stopped = false;
|
|
53
62
|
|
|
63
|
+
// Queue for restarting fake running blocklets
|
|
64
|
+
restartQueue = [];
|
|
65
|
+
|
|
66
|
+
// Set to track queue keys for fast lookup
|
|
67
|
+
restartQueueKeys = new Set();
|
|
68
|
+
|
|
69
|
+
restartQueueProcessing = false;
|
|
70
|
+
|
|
71
|
+
// Track pending jobs by componentDid to prevent duplicate processing
|
|
72
|
+
pendingJobs = {};
|
|
73
|
+
|
|
54
74
|
// Ease to mock
|
|
55
75
|
isBlockletPortHealthy = isBlockletPortHealthy;
|
|
56
76
|
|
|
57
|
-
isBlockletPortHealthyWithRetries = async (blocklet
|
|
77
|
+
isBlockletPortHealthyWithRetries = async (blocklet) => {
|
|
58
78
|
let error;
|
|
59
79
|
if (!this.whenCycleCheck) {
|
|
60
80
|
try {
|
|
@@ -81,9 +101,7 @@ class EnsureBlockletRunning {
|
|
|
81
101
|
} catch (e) {
|
|
82
102
|
error = e;
|
|
83
103
|
// eslint-disable-next-line no-await-in-loop
|
|
84
|
-
await sleep(
|
|
85
|
-
fastCheck && this.whenCycleCheck ? this.everyBlockletDoingInterval : this.everyBlockletCheckInterval
|
|
86
|
-
);
|
|
104
|
+
await sleep(this.everyBlockletCheckInterval);
|
|
87
105
|
}
|
|
88
106
|
}
|
|
89
107
|
logger.error('blocklet port is not healthy', error);
|
|
@@ -106,27 +124,40 @@ class EnsureBlockletRunning {
|
|
|
106
124
|
this.checkSystemHighLoad = checkSystemHighLoad;
|
|
107
125
|
logger.info('check and fix blocklet status interval', this.checkInterval);
|
|
108
126
|
const task = async () => {
|
|
127
|
+
await sleep(this.preCheckInterval);
|
|
128
|
+
|
|
129
|
+
// 完全停止,后续也不再继续检查
|
|
130
|
+
if (this.stopped) {
|
|
131
|
+
logger.info('blocklet status checker stopped');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
109
134
|
// 如果还没进入到需要检查的阶段,则等待 1 秒后继续检查
|
|
110
135
|
if (!this.canRunEnsureBlockletRunning) {
|
|
111
|
-
await sleep(1000);
|
|
112
136
|
task();
|
|
113
137
|
return;
|
|
114
138
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
139
|
+
|
|
140
|
+
// 首次检查前不等待(whenCycleCheck 为 false)
|
|
118
141
|
try {
|
|
119
142
|
await this.checkAndFix();
|
|
143
|
+
|
|
144
|
+
// 每次检查完之后查看消耗的时间
|
|
145
|
+
await sleep(Math.max(this.checkInterval, this.minCheckInterval));
|
|
146
|
+
this.whenCycleCheck = true;
|
|
120
147
|
} catch (e) {
|
|
121
148
|
logger.error('check and fix blocklet status failed', e);
|
|
149
|
+
// 出错时也要等待,避免频繁重试
|
|
150
|
+
if (this.whenCycleCheck) {
|
|
151
|
+
await sleep(Math.max(this.checkInterval, this.minCheckInterval));
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
task();
|
|
124
155
|
};
|
|
125
156
|
task();
|
|
126
157
|
};
|
|
127
158
|
|
|
128
|
-
getDisplayNameByRootDid = (rootDid) => {
|
|
129
|
-
const rootBlocklet = this.
|
|
159
|
+
getDisplayNameByRootDid = async (rootDid) => {
|
|
160
|
+
const rootBlocklet = this.rootBlockletsInfo[rootDid] || (await this.states.blocklet.getBlocklet(rootDid));
|
|
130
161
|
if (rootBlocklet) {
|
|
131
162
|
return getDisplayName(rootBlocklet);
|
|
132
163
|
}
|
|
@@ -137,6 +168,42 @@ class EnsureBlockletRunning {
|
|
|
137
168
|
return blocklet.meta.title || blocklet.meta.name || blocklet.meta.did;
|
|
138
169
|
};
|
|
139
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Get timeout threshold for a specific status
|
|
173
|
+
* @param {string} status - Blocklet status
|
|
174
|
+
* @returns {number} Timeout threshold in milliseconds
|
|
175
|
+
*/
|
|
176
|
+
getStatusTimeout = (status) => {
|
|
177
|
+
// 如果是首次调用,所有阈值都是 0
|
|
178
|
+
if (!this.whenCycleCheck) {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let timeout = 0;
|
|
183
|
+
switch (status) {
|
|
184
|
+
case BlockletStatus.stopping:
|
|
185
|
+
timeout = this.stoppingTimeout;
|
|
186
|
+
break;
|
|
187
|
+
case BlockletStatus.restarting:
|
|
188
|
+
timeout = this.restartingTimeout;
|
|
189
|
+
break;
|
|
190
|
+
case BlockletStatus.waiting:
|
|
191
|
+
timeout = this.waitingTimeout;
|
|
192
|
+
break;
|
|
193
|
+
case BlockletStatus.downloading:
|
|
194
|
+
timeout = this.downloadingTimeout;
|
|
195
|
+
break;
|
|
196
|
+
case BlockletStatus.starting:
|
|
197
|
+
timeout = this.startingTimeout;
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
timeout = this.downloadingTimeout;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
// 需要减去检查间隔时间,因为每次检查都会在第二次检查之后才比对时间,最少间隔时间不能小于 waitingTimeout
|
|
204
|
+
return Math.max(timeout - this.checkInterval, this.waitingTimeout);
|
|
205
|
+
};
|
|
206
|
+
|
|
140
207
|
checkAndFix = async () => {
|
|
141
208
|
logger.info('check and fix blocklet status');
|
|
142
209
|
const systemHighLoad = this.checkSystemHighLoad({
|
|
@@ -147,50 +214,51 @@ class EnsureBlockletRunning {
|
|
|
147
214
|
|
|
148
215
|
if (this.whenCycleCheck && systemHighLoad.isHighLoad) {
|
|
149
216
|
logger.warn('Skip once ensure blocklet running because system high load', systemHighLoad);
|
|
150
|
-
return;
|
|
217
|
+
return 0;
|
|
151
218
|
}
|
|
152
219
|
|
|
153
220
|
this.runningBlocklets = {};
|
|
154
|
-
this.fakeRunningBlocklets = {};
|
|
155
|
-
this.needRestartBlocklets = {};
|
|
156
|
-
|
|
157
221
|
const startTime = Date.now();
|
|
158
222
|
try {
|
|
223
|
+
this.startRestartQueueProcessor();
|
|
159
224
|
await this.getRunningBlocklets();
|
|
160
225
|
await this.getFakeRunningBlocklets();
|
|
161
|
-
await this.restartFakeRunningBlocklets();
|
|
162
226
|
} catch (e) {
|
|
163
227
|
logger.error('ensure blocklet status failed', e);
|
|
164
228
|
}
|
|
229
|
+
const elapsedTime = Date.now() - startTime;
|
|
165
230
|
logger.info(
|
|
166
|
-
`ensure blocklet status finished in ${
|
|
231
|
+
`ensure blocklet status finished in ${elapsedTime}ms. It's server first start: ${!this.whenCycleCheck}`
|
|
167
232
|
);
|
|
168
|
-
|
|
169
|
-
this.whenCycleCheck = true;
|
|
233
|
+
return elapsedTime;
|
|
170
234
|
};
|
|
171
235
|
|
|
172
236
|
getRunningBlocklets = async () => {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const blocklets = await this.states.blocklet.getBlocklets();
|
|
178
|
-
for (const rootBlocklet of blocklets) {
|
|
179
|
-
const { did } = rootBlocklet.meta;
|
|
237
|
+
const rootBlocklets = await this.states.blocklet.getBlocklets();
|
|
238
|
+
for (const rootBlocklet of rootBlocklets) {
|
|
239
|
+
const rootDid = rootBlocklet.appPid || rootBlocklet.meta.did;
|
|
180
240
|
if (rootBlocklet.children) {
|
|
181
241
|
for (const childBlocklet of rootBlocklet.children) {
|
|
182
242
|
const isRunning =
|
|
183
|
-
|
|
184
|
-
const isInProgress =
|
|
243
|
+
childBlocklet.status === BlockletStatus.running || childBlocklet.greenStatus === BlockletStatus.running;
|
|
244
|
+
const isInProgress =
|
|
245
|
+
inProgressStatuses.includes(childBlocklet.status) || inProgressStatuses.includes(childBlocklet.greenStatus);
|
|
246
|
+
const isStopped =
|
|
247
|
+
childBlocklet.status === BlockletStatus.stopped && childBlocklet.greenStatus === BlockletStatus.stopped;
|
|
248
|
+
|
|
249
|
+
// 如果处于过 running, 或 stopped,则删除 progressBlockletsTime
|
|
250
|
+
if (isRunning || isStopped) {
|
|
251
|
+
delete this.progressBlockletsTime[`${rootDid}-${childBlocklet.meta.did}`];
|
|
252
|
+
}
|
|
185
253
|
if (isRunning || isInProgress) {
|
|
186
|
-
if (!this.runningBlocklets[
|
|
187
|
-
this.runningBlocklets[
|
|
254
|
+
if (!this.runningBlocklets[rootDid]) {
|
|
255
|
+
this.runningBlocklets[rootDid] = [];
|
|
188
256
|
}
|
|
189
|
-
if (this.runningBlocklets[
|
|
257
|
+
if (this.runningBlocklets[rootDid].find((child) => child.meta.did === childBlocklet.meta.did)) {
|
|
190
258
|
continue;
|
|
191
259
|
}
|
|
192
|
-
this.runningBlocklets[
|
|
193
|
-
this.
|
|
260
|
+
this.runningBlocklets[rootDid].push(childBlocklet);
|
|
261
|
+
this.rootBlockletsInfo[rootDid] = rootBlocklet;
|
|
194
262
|
}
|
|
195
263
|
}
|
|
196
264
|
}
|
|
@@ -201,195 +269,294 @@ class EnsureBlockletRunning {
|
|
|
201
269
|
getFakeRunningBlocklets = async () => {
|
|
202
270
|
const rootDids = Object.keys(this.runningBlocklets);
|
|
203
271
|
await pAll(
|
|
204
|
-
rootDids.map((
|
|
272
|
+
rootDids.map((rootDid) => {
|
|
205
273
|
return async () => {
|
|
206
|
-
|
|
274
|
+
// runningBlocklets[rootDid] 存储的是该根 blocklet 下的所有子组件(childBlocklets)
|
|
275
|
+
const childBlocklets = this.runningBlocklets[rootDid];
|
|
207
276
|
// eslint-disable-next-line
|
|
277
|
+
const fakeDids = [];
|
|
208
278
|
await pAll(
|
|
209
|
-
|
|
279
|
+
childBlocklets.map((childBlocklet) => {
|
|
210
280
|
return async () => {
|
|
211
|
-
if (!shouldCheckHealthy(
|
|
212
|
-
// 如果
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
281
|
+
if (!shouldCheckHealthy(childBlocklet)) {
|
|
282
|
+
// 如果 childBlocklet 是不需要启动的,并且不是 running,则设置为 running 状态
|
|
283
|
+
if (
|
|
284
|
+
childBlocklet.status !== BlockletStatus.running &&
|
|
285
|
+
childBlocklet.greenStatus !== BlockletStatus.running
|
|
286
|
+
) {
|
|
287
|
+
await this.states.blocklet.setBlockletStatus(rootDid, BlockletStatus.running, {
|
|
288
|
+
componentDids: [childBlocklet.meta.did],
|
|
216
289
|
});
|
|
217
290
|
}
|
|
218
291
|
return;
|
|
219
292
|
}
|
|
220
293
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// 如果 operator 是 z 开头的字符串,表示是 did, 跳过健康检查
|
|
225
|
-
if (blocklet.operator && isValid(blocklet.operator)) {
|
|
226
|
-
logger.info('Skip ensure running check for user-initiated operation', {
|
|
227
|
-
did,
|
|
228
|
-
componentDid: blocklet.meta.did,
|
|
229
|
-
status: blocklet.status,
|
|
230
|
-
operator: blocklet.operator,
|
|
231
|
-
});
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
294
|
+
const isInProgress =
|
|
295
|
+
inProgressStatuses.includes(childBlocklet.status) ||
|
|
296
|
+
inProgressStatuses.includes(childBlocklet.greenStatus);
|
|
234
297
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
298
|
+
// 如果处于进行中状态,则记录上次检查时间
|
|
299
|
+
if (isInProgress) {
|
|
300
|
+
const key = `${rootDid}-${childBlocklet.meta.did}`;
|
|
301
|
+
if (!this.progressBlockletsTime[key]) {
|
|
302
|
+
this.progressBlockletsTime[key] = Date.now();
|
|
303
|
+
}
|
|
304
|
+
const lastProgressTime = this.progressBlockletsTime[key];
|
|
305
|
+
// 首次调用或者超过阈值时间,则认为是 fake running
|
|
306
|
+
if (
|
|
307
|
+
!this.whenCycleCheck ||
|
|
308
|
+
Date.now() - lastProgressTime > this.getStatusTimeout(childBlocklet.status)
|
|
309
|
+
) {
|
|
248
310
|
logger.warn('InProgress timeout reached, proceeding with health check', {
|
|
249
|
-
did,
|
|
250
|
-
componentDid:
|
|
251
|
-
status:
|
|
252
|
-
|
|
253
|
-
|
|
311
|
+
did: rootDid,
|
|
312
|
+
componentDid: childBlocklet.meta.did,
|
|
313
|
+
status: childBlocklet.status,
|
|
314
|
+
});
|
|
315
|
+
} else if (this.whenCycleCheck) {
|
|
316
|
+
// 如果没有 inProgressStart 时间戳,且非首次调用,跳过检查
|
|
317
|
+
logger.info('Skip ensure running check: no inProgressStart timestamp', {
|
|
318
|
+
did: rootDid,
|
|
319
|
+
componentDid: childBlocklet.meta.did,
|
|
320
|
+
status: childBlocklet.status,
|
|
254
321
|
});
|
|
322
|
+
return;
|
|
255
323
|
}
|
|
256
324
|
}
|
|
257
325
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (health) {
|
|
264
|
-
return;
|
|
326
|
+
if (!isInProgress) {
|
|
327
|
+
const health = await this.isBlockletPortHealthyWithRetries(childBlocklet);
|
|
328
|
+
if (health) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
265
331
|
}
|
|
266
332
|
|
|
267
|
-
logger.warn('check blocklet port healthy',
|
|
333
|
+
logger.warn('check blocklet port healthy', rootDid, childBlocklet.meta.did, 'no healthy');
|
|
268
334
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
if (this.fakeRunningBlocklets[did].find((b) => b.meta.did === blocklet.meta.did)) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
this.fakeRunningBlocklets[did].push(blocklet);
|
|
335
|
+
// Add to restart queue immediately
|
|
336
|
+
fakeDids.push(childBlocklet.meta.did);
|
|
276
337
|
};
|
|
277
338
|
}),
|
|
278
339
|
{ concurrency: 10 }
|
|
279
340
|
);
|
|
341
|
+
if (fakeDids.length > 0) {
|
|
342
|
+
this.addToRestartQueue(rootDid, fakeDids);
|
|
343
|
+
}
|
|
280
344
|
};
|
|
281
345
|
}),
|
|
282
346
|
{ concurrency: 8 }
|
|
283
347
|
);
|
|
348
|
+
};
|
|
284
349
|
|
|
285
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Add a childBlocklet to the restart queue
|
|
352
|
+
* @param {string} rootDid - Root blocklet DID
|
|
353
|
+
* @param {Object} childBlocklet - Child blocklet (component) to restart
|
|
354
|
+
*/
|
|
355
|
+
addToRestartQueue = (rootDid, dids) => {
|
|
356
|
+
// Check if job is pending (being processed)
|
|
357
|
+
if (this.restartQueueKeys.has(rootDid) || this.pendingJobs[rootDid]) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.restartQueue.push({
|
|
362
|
+
rootDid,
|
|
363
|
+
componentDids: dids,
|
|
364
|
+
firstCycle: !this.whenCycleCheck,
|
|
365
|
+
});
|
|
366
|
+
this.restartQueueKeys.add(rootDid);
|
|
286
367
|
};
|
|
287
368
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const blocklets = this.fakeRunningBlocklets[did];
|
|
295
|
-
const componentDids = blocklets.map((b) => b.meta.did);
|
|
296
|
-
if (componentDids.length > 0) {
|
|
297
|
-
const key = `${did}-${componentDids.join('-')}`;
|
|
298
|
-
this.needRestartBlocklets[key] = true;
|
|
299
|
-
if (this.restartingBlocklets[key] && this.restartingBlocklets[key] + this.checkInterval < Date.now()) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
this.restartingBlocklets[key] = Date.now();
|
|
369
|
+
// 启动重启队列,保持 4 个 worker 并发处理,如果有一个完成了,则会补充到队列中
|
|
370
|
+
startRestartQueueProcessor = () => {
|
|
371
|
+
if (this.restartQueueProcessing) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this.restartQueueProcessing = true;
|
|
303
375
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
376
|
+
const runWorker = async () => {
|
|
377
|
+
while (!this.stopped) {
|
|
378
|
+
const item = this.restartQueue.shift();
|
|
379
|
+
if (!item) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
310
382
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
383
|
+
this.restartQueueKeys.delete(item.rootDid);
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await this.restartBlockletFromQueue(item);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
logger.error('restart blocklet failed', err);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const processQueue = async () => {
|
|
394
|
+
while (!this.stopped) {
|
|
395
|
+
// 防止没有重启队列时,快速空转
|
|
396
|
+
await sleep(this.preCheckInterval);
|
|
397
|
+
|
|
398
|
+
if (this.restartQueue.length === 0) {
|
|
399
|
+
continue; // 等下一轮
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 创建固定数量 worker
|
|
403
|
+
const workers = [];
|
|
404
|
+
for (let i = 0; i < RESTART_CONCURRENCY; i++) {
|
|
405
|
+
workers.push(runWorker());
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await Promise.all(workers);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
logger.error('restart queue processor batch failed', err);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
processQueue().catch((err) => {
|
|
417
|
+
logger.error('restart queue processor failed', err);
|
|
418
|
+
this.restartQueueProcessing = false;
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Create restart notification and audit log context
|
|
424
|
+
* @param {string} rootDid - Root blocklet DID
|
|
425
|
+
* @param {Object} childBlocklet - Child blocklet object
|
|
426
|
+
* @param {string[]} componentDids - Component DIDs
|
|
427
|
+
* @returns {Object} Context object with displayName, title, description
|
|
428
|
+
*/
|
|
429
|
+
createRestartContext = async (rootDid, componentDids) => {
|
|
430
|
+
const blockletDisplayName = await this.getDisplayNameByRootDid(rootDid);
|
|
431
|
+
const componentNames = componentDids.map((componentDid) => {
|
|
432
|
+
const child = this.rootBlockletsInfo[rootDid]?.children?.find((bl) => bl.meta.did === componentDid);
|
|
433
|
+
return child ? this.getDisplayName(child) : componentDid;
|
|
434
|
+
});
|
|
435
|
+
const componentNamesStr = componentNames.length === 1 ? componentNames[0] : componentNames.join(', ');
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
blockletDisplayName,
|
|
439
|
+
title: 'Blocklet health check failed',
|
|
440
|
+
description: `Blocklet ${blockletDisplayName} with component${componentNames.length > 1 ? 's' : ''} ${componentNamesStr} health check failed, restarting...`,
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Handle restart success
|
|
446
|
+
* @param {string} key - Queue item key
|
|
447
|
+
* @param {string} rootDid - Root blocklet DID
|
|
448
|
+
* @param {string} componentDid - Component DID
|
|
449
|
+
* @param {Object} context - Restart context
|
|
450
|
+
*/
|
|
451
|
+
handleRestartSuccess = (rootDid, componentDids, firstCycle, context) => {
|
|
452
|
+
if (firstCycle) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.createAuditLog({
|
|
456
|
+
action: 'ensureBlockletRunning',
|
|
457
|
+
args: {
|
|
458
|
+
teamDid: rootDid,
|
|
459
|
+
componentDids,
|
|
460
|
+
},
|
|
461
|
+
context: {
|
|
462
|
+
user: {
|
|
463
|
+
did: rootDid,
|
|
464
|
+
role: 'daemon',
|
|
465
|
+
blockletDid: rootDid,
|
|
466
|
+
fullName: context.blockletDisplayName,
|
|
467
|
+
elevated: false,
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
result: {
|
|
471
|
+
title: context.title,
|
|
472
|
+
description: context.description,
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Handle restart failure
|
|
479
|
+
* @param {string} key - Queue item key
|
|
480
|
+
* @param {string} rootDid - Root blocklet DID
|
|
481
|
+
* @param {string[]} componentDids - Component DIDs
|
|
482
|
+
* @param {Object} context - Restart context
|
|
483
|
+
* @param {Error} error - Error object
|
|
484
|
+
*/
|
|
485
|
+
handleRestartFailure = (rootDid, componentDids, context, error) => {
|
|
486
|
+
const title = 'Restart blocklet failed when health check failed';
|
|
487
|
+
const description = `Ensure blocklet running failed, restart blocklet ${context.blockletDisplayName} with component${componentDids.length > 1 ? 's' : ''} ${componentDids.join(', ')} failed`;
|
|
488
|
+
this.notification(rootDid, title, description, 'error');
|
|
489
|
+
logger.error('restart many times blocklet failed', rootDid, componentDids, error);
|
|
490
|
+
try {
|
|
491
|
+
this.createAuditLog({
|
|
492
|
+
action: 'ensureBlockletRunning',
|
|
493
|
+
args: {
|
|
494
|
+
blockletDisplayName: context.blockletDisplayName,
|
|
495
|
+
teamDid: rootDid,
|
|
496
|
+
componentDids,
|
|
497
|
+
},
|
|
498
|
+
context: {
|
|
499
|
+
user: {
|
|
500
|
+
did: rootDid,
|
|
501
|
+
role: 'daemon',
|
|
502
|
+
blockletDid: rootDid,
|
|
503
|
+
fullName: context.blockletDisplayName,
|
|
504
|
+
elevated: false,
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
result: {
|
|
508
|
+
title,
|
|
509
|
+
description,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
} catch (err) {
|
|
513
|
+
logger.error('ensure blocklet running, create audit log failed', rootDid, componentDids, err);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Restart a childBlocklet from the queue
|
|
519
|
+
* @param {Object} item - Queue item with rootDid, componentDids, firstCycle
|
|
520
|
+
*/
|
|
521
|
+
restartBlockletFromQueue = async ({ rootDid, componentDids, firstCycle }) => {
|
|
522
|
+
// Set pending status to prevent duplicate processing
|
|
523
|
+
if (this.pendingJobs[rootDid]) {
|
|
524
|
+
logger.warn('Skip restart: job is already pending', { rootDid });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
this.pendingJobs[rootDid] = true;
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
const context = await this.createRestartContext(rootDid, componentDids);
|
|
531
|
+
if (!firstCycle) {
|
|
532
|
+
this.notification(rootDid, context.title, context.description, 'warning');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
logger.info('restart blocklet:', rootDid, componentDids);
|
|
536
|
+
await this.start({
|
|
537
|
+
did: rootDid,
|
|
538
|
+
componentDids,
|
|
539
|
+
checkHealthImmediately: true,
|
|
540
|
+
atomic: true,
|
|
541
|
+
operator: 'ensure-blocklet-running',
|
|
542
|
+
});
|
|
543
|
+
this.handleRestartSuccess(rootDid, componentDids, firstCycle, context);
|
|
544
|
+
} catch (e) {
|
|
545
|
+
await this.handleRestartFailure(rootDid, componentDids, context, e);
|
|
546
|
+
} finally {
|
|
547
|
+
// Clear pending status after processing
|
|
548
|
+
delete this.pendingJobs[rootDid];
|
|
549
|
+
// Clear progress blocklets time
|
|
550
|
+
for (const componentDid of componentDids) {
|
|
551
|
+
delete this.progressBlockletsTime[`${rootDid}-${componentDid}`];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
392
554
|
};
|
|
393
555
|
}
|
|
394
556
|
|
|
395
|
-
|
|
557
|
+
const ensureBlockletRunning = new EnsureBlockletRunning();
|
|
558
|
+
|
|
559
|
+
module.exports = {
|
|
560
|
+
ensureBlockletRunning,
|
|
561
|
+
EnsureBlockletRunning,
|
|
562
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { outputJson } = require('fs-extra');
|
|
2
2
|
const { join } = require('path');
|
|
3
|
+
const { BLOCKLET_SITE_GROUP_SUFFIX } = require('@abtnode/constant');
|
|
3
4
|
const states = require('../../../states');
|
|
4
5
|
const { BaseBackup } = require('./base');
|
|
5
6
|
const { getFileObject } = require('../utils/disk');
|
|
@@ -24,7 +25,7 @@ class RoutingRuleBackup extends BaseBackup {
|
|
|
24
25
|
*/
|
|
25
26
|
async export() {
|
|
26
27
|
const routingRule = await states.site.findOne({
|
|
27
|
-
domain: `${this.blocklet.meta.did}
|
|
28
|
+
domain: `${this.blocklet.meta.did}${BLOCKLET_SITE_GROUP_SUFFIX}`,
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
await outputJson(this.routingRuleExportPath, routingRule);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-continue */
|
|
2
2
|
/* eslint-disable no-await-in-loop */
|
|
3
3
|
const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
|
|
4
|
+
const { BLOCKLET_SITE_GROUP_SUFFIX } = require('@abtnode/constant');
|
|
4
5
|
|
|
5
6
|
const findBlocklet = (site, blocklets) => {
|
|
6
7
|
// prefix = /
|
|
@@ -101,7 +102,7 @@ module.exports = async ({ states, node, printInfo }) => {
|
|
|
101
102
|
// generate new blocklet site for every installed blocklet
|
|
102
103
|
const newBlockletSites = {}; // <blockletDid>: <site>
|
|
103
104
|
for (const blocklet of blocklets) {
|
|
104
|
-
const domain = `${blocklet.meta.did}
|
|
105
|
+
const domain = `${blocklet.meta.did}${BLOCKLET_SITE_GROUP_SUFFIX}`;
|
|
105
106
|
newBlockletSites[blocklet.meta.did] = {
|
|
106
107
|
domain,
|
|
107
108
|
domainAliases: [],
|
package/lib/router/manager.js
CHANGED
|
@@ -56,7 +56,7 @@ const {
|
|
|
56
56
|
revokeAndDeleteNFTDomainRecord,
|
|
57
57
|
getAvailableGatewayPorts,
|
|
58
58
|
} = require('../util/router');
|
|
59
|
-
const checkDNS = require('../util/check-dns
|
|
59
|
+
const checkDNS = require('../util/check-dns');
|
|
60
60
|
|
|
61
61
|
const checkPathPrefixInBlackList = (pathPrefix, extraBlackList = []) => {
|
|
62
62
|
const blacklist = [
|
package/lib/util/blocklet.js
CHANGED
|
@@ -980,7 +980,7 @@ const startBlockletProcess = async (
|
|
|
980
980
|
try {
|
|
981
981
|
await promiseSpawn(nextOptions.env.connectInternalDockerNetwork, { mute: true });
|
|
982
982
|
} catch (err) {
|
|
983
|
-
logger.
|
|
983
|
+
logger.warn('blocklet connect internal docker network failed', { processId: processIdName, error: err });
|
|
984
984
|
}
|
|
985
985
|
}
|
|
986
986
|
|
package/lib/util/check-dns.js
CHANGED
|
@@ -38,8 +38,8 @@ async function checkIsRedirectedBlocklet(domain1, domain2) {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
return domain1Blocklet === domain2Blocklet;
|
|
41
|
-
} catch (
|
|
42
|
-
logger.error('DNS resolution error:', { domain1, domain2, error
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.error('DNS resolution error:', { domain1, domain2, error });
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -66,7 +66,7 @@ async function checkDnsAndCname(domain, expectedCname = '') {
|
|
|
66
66
|
hasCname: false,
|
|
67
67
|
cnameRecords: [],
|
|
68
68
|
isCnameMatch: false,
|
|
69
|
-
error
|
|
69
|
+
error,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
}
|
package/lib/validators/org.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { Joi } = require('@arcblock/validator');
|
|
2
2
|
|
|
3
3
|
const createOrgInputSchema = Joi.object({
|
|
4
|
-
name: Joi.string().required().trim().min(1).max(
|
|
4
|
+
name: Joi.string().required().trim().min(1).max(64),
|
|
5
5
|
description: Joi.string().optional().allow('').trim().min(1).max(255),
|
|
6
6
|
ownerDid: Joi.DID().optional().allow('').allow(null),
|
|
7
7
|
});
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.17.4-beta-
|
|
6
|
+
"version": "1.17.4-beta-20251204-152224-243ff54f",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -17,21 +17,21 @@
|
|
|
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.4-beta-
|
|
21
|
-
"@abtnode/auth": "1.17.4-beta-
|
|
22
|
-
"@abtnode/certificate-manager": "1.17.4-beta-
|
|
23
|
-
"@abtnode/constant": "1.17.4-beta-
|
|
24
|
-
"@abtnode/cron": "1.17.4-beta-
|
|
25
|
-
"@abtnode/db-cache": "1.17.4-beta-
|
|
26
|
-
"@abtnode/docker-utils": "1.17.4-beta-
|
|
27
|
-
"@abtnode/logger": "1.17.4-beta-
|
|
28
|
-
"@abtnode/models": "1.17.4-beta-
|
|
29
|
-
"@abtnode/queue": "1.17.4-beta-
|
|
30
|
-
"@abtnode/rbac": "1.17.4-beta-
|
|
31
|
-
"@abtnode/router-provider": "1.17.4-beta-
|
|
32
|
-
"@abtnode/static-server": "1.17.4-beta-
|
|
33
|
-
"@abtnode/timemachine": "1.17.4-beta-
|
|
34
|
-
"@abtnode/util": "1.17.4-beta-
|
|
20
|
+
"@abtnode/analytics": "1.17.4-beta-20251204-152224-243ff54f",
|
|
21
|
+
"@abtnode/auth": "1.17.4-beta-20251204-152224-243ff54f",
|
|
22
|
+
"@abtnode/certificate-manager": "1.17.4-beta-20251204-152224-243ff54f",
|
|
23
|
+
"@abtnode/constant": "1.17.4-beta-20251204-152224-243ff54f",
|
|
24
|
+
"@abtnode/cron": "1.17.4-beta-20251204-152224-243ff54f",
|
|
25
|
+
"@abtnode/db-cache": "1.17.4-beta-20251204-152224-243ff54f",
|
|
26
|
+
"@abtnode/docker-utils": "1.17.4-beta-20251204-152224-243ff54f",
|
|
27
|
+
"@abtnode/logger": "1.17.4-beta-20251204-152224-243ff54f",
|
|
28
|
+
"@abtnode/models": "1.17.4-beta-20251204-152224-243ff54f",
|
|
29
|
+
"@abtnode/queue": "1.17.4-beta-20251204-152224-243ff54f",
|
|
30
|
+
"@abtnode/rbac": "1.17.4-beta-20251204-152224-243ff54f",
|
|
31
|
+
"@abtnode/router-provider": "1.17.4-beta-20251204-152224-243ff54f",
|
|
32
|
+
"@abtnode/static-server": "1.17.4-beta-20251204-152224-243ff54f",
|
|
33
|
+
"@abtnode/timemachine": "1.17.4-beta-20251204-152224-243ff54f",
|
|
34
|
+
"@abtnode/util": "1.17.4-beta-20251204-152224-243ff54f",
|
|
35
35
|
"@aigne/aigne-hub": "^0.10.10",
|
|
36
36
|
"@arcblock/did": "^1.27.12",
|
|
37
37
|
"@arcblock/did-connect-js": "^1.27.12",
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"@arcblock/pm2-events": "^0.0.5",
|
|
44
44
|
"@arcblock/validator": "^1.27.12",
|
|
45
45
|
"@arcblock/vc": "^1.27.12",
|
|
46
|
-
"@blocklet/constant": "1.17.4-beta-
|
|
46
|
+
"@blocklet/constant": "1.17.4-beta-20251204-152224-243ff54f",
|
|
47
47
|
"@blocklet/did-space-js": "^1.2.6",
|
|
48
|
-
"@blocklet/env": "1.17.4-beta-
|
|
48
|
+
"@blocklet/env": "1.17.4-beta-20251204-152224-243ff54f",
|
|
49
49
|
"@blocklet/error": "^0.3.3",
|
|
50
|
-
"@blocklet/meta": "1.17.4-beta-
|
|
51
|
-
"@blocklet/resolver": "1.17.4-beta-
|
|
52
|
-
"@blocklet/sdk": "1.17.4-beta-
|
|
53
|
-
"@blocklet/server-js": "1.17.4-beta-
|
|
54
|
-
"@blocklet/store": "1.17.4-beta-
|
|
50
|
+
"@blocklet/meta": "1.17.4-beta-20251204-152224-243ff54f",
|
|
51
|
+
"@blocklet/resolver": "1.17.4-beta-20251204-152224-243ff54f",
|
|
52
|
+
"@blocklet/sdk": "1.17.4-beta-20251204-152224-243ff54f",
|
|
53
|
+
"@blocklet/server-js": "1.17.4-beta-20251204-152224-243ff54f",
|
|
54
|
+
"@blocklet/store": "1.17.4-beta-20251204-152224-243ff54f",
|
|
55
55
|
"@blocklet/theme": "^3.2.11",
|
|
56
56
|
"@fidm/x509": "^1.2.1",
|
|
57
57
|
"@ocap/mcrypto": "^1.27.12",
|
|
@@ -116,5 +116,5 @@
|
|
|
116
116
|
"express": "^4.18.2",
|
|
117
117
|
"unzipper": "^0.10.11"
|
|
118
118
|
},
|
|
119
|
-
"gitHead": "
|
|
119
|
+
"gitHead": "090b2f960b834168dfa12b2a559b9256a98be312"
|
|
120
120
|
}
|