@gravito/launchpad 1.2.2 → 1.3.1
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 +49 -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 +393 -20
- package/dist/index.mjs +381 -20
- package/package.json +7 -5
- package/scripts/check-coverage.ts +64 -0
- package/src/Application/MissionQueue.ts +143 -0
- package/src/Application/PoolManager.ts +169 -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,219 @@ 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
|
+
case "queue":
|
|
568
|
+
default:
|
|
569
|
+
if (this.missionQueue.isFull) {
|
|
570
|
+
throw new PoolExhaustedException(`Rocket Pool \u8207\u968A\u5217\u5747\u5DF2\u6EFF\uFF0C\u7121\u6CD5\u8655\u7406\u4EFB\u52D9 ${mission.id}`);
|
|
571
|
+
}
|
|
572
|
+
console.log(`[PoolManager] \u4EFB\u52D9 ${mission.id} \u9032\u5165\u7B49\u5F85\u968A\u5217`);
|
|
573
|
+
return this.missionQueue.enqueue(mission);
|
|
315
574
|
}
|
|
316
|
-
rocket.assignMission(mission);
|
|
317
|
-
await this.rocketRepository.save(rocket);
|
|
318
|
-
return rocket;
|
|
319
575
|
}
|
|
320
576
|
/**
|
|
321
577
|
* 回收指定任務的火箭
|
|
@@ -324,7 +580,7 @@ var PoolManager = class {
|
|
|
324
580
|
const allRockets = await this.rocketRepository.findAll();
|
|
325
581
|
const rocket = allRockets.find((r) => r.currentMission?.id === missionId);
|
|
326
582
|
if (!rocket) {
|
|
327
|
-
console.warn(`[
|
|
583
|
+
console.warn(`[PoolManager] \u627E\u4E0D\u5230\u5C6C\u65BC\u4EFB\u52D9 ${missionId} \u7684\u706B\u7BAD`);
|
|
328
584
|
return;
|
|
329
585
|
}
|
|
330
586
|
if (this.router && rocket.assignedDomain) {
|
|
@@ -337,30 +593,67 @@ var PoolManager = class {
|
|
|
337
593
|
rocket.finishRefurbishment();
|
|
338
594
|
}
|
|
339
595
|
await this.rocketRepository.save(rocket);
|
|
596
|
+
await this.processQueue();
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* 處理等待隊列中的任務
|
|
600
|
+
*/
|
|
601
|
+
async processQueue() {
|
|
602
|
+
const queuedItem = this.missionQueue.dequeue();
|
|
603
|
+
if (!queuedItem) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const rocket = await this.rocketRepository.findIdle();
|
|
608
|
+
if (rocket) {
|
|
609
|
+
rocket.assignMission(queuedItem.mission);
|
|
610
|
+
await this.rocketRepository.save(rocket);
|
|
611
|
+
queuedItem.resolve(rocket);
|
|
612
|
+
console.log(`[PoolManager] \u968A\u5217\u4EFB\u52D9 ${queuedItem.mission.id} \u5DF2\u5206\u914D\u706B\u7BAD ${rocket.id}`);
|
|
613
|
+
} else {
|
|
614
|
+
console.warn(`[PoolManager] \u8655\u7406\u968A\u5217\u6642\u4ECD\u7121\u53EF\u7528\u706B\u7BAD\uFF0C\u4EFB\u52D9\u91CD\u65B0\u5165\u968A`);
|
|
615
|
+
this.missionQueue.enqueue(queuedItem.mission).then(queuedItem.resolve).catch(queuedItem.reject);
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
queuedItem.reject(error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* 取得隊列統計資訊
|
|
623
|
+
*/
|
|
624
|
+
getQueueStats() {
|
|
625
|
+
return this.missionQueue.getStats();
|
|
340
626
|
}
|
|
341
627
|
};
|
|
342
628
|
|
|
343
629
|
// src/Application/RefurbishUnit.ts
|
|
344
630
|
var RefurbishUnit = class {
|
|
345
|
-
constructor(docker) {
|
|
631
|
+
constructor(docker, config) {
|
|
346
632
|
this.docker = docker;
|
|
633
|
+
this.config = { ...DEFAULT_REFURBISH_CONFIG, ...config };
|
|
347
634
|
}
|
|
635
|
+
config;
|
|
348
636
|
/**
|
|
349
637
|
* 執行火箭翻新邏輯
|
|
350
638
|
*/
|
|
351
639
|
async refurbish(rocket) {
|
|
352
|
-
console.log(
|
|
640
|
+
console.log(
|
|
641
|
+
`[RefurbishUnit] \u6B63\u5728\u7FFB\u65B0\u706B\u7BAD: ${rocket.id} (\u7B56\u7565: ${this.config.strategy}, \u5BB9\u5668: ${rocket.containerId})`
|
|
642
|
+
);
|
|
353
643
|
rocket.splashDown();
|
|
354
644
|
try {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
645
|
+
const commands = this.config.cleanupCommands ?? [];
|
|
646
|
+
const fullCommand = commands.join(" && ");
|
|
647
|
+
const result = await this.docker.executeCommand(rocket.containerId, ["sh", "-c", fullCommand]);
|
|
357
648
|
if (result.exitCode !== 0) {
|
|
358
649
|
console.error(`[RefurbishUnit] \u6E05\u7406\u5931\u6557: ${result.stderr}`);
|
|
359
|
-
|
|
360
|
-
|
|
650
|
+
if (this.config.failureAction === "decommission") {
|
|
651
|
+
rocket.decommission();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
361
654
|
}
|
|
362
655
|
rocket.finishRefurbishment();
|
|
363
|
-
console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B
|
|
656
|
+
console.log(`[RefurbishUnit] \u706B\u7BAD ${rocket.id} \u7FFB\u65B0\u5B8C\u6210\uFF0C\u5DF2\u9032\u5165 IDLE \u72C0\u614B`);
|
|
364
657
|
} catch (error) {
|
|
365
658
|
console.error(`[RefurbishUnit] \u56DE\u6536\u904E\u7A0B\u767C\u751F\u7570\u5E38:`, error);
|
|
366
659
|
rocket.decommission();
|
|
@@ -393,8 +686,15 @@ var import_core = require("@gravito/core");
|
|
|
393
686
|
var DockerAdapter = class {
|
|
394
687
|
baseImage = "oven/bun:1.0-slim";
|
|
395
688
|
runtime = (0, import_core.getRuntimeAdapter)();
|
|
689
|
+
// 快取目錄配置
|
|
690
|
+
cacheConfig = {
|
|
691
|
+
hostCachePath: process.env.BUN_CACHE_PATH || `${process.env.HOME}/.bun/install/cache`,
|
|
692
|
+
containerCachePathRoot: "/root/.bun/install/cache",
|
|
693
|
+
containerCachePathBun: "/home/bun/.bun/install/cache"
|
|
694
|
+
};
|
|
396
695
|
async createBaseContainer() {
|
|
397
696
|
const rocketId = `rocket-${crypto.randomUUID()}`;
|
|
697
|
+
await this.ensureCacheDirectory();
|
|
398
698
|
const proc = this.runtime.spawn([
|
|
399
699
|
"docker",
|
|
400
700
|
"run",
|
|
@@ -406,10 +706,39 @@ var DockerAdapter = class {
|
|
|
406
706
|
"-p",
|
|
407
707
|
"3000",
|
|
408
708
|
// 讓 Docker 分配隨機宿主機埠
|
|
709
|
+
// === 快取掛載(關鍵) ===
|
|
409
710
|
"-v",
|
|
410
|
-
`${
|
|
711
|
+
`${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathRoot}:rw`,
|
|
411
712
|
"-v",
|
|
412
|
-
`${
|
|
713
|
+
`${this.cacheConfig.hostCachePath}:${this.cacheConfig.containerCachePathBun}:rw`,
|
|
714
|
+
// === 環境變數配置(確保 bun 使用快取) ===
|
|
715
|
+
"-e",
|
|
716
|
+
`BUN_INSTALL_CACHE_DIR=${this.cacheConfig.containerCachePathBun}`,
|
|
717
|
+
"-e",
|
|
718
|
+
"BUN_INSTALL_CACHE=shared",
|
|
719
|
+
"-e",
|
|
720
|
+
"BUN_INSTALL_PREFER_OFFLINE=true",
|
|
721
|
+
// === 效能相關環境變數 ===
|
|
722
|
+
"-e",
|
|
723
|
+
"NODE_ENV=development",
|
|
724
|
+
// === 資源限制(防止單一容器占用過多資源) ===
|
|
725
|
+
"--memory",
|
|
726
|
+
"1g",
|
|
727
|
+
"--memory-swap",
|
|
728
|
+
"1g",
|
|
729
|
+
"--cpus",
|
|
730
|
+
"1.0",
|
|
731
|
+
// === 安全設定 ===
|
|
732
|
+
"--security-opt",
|
|
733
|
+
"no-new-privileges:true",
|
|
734
|
+
"--cap-drop",
|
|
735
|
+
"ALL",
|
|
736
|
+
"--cap-add",
|
|
737
|
+
"CHOWN",
|
|
738
|
+
"--cap-add",
|
|
739
|
+
"SETUID",
|
|
740
|
+
"--cap-add",
|
|
741
|
+
"SETGID",
|
|
413
742
|
this.baseImage,
|
|
414
743
|
"tail",
|
|
415
744
|
"-f",
|
|
@@ -419,6 +748,7 @@ var DockerAdapter = class {
|
|
|
419
748
|
const containerId = stdout.trim();
|
|
420
749
|
const exitCode = await proc.exited;
|
|
421
750
|
if (containerId.length === 64 && /^[0-9a-f]+$/.test(containerId)) {
|
|
751
|
+
await this.verifyCacheMount(containerId);
|
|
422
752
|
return containerId;
|
|
423
753
|
}
|
|
424
754
|
if (exitCode !== 0) {
|
|
@@ -427,6 +757,37 @@ var DockerAdapter = class {
|
|
|
427
757
|
}
|
|
428
758
|
return containerId;
|
|
429
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* 確保快取目錄存在
|
|
762
|
+
*
|
|
763
|
+
* @private
|
|
764
|
+
* @since 1.3.0
|
|
765
|
+
*/
|
|
766
|
+
async ensureCacheDirectory() {
|
|
767
|
+
const cachePath = this.cacheConfig.hostCachePath;
|
|
768
|
+
const proc = this.runtime.spawn(["mkdir", "-p", cachePath]);
|
|
769
|
+
await proc.exited;
|
|
770
|
+
const chmodProc = this.runtime.spawn(["chmod", "777", cachePath]);
|
|
771
|
+
await chmodProc.exited;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* 驗證快取是否正確掛載
|
|
775
|
+
*
|
|
776
|
+
* @private
|
|
777
|
+
* @since 1.3.0
|
|
778
|
+
*/
|
|
779
|
+
async verifyCacheMount(containerId) {
|
|
780
|
+
const result = await this.executeCommand(containerId, [
|
|
781
|
+
"sh",
|
|
782
|
+
"-c",
|
|
783
|
+
`ls -la ${this.cacheConfig.containerCachePathBun} 2>/dev/null || echo 'NOT_MOUNTED'`
|
|
784
|
+
]);
|
|
785
|
+
if (result.stdout.includes("NOT_MOUNTED")) {
|
|
786
|
+
console.warn(`[DockerAdapter] \u8B66\u544A: \u5BB9\u5668 ${containerId} \u7684\u5FEB\u53D6\u76EE\u9304\u53EF\u80FD\u672A\u6B63\u78BA\u639B\u8F09`);
|
|
787
|
+
} else {
|
|
788
|
+
console.log(`[DockerAdapter] \u5FEB\u53D6\u76EE\u9304\u5DF2\u78BA\u8A8D\u639B\u8F09: ${containerId}`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
430
791
|
async getExposedPort(containerId, containerPort = 3e3) {
|
|
431
792
|
const proc = this.runtime.spawn(["docker", "port", containerId, containerPort.toString()]);
|
|
432
793
|
const stdout = await new Response(proc.stdout ?? null).text();
|
|
@@ -807,13 +1168,25 @@ async function bootstrapLaunchpad() {
|
|
|
807
1168
|
}
|
|
808
1169
|
// Annotate the CommonJS export names for ESM import in node:
|
|
809
1170
|
0 && (module.exports = {
|
|
1171
|
+
DEFAULT_POOL_CONFIG,
|
|
1172
|
+
DEFAULT_REFURBISH_CONFIG,
|
|
810
1173
|
LaunchpadOrbit,
|
|
811
1174
|
Mission,
|
|
1175
|
+
MissionAssigned,
|
|
812
1176
|
MissionControl,
|
|
1177
|
+
MissionQueue,
|
|
1178
|
+
MissionQueueTimeout,
|
|
1179
|
+
MissionQueued,
|
|
813
1180
|
PayloadInjector,
|
|
1181
|
+
PoolExhausted,
|
|
1182
|
+
PoolExhaustedException,
|
|
814
1183
|
PoolManager,
|
|
1184
|
+
QueueTimeoutException,
|
|
815
1185
|
RefurbishUnit,
|
|
1186
|
+
RefurbishmentCompleted,
|
|
816
1187
|
Rocket,
|
|
1188
|
+
RocketIgnited,
|
|
1189
|
+
RocketSplashedDown,
|
|
817
1190
|
RocketStatus,
|
|
818
1191
|
bootstrapLaunchpad
|
|
819
1192
|
});
|