@gravito/launchpad 1.2.2 → 1.3.2
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/CHANGELOG.md +66 -0
- package/README.md +51 -43
- package/README.zh-TW.md +110 -0
- package/dist/index.d.mts +248 -5
- package/dist/index.d.ts +248 -5
- package/dist/index.js +392 -20
- package/dist/index.mjs +380 -20
- package/package.json +11 -9
- package/scripts/check-coverage.ts +64 -0
- package/src/Application/MissionQueue.ts +143 -0
- package/src/Application/PoolManager.ts +167 -17
- package/src/Application/RefurbishUnit.ts +24 -12
- package/src/Domain/Events.ts +46 -0
- package/src/Domain/Interfaces.ts +82 -0
- package/src/Domain/Rocket.ts +18 -0
- package/src/Infrastructure/Docker/DockerAdapter.ts +79 -2
- package/src/index.ts +3 -0
- package/tests/Deployment.test.ts +3 -1
- package/tests/pool-manager.test.ts +3 -1
- package/tests/resource-limits.test.ts +241 -0
- package/tsconfig.json +14 -26
package/dist/index.js
CHANGED
|
@@ -20,13 +20,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
DEFAULT_POOL_CONFIG: () => DEFAULT_POOL_CONFIG,
|
|
24
|
+
DEFAULT_REFURBISH_CONFIG: () => DEFAULT_REFURBISH_CONFIG,
|
|
23
25
|
LaunchpadOrbit: () => LaunchpadOrbit,
|
|
24
26
|
Mission: () => Mission,
|
|
27
|
+
MissionAssigned: () => MissionAssigned,
|
|
25
28
|
MissionControl: () => MissionControl,
|
|
29
|
+
MissionQueue: () => MissionQueue,
|
|
30
|
+
MissionQueueTimeout: () => MissionQueueTimeout,
|
|
31
|
+
MissionQueued: () => MissionQueued,
|
|
26
32
|
PayloadInjector: () => PayloadInjector,
|
|
33
|
+
PoolExhausted: () => PoolExhausted,
|
|
34
|
+
PoolExhaustedException: () => PoolExhaustedException,
|
|
27
35
|
PoolManager: () => PoolManager,
|
|
36
|
+
QueueTimeoutException: () => QueueTimeoutException,
|
|
28
37
|
RefurbishUnit: () => RefurbishUnit,
|
|
38
|
+
RefurbishmentCompleted: () => RefurbishmentCompleted,
|
|
29
39
|
Rocket: () => Rocket,
|
|
40
|
+
RocketIgnited: () => RocketIgnited,
|
|
41
|
+
RocketSplashedDown: () => RocketSplashedDown,
|
|
30
42
|
RocketStatus: () => RocketStatus,
|
|
31
43
|
bootstrapLaunchpad: () => bootstrapLaunchpad
|
|
32
44
|
});
|
|
@@ -143,6 +155,36 @@ registry = "${fallbackRegistry}"
|
|
|
143
155
|
}
|
|
144
156
|
};
|
|
145
157
|
|
|
158
|
+
// src/Domain/Interfaces.ts
|
|
159
|
+
var DEFAULT_POOL_CONFIG = {
|
|
160
|
+
maxRockets: 20,
|
|
161
|
+
warmupCount: 3,
|
|
162
|
+
maxQueueSize: 50,
|
|
163
|
+
queueTimeoutMs: 3e4,
|
|
164
|
+
exhaustionStrategy: "queue",
|
|
165
|
+
dynamicLimit: 5
|
|
166
|
+
};
|
|
167
|
+
var DEFAULT_REFURBISH_CONFIG = {
|
|
168
|
+
strategy: "deep-clean",
|
|
169
|
+
cleanupCommands: [
|
|
170
|
+
// 1. 殺掉所有使用者進程
|
|
171
|
+
"pkill -9 -u bun || true",
|
|
172
|
+
'pkill -9 -u root -f "bun|node" || true',
|
|
173
|
+
// 2. 清理應用目錄
|
|
174
|
+
"rm -rf /app/* /app/.* 2>/dev/null || true",
|
|
175
|
+
// 3. 清理暫存檔案
|
|
176
|
+
"rm -rf /tmp/* /var/tmp/* 2>/dev/null || true",
|
|
177
|
+
// 4. 清理 bun 暫存
|
|
178
|
+
"rm -rf /root/.bun/install/cache/tmp/* 2>/dev/null || true",
|
|
179
|
+
"rm -rf /home/bun/.bun/install/cache/tmp/* 2>/dev/null || true",
|
|
180
|
+
// 5. 清理日誌
|
|
181
|
+
"rm -rf /var/log/*.log 2>/dev/null || true"
|
|
182
|
+
],
|
|
183
|
+
cleanupTimeoutMs: 3e4,
|
|
184
|
+
failureAction: "decommission",
|
|
185
|
+
maxRetries: 2
|
|
186
|
+
};
|
|
187
|
+
|
|
146
188
|
// src/Domain/Rocket.ts
|
|
147
189
|
var import_enterprise2 = require("@gravito/enterprise");
|
|
148
190
|
|
|
@@ -173,6 +215,28 @@ var RefurbishmentCompleted = class extends import_enterprise.DomainEvent {
|
|
|
173
215
|
this.rocketId = rocketId;
|
|
174
216
|
}
|
|
175
217
|
};
|
|
218
|
+
var PoolExhausted = class extends import_enterprise.DomainEvent {
|
|
219
|
+
constructor(totalRockets, maxRockets, queueLength) {
|
|
220
|
+
super();
|
|
221
|
+
this.totalRockets = totalRockets;
|
|
222
|
+
this.maxRockets = maxRockets;
|
|
223
|
+
this.queueLength = queueLength;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var MissionQueued = class extends import_enterprise.DomainEvent {
|
|
227
|
+
constructor(missionId, queuePosition) {
|
|
228
|
+
super();
|
|
229
|
+
this.missionId = missionId;
|
|
230
|
+
this.queuePosition = queuePosition;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var MissionQueueTimeout = class extends import_enterprise.DomainEvent {
|
|
234
|
+
constructor(missionId, waitTimeMs) {
|
|
235
|
+
super();
|
|
236
|
+
this.missionId = missionId;
|
|
237
|
+
this.waitTimeMs = waitTimeMs;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
176
240
|
|
|
177
241
|
// src/Domain/RocketStatus.ts
|
|
178
242
|
var RocketStatus = /* @__PURE__ */ ((RocketStatus2) => {
|
|
@@ -260,6 +324,23 @@ var Rocket = class _Rocket extends import_enterprise2.AggregateRoot {
|
|
|
260
324
|
decommission() {
|
|
261
325
|
this._status = "DECOMMISSIONED" /* DECOMMISSIONED */;
|
|
262
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* 替換底層容器(僅在 REFURBISHING 狀態時允許)
|
|
329
|
+
* 用於 destroy-recreate 策略
|
|
330
|
+
*
|
|
331
|
+
* @param newContainerId - 新容器 ID
|
|
332
|
+
* @throws {Error} 當火箭不在 REFURBISHING 狀態時
|
|
333
|
+
*
|
|
334
|
+
* @public
|
|
335
|
+
* @since 1.3.0
|
|
336
|
+
*/
|
|
337
|
+
replaceContainer(newContainerId) {
|
|
338
|
+
if (this._status !== "REFURBISHING" /* REFURBISHING */) {
|
|
339
|
+
throw new Error(`\u7121\u6CD5\u66FF\u63DB\u5BB9\u5668\uFF1A\u706B\u7BAD ${this.id} \u4E0D\u5728 REFURBISHING \u72C0\u614B`);
|
|
340
|
+
}
|
|
341
|
+
this._containerId = newContainerId;
|
|
342
|
+
this._assignedDomain = null;
|
|
343
|
+
}
|
|
263
344
|
toJSON() {
|
|
264
345
|
return {
|
|
265
346
|
id: this.id,
|
|
@@ -278,44 +359,218 @@ var Rocket = class _Rocket extends import_enterprise2.AggregateRoot {
|
|
|
278
359
|
}
|
|
279
360
|
};
|
|
280
361
|
|
|
362
|
+
// src/Application/MissionQueue.ts
|
|
363
|
+
var MissionQueue = class {
|
|
364
|
+
queue = [];
|
|
365
|
+
maxSize;
|
|
366
|
+
timeoutMs;
|
|
367
|
+
constructor(maxSize = 50, timeoutMs = 3e4) {
|
|
368
|
+
this.maxSize = maxSize;
|
|
369
|
+
this.timeoutMs = timeoutMs;
|
|
370
|
+
}
|
|
371
|
+
get length() {
|
|
372
|
+
return this.queue.length;
|
|
373
|
+
}
|
|
374
|
+
get isFull() {
|
|
375
|
+
return this.queue.length >= this.maxSize;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* 將任務加入隊列,返回 Promise 等待分配
|
|
379
|
+
*/
|
|
380
|
+
enqueue(mission) {
|
|
381
|
+
if (this.isFull) {
|
|
382
|
+
return Promise.reject(
|
|
383
|
+
new PoolExhaustedException(`\u4EFB\u52D9\u968A\u5217\u5DF2\u6EFF\uFF08${this.maxSize}\uFF09\uFF0C\u7121\u6CD5\u63A5\u53D7\u65B0\u4EFB\u52D9`)
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const timeoutId = setTimeout(() => {
|
|
388
|
+
this.removeFromQueue(mission.id);
|
|
389
|
+
reject(new QueueTimeoutException(`\u4EFB\u52D9 ${mission.id} \u7B49\u5F85\u8D85\u6642`));
|
|
390
|
+
}, this.timeoutMs);
|
|
391
|
+
this.queue.push({
|
|
392
|
+
mission,
|
|
393
|
+
resolve,
|
|
394
|
+
reject,
|
|
395
|
+
enqueuedAt: Date.now(),
|
|
396
|
+
timeoutId
|
|
397
|
+
});
|
|
398
|
+
console.log(
|
|
399
|
+
`[MissionQueue] \u4EFB\u52D9 ${mission.id} \u5DF2\u52A0\u5165\u968A\u5217\uFF0C\u7576\u524D\u968A\u5217\u9577\u5EA6: ${this.queue.length}`
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 取出下一個等待的任務
|
|
405
|
+
*/
|
|
406
|
+
dequeue() {
|
|
407
|
+
const item = this.queue.shift();
|
|
408
|
+
if (item) {
|
|
409
|
+
clearTimeout(item.timeoutId);
|
|
410
|
+
}
|
|
411
|
+
return item ?? null;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* 從隊列中移除指定任務
|
|
415
|
+
*/
|
|
416
|
+
removeFromQueue(missionId) {
|
|
417
|
+
const index = this.queue.findIndex((q) => q.mission.id === missionId);
|
|
418
|
+
if (index >= 0) {
|
|
419
|
+
const [removed] = this.queue.splice(index, 1);
|
|
420
|
+
clearTimeout(removed.timeoutId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* 清空隊列並拒絕所有等待的任務
|
|
425
|
+
*/
|
|
426
|
+
clear(reason) {
|
|
427
|
+
for (const item of this.queue) {
|
|
428
|
+
clearTimeout(item.timeoutId);
|
|
429
|
+
item.reject(new Error(reason));
|
|
430
|
+
}
|
|
431
|
+
this.queue = [];
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* 取得隊列統計資訊
|
|
435
|
+
*/
|
|
436
|
+
getStats() {
|
|
437
|
+
const oldestWaitMs = this.queue.length > 0 ? Date.now() - this.queue[0].enqueuedAt : null;
|
|
438
|
+
return {
|
|
439
|
+
length: this.queue.length,
|
|
440
|
+
maxSize: this.maxSize,
|
|
441
|
+
oldestWaitMs
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
var PoolExhaustedException = class extends Error {
|
|
446
|
+
constructor(message) {
|
|
447
|
+
super(message);
|
|
448
|
+
this.name = "PoolExhaustedException";
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var QueueTimeoutException = class extends Error {
|
|
452
|
+
constructor(message) {
|
|
453
|
+
super(message);
|
|
454
|
+
this.name = "QueueTimeoutException";
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
281
458
|
// src/Application/PoolManager.ts
|
|
282
459
|
var PoolManager = class {
|
|
283
|
-
constructor(dockerAdapter, rocketRepository, refurbishUnit, router) {
|
|
460
|
+
constructor(dockerAdapter, rocketRepository, refurbishUnit, router, config) {
|
|
284
461
|
this.dockerAdapter = dockerAdapter;
|
|
285
462
|
this.rocketRepository = rocketRepository;
|
|
286
463
|
this.refurbishUnit = refurbishUnit;
|
|
287
464
|
this.router = router;
|
|
465
|
+
this.config = { ...DEFAULT_POOL_CONFIG, ...config };
|
|
466
|
+
this.missionQueue = new MissionQueue(this.config.maxQueueSize, this.config.queueTimeoutMs);
|
|
467
|
+
}
|
|
468
|
+
config;
|
|
469
|
+
missionQueue;
|
|
470
|
+
dynamicRocketCount = 0;
|
|
471
|
+
/**
|
|
472
|
+
* 取得當前 Pool 狀態
|
|
473
|
+
*/
|
|
474
|
+
async getPoolStatus() {
|
|
475
|
+
const rockets = await this.rocketRepository.findAll();
|
|
476
|
+
const statusCounts = {
|
|
477
|
+
total: rockets.length,
|
|
478
|
+
idle: 0,
|
|
479
|
+
orbiting: 0,
|
|
480
|
+
refurbishing: 0,
|
|
481
|
+
decommissioned: 0
|
|
482
|
+
};
|
|
483
|
+
for (const rocket of rockets) {
|
|
484
|
+
switch (rocket.status) {
|
|
485
|
+
case "IDLE" /* IDLE */:
|
|
486
|
+
statusCounts.idle++;
|
|
487
|
+
break;
|
|
488
|
+
case "PREPARING" /* PREPARING */:
|
|
489
|
+
case "ORBITING" /* ORBITING */:
|
|
490
|
+
statusCounts.orbiting++;
|
|
491
|
+
break;
|
|
492
|
+
case "REFURBISHING" /* REFURBISHING */:
|
|
493
|
+
statusCounts.refurbishing++;
|
|
494
|
+
break;
|
|
495
|
+
case "DECOMMISSIONED" /* DECOMMISSIONED */:
|
|
496
|
+
statusCounts.decommissioned++;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
...statusCounts,
|
|
502
|
+
dynamicCount: this.dynamicRocketCount,
|
|
503
|
+
maxRockets: this.config.maxRockets,
|
|
504
|
+
queueLength: this.missionQueue.length
|
|
505
|
+
};
|
|
288
506
|
}
|
|
289
507
|
/**
|
|
290
508
|
* 初始化發射場:預先準備指定數量的火箭
|
|
291
509
|
*/
|
|
292
510
|
async warmup(count) {
|
|
511
|
+
const targetCount = count ?? this.config.warmupCount;
|
|
293
512
|
const currentRockets = await this.rocketRepository.findAll();
|
|
294
|
-
const
|
|
513
|
+
const activeRockets = currentRockets.filter((r) => r.status !== "DECOMMISSIONED" /* DECOMMISSIONED */);
|
|
514
|
+
const needed = Math.min(
|
|
515
|
+
targetCount - activeRockets.length,
|
|
516
|
+
this.config.maxRockets - activeRockets.length
|
|
517
|
+
);
|
|
295
518
|
if (needed <= 0) {
|
|
519
|
+
console.log(`[PoolManager] \u7121\u9700\u71B1\u6A5F\uFF0C\u7576\u524D\u5DF2\u6709 ${activeRockets.length} \u67B6\u706B\u7BAD`);
|
|
296
520
|
return;
|
|
297
521
|
}
|
|
298
|
-
console.log(`[
|
|
522
|
+
console.log(`[PoolManager] \u6B63\u5728\u71B1\u6A5F\uFF0C\u6E96\u5099\u767C\u5C04 ${needed} \u67B6\u65B0\u706B\u7BAD...`);
|
|
299
523
|
for (let i = 0; i < needed; i++) {
|
|
300
524
|
const containerId = await this.dockerAdapter.createBaseContainer();
|
|
301
525
|
const rocketId = `rocket-${crypto.randomUUID()}`;
|
|
302
526
|
const rocket = new Rocket(rocketId, containerId);
|
|
303
527
|
await this.rocketRepository.save(rocket);
|
|
304
528
|
}
|
|
529
|
+
console.log(`[PoolManager] \u71B1\u6A5F\u5B8C\u6210\uFF0CPool \u7576\u524D\u5171 ${activeRockets.length + needed} \u67B6\u706B\u7BAD`);
|
|
305
530
|
}
|
|
306
531
|
/**
|
|
307
532
|
* 獲取一架可用的火箭並分配任務
|
|
533
|
+
*
|
|
534
|
+
* @throws {PoolExhaustedException} 當 Pool 耗盡且無法處理請求時
|
|
308
535
|
*/
|
|
309
536
|
async assignMission(mission) {
|
|
310
537
|
let rocket = await this.rocketRepository.findIdle();
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
538
|
+
if (rocket) {
|
|
539
|
+
rocket.assignMission(mission);
|
|
540
|
+
await this.rocketRepository.save(rocket);
|
|
541
|
+
return rocket;
|
|
542
|
+
}
|
|
543
|
+
const poolStatus = await this.getPoolStatus();
|
|
544
|
+
const activeCount = poolStatus.total - poolStatus.decommissioned;
|
|
545
|
+
console.log(
|
|
546
|
+
`[PoolManager] \u7121\u53EF\u7528\u706B\u7BAD\uFF0C\u7576\u524D\u72C0\u614B: \u7E3D\u6578=${activeCount}/${this.config.maxRockets}, \u968A\u5217=${this.missionQueue.length}/${this.config.maxQueueSize}`
|
|
547
|
+
);
|
|
548
|
+
switch (this.config.exhaustionStrategy) {
|
|
549
|
+
case "reject":
|
|
550
|
+
throw new PoolExhaustedException(`Rocket Pool \u5DF2\u8017\u76E1\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
|
|
551
|
+
case "dynamic": {
|
|
552
|
+
if (activeCount < this.config.maxRockets && this.dynamicRocketCount < (this.config.dynamicLimit ?? 5)) {
|
|
553
|
+
console.log(`[PoolManager] \u52D5\u614B\u5EFA\u7ACB\u5F8C\u63F4\u706B\u7BAD...`);
|
|
554
|
+
const containerId = await this.dockerAdapter.createBaseContainer();
|
|
555
|
+
rocket = new Rocket(`rocket-dynamic-${Date.now()}`, containerId);
|
|
556
|
+
this.dynamicRocketCount++;
|
|
557
|
+
rocket.assignMission(mission);
|
|
558
|
+
await this.rocketRepository.save(rocket);
|
|
559
|
+
return rocket;
|
|
560
|
+
}
|
|
561
|
+
if (this.missionQueue.isFull) {
|
|
562
|
+
throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
|
|
563
|
+
}
|
|
564
|
+
console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
|
|
565
|
+
return this.missionQueue.enqueue(mission);
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
if (this.missionQueue.isFull) {
|
|
569
|
+
throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
|
|
570
|
+
}
|
|
571
|
+
console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
|
|
572
|
+
return this.missionQueue.enqueue(mission);
|
|
315
573
|
}
|
|
316
|
-
rocket.assignMission(mission);
|
|
317
|
-
await this.rocketRepository.save(rocket);
|
|
318
|
-
return rocket;
|
|
319
574
|
}
|
|
320
575
|
/**
|
|
321
576
|
* 回收指定任務的火箭
|
|
@@ -324,7 +579,7 @@ var PoolManager = class {
|
|
|
324
579
|
const allRockets = await this.rocketRepository.findAll();
|
|
325
580
|
const rocket = allRockets.find((r) => r.currentMission?.id === missionId);
|
|
326
581
|
if (!rocket) {
|
|
327
|
-
console.warn(`[
|
|
582
|
+
console.warn(`[PoolManager] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
|
|
328
583
|
return;
|
|
329
584
|
}
|
|
330
585
|
if (this.router && rocket.assignedDomain) {
|
|
@@ -337,30 +592,67 @@ var PoolManager = class {
|
|
|
337
592
|
rocket.finishRefurbishment();
|
|
338
593
|
}
|
|
339
594
|
await this.rocketRepository.save(rocket);
|
|
595
|
+
await this.processQueue();
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 處理等待隊列中的任務
|
|
599
|
+
*/
|
|
600
|
+
async processQueue() {
|
|
601
|
+
const queuedItem = this.missionQueue.dequeue();
|
|
602
|
+
if (!queuedItem) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const rocket = await this.rocketRepository.findIdle();
|
|
607
|
+
if (rocket) {
|
|
608
|
+
rocket.assignMission(queuedItem.mission);
|
|
609
|
+
await this.rocketRepository.save(rocket);
|
|
610
|
+
queuedItem.resolve(rocket);
|
|
611
|
+
console.log(`[PoolManager] \u968A\u5217\u4EFB\u52D9 ${queuedItem.mission.id} \u5DF2\u5206\u914D\u706B\u7BAD ${rocket.id}`);
|
|
612
|
+
} else {
|
|
613
|
+
console.warn(`[PoolManager] \u8655\u7406\u968A\u5217\u6642\u4ECD\u7121\u53EF\u7528\u706B\u7BAD\uFF0C\u4EFB\u52D9\u91CD\u65B0\u5165\u968A`);
|
|
614
|
+
this.missionQueue.enqueue(queuedItem.mission).then(queuedItem.resolve).catch(queuedItem.reject);
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
queuedItem.reject(error);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* 取得隊列統計資訊
|
|
622
|
+
*/
|
|
623
|
+
getQueueStats() {
|
|
624
|
+
return this.missionQueue.getStats();
|
|
340
625
|
}
|
|
341
626
|
};
|
|
342
627
|
|
|
343
628
|
// src/Application/RefurbishUnit.ts
|
|
344
629
|
var RefurbishUnit = class {
|
|
345
|
-
constructor(docker) {
|
|
630
|
+
constructor(docker, config) {
|
|
346
631
|
this.docker = docker;
|
|
632
|
+
this.config = { ...DEFAULT_REFURBISH_CONFIG, ...config };
|
|
347
633
|
}
|
|
634
|
+
config;
|
|
348
635
|
/**
|
|
349
636
|
* 執行火箭翻新邏輯
|
|
350
637
|
*/
|
|
351
638
|
async refurbish(rocket) {
|
|
352
|
-
console.log(
|
|
639
|
+
console.log(
|
|
640
|
+
`[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u7B56\u7565: ${this.config.strategy}, \u5BB9\u5668: ${rocket.containerId})`
|
|
641
|
+
);
|
|
353
642
|
rocket.splashDown();
|
|
354
643
|
try {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
644
|
+
const commands = this.config.cleanupCommands ?? [];
|
|
645
|
+
const fullCommand = commands.join(" && ");
|
|
646
|
+
const result = await this.docker.executeCommand(rocket.containerId, ["sh", "-c", fullCommand]);
|
|
357
647
|
if (result.exitCode !== 0) {
|
|
358
648
|
console.error(`[RefurbishUnit] \u6E05\u7406\u5931\u6557: ${result.stderr}`);
|
|
359
|
-
|
|
360
|
-
|
|
649
|
+
if (this.config.failureAction === "decommission") {
|
|
650
|
+
rocket.decommission();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
361
653
|
}
|
|
362
654
|
rocket.finishRefurbishment();
|
|
363
|
-
console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B
|
|
655
|
+
console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B`);
|
|
364
656
|
} catch (error) {
|
|
365
657
|
console.error(`[RefurbishUnit] \u56DE\u6536\u904E\u7A0B\u767C\u751F\u7570\u5E38:`, error);
|
|
366
658
|
rocket.decommission();
|
|
@@ -393,8 +685,15 @@ var import_core = require("@gravito/core");
|
|
|
393
685
|
var DockerAdapter = class {
|
|
394
686
|
baseImage = "oven/bun:1.0-slim";
|
|
395
687
|
runtime = (0, import_core.getRuntimeAdapter)();
|
|
688
|
+
// 快取目錄配置
|
|
689
|
+
cacheConfig = {
|
|
690
|
+
hostCachePath: process.env.BUN_CACHE_PATH || `${process.env.HOME}/.bun/install/cache`,
|
|
691
|
+
containerCachePathRoot: "/root/.bun/install/cache",
|
|
692
|
+
containerCachePathBun: "/home/bun/.bun/install/cache"
|
|
693
|
+
};
|
|
396
694
|
async createBaseContainer() {
|
|
397
695
|
const rocketId = `rocket-${crypto.randomUUID()}`;
|
|
696
|
+
await this.ensureCacheDirectory();
|
|
398
697
|
const proc = this.runtime.spawn([
|
|
399
698
|
"docker",
|
|
400
699
|
"run",
|
|
@@ -406,10 +705,39 @@ var DockerAdapter = class {
|
|
|
406
705
|
"-p",
|
|
407
706
|
"3000",
|
|
408
707
|
// 讓 Docker 分配隨機宿主機埠
|
|
708
|
+
// === 快取掛載(關鍵) ===
|
|
409
709
|
"-v",
|
|
410
|
-
`${
|
|
710
|
+
`${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathRoot}:rw`,
|
|
411
711
|
"-v",
|
|
412
|
-
`${
|
|
712
|
+
`${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathBun}:rw`,
|
|
713
|
+
// === 環境變數配置(確保 bun 使用快取) ===
|
|
714
|
+
"-e",
|
|
715
|
+
`BUN_INSTALL_CACHE_DIR=${this.cacheConfig.containerCachePathBun}`,
|
|
716
|
+
"-e",
|
|
717
|
+
"BUN_INSTALL_CACHE=shared",
|
|
718
|
+
"-e",
|
|
719
|
+
"BUN_INSTALL_PREFER_OFFLINE=true",
|
|
720
|
+
// === 效能相關環境變數 ===
|
|
721
|
+
"-e",
|
|
722
|
+
"NODE_ENV=development",
|
|
723
|
+
// === 資源限制(防止單一容器占用過多資源) ===
|
|
724
|
+
"--memory",
|
|
725
|
+
"1g",
|
|
726
|
+
"--memory-swap",
|
|
727
|
+
"1g",
|
|
728
|
+
"--cpus",
|
|
729
|
+
"1.0",
|
|
730
|
+
// === 安全設定 ===
|
|
731
|
+
"--security-opt",
|
|
732
|
+
"no-new-privileges:true",
|
|
733
|
+
"--cap-drop",
|
|
734
|
+
"ALL",
|
|
735
|
+
"--cap-add",
|
|
736
|
+
"CHOWN",
|
|
737
|
+
"--cap-add",
|
|
738
|
+
"SETUID",
|
|
739
|
+
"--cap-add",
|
|
740
|
+
"SETGID",
|
|
413
741
|
this.baseImage,
|
|
414
742
|
"tail",
|
|
415
743
|
"-f",
|
|
@@ -419,6 +747,7 @@ var DockerAdapter = class {
|
|
|
419
747
|
const containerId = stdout.trim();
|
|
420
748
|
const exitCode = await proc.exited;
|
|
421
749
|
if (containerId.length === 64 && /^[0-9a-f]+$/.test(containerId)) {
|
|
750
|
+
await this.verifyCacheMount(containerId);
|
|
422
751
|
return containerId;
|
|
423
752
|
}
|
|
424
753
|
if (exitCode !== 0) {
|
|
@@ -427,6 +756,37 @@ var DockerAdapter = class {
|
|
|
427
756
|
}
|
|
428
757
|
return containerId;
|
|
429
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* 確保快取目錄存在
|
|
761
|
+
*
|
|
762
|
+
* @private
|
|
763
|
+
* @since 1.3.0
|
|
764
|
+
*/
|
|
765
|
+
async ensureCacheDirectory() {
|
|
766
|
+
const cachePath = this.cacheConfig.hostCachePath;
|
|
767
|
+
const proc = this.runtime.spawn(["mkdir", "-p", cachePath]);
|
|
768
|
+
await proc.exited;
|
|
769
|
+
const chmodProc = this.runtime.spawn(["chmod", "777", cachePath]);
|
|
770
|
+
await chmodProc.exited;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* 驗證快取是否正確掛載
|
|
774
|
+
*
|
|
775
|
+
* @private
|
|
776
|
+
* @since 1.3.0
|
|
777
|
+
*/
|
|
778
|
+
async verifyCacheMount(containerId) {
|
|
779
|
+
const result = await this.executeCommand(containerId, [
|
|
780
|
+
"sh",
|
|
781
|
+
"-c",
|
|
782
|
+
`ls -la ${this.cacheConfig.containerCachePathBun} 2>/dev/null || echo 'NOT_MOUNTED'`
|
|
783
|
+
]);
|
|
784
|
+
if (result.stdout.includes("NOT_MOUNTED")) {
|
|
785
|
+
console.warn(`[DockerAdapter] \u8B66\u544A: \u5BB9\u5668 ${containerId} \u7684\u5FEB\u53D6\u76EE\u9304\u53EF\u80FD\u672A\u6B63\u78BA\u639B\u8F09`);
|
|
786
|
+
} else {
|
|
787
|
+
console.log(`[DockerAdapter] \u5FEB\u53D6\u76EE\u9304\u5DF2\u78BA\u8A8D\u639B\u8F09: ${containerId}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
430
790
|
async getExposedPort(containerId, containerPort = 3e3) {
|
|
431
791
|
const proc = this.runtime.spawn(["docker", "port", containerId, containerPort.toString()]);
|
|
432
792
|
const stdout = await new Response(proc.stdout ?? null).text();
|
|
@@ -807,13 +1167,25 @@ async function bootstrapLaunchpad() {
|
|
|
807
1167
|
}
|
|
808
1168
|
// Annotate the CommonJS export names for ESM import in node:
|
|
809
1169
|
0 && (module.exports = {
|
|
1170
|
+
DEFAULT_POOL_CONFIG,
|
|
1171
|
+
DEFAULT_REFURBISH_CONFIG,
|
|
810
1172
|
LaunchpadOrbit,
|
|
811
1173
|
Mission,
|
|
1174
|
+
MissionAssigned,
|
|
812
1175
|
MissionControl,
|
|
1176
|
+
MissionQueue,
|
|
1177
|
+
MissionQueueTimeout,
|
|
1178
|
+
MissionQueued,
|
|
813
1179
|
PayloadInjector,
|
|
1180
|
+
PoolExhausted,
|
|
1181
|
+
PoolExhaustedException,
|
|
814
1182
|
PoolManager,
|
|
1183
|
+
QueueTimeoutException,
|
|
815
1184
|
RefurbishUnit,
|
|
1185
|
+
RefurbishmentCompleted,
|
|
816
1186
|
Rocket,
|
|
1187
|
+
RocketIgnited,
|
|
1188
|
+
RocketSplashedDown,
|
|
817
1189
|
RocketStatus,
|
|
818
1190
|
bootstrapLaunchpad
|
|
819
1191
|
});
|