@gravito/launchpad 1.2.1 → 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 +55 -0
- package/README.md +51 -43
- package/README.zh-TW.md +110 -0
- package/dist/index.d.mts +324 -7
- package/dist/index.d.ts +324 -7
- package/dist/index.js +407 -29
- package/dist/index.mjs +395 -29
- package/package.json +7 -5
- package/scripts/check-coverage.ts +64 -0
- package/src/Application/MissionQueue.ts +143 -0
- package/src/Application/PoolManager.ts +170 -18
- 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 +80 -3
- package/src/Infrastructure/Git/ShellGitAdapter.ts +1 -1
- 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/.turbo/turbo-build.log +0 -18
- package/.turbo/turbo-test$colon$coverage.log +0 -183
- package/.turbo/turbo-test.log +0 -100
- package/.turbo/turbo-typecheck.log +0 -1
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
|
-
const rocketId = `rocket-${
|
|
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
|
-
const rocketId = `rocket-${
|
|
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();
|
|
@@ -509,7 +870,7 @@ var import_core2 = require("@gravito/core");
|
|
|
509
870
|
var ShellGitAdapter = class {
|
|
510
871
|
baseDir = "/tmp/gravito-launchpad-git";
|
|
511
872
|
async clone(repoUrl, branch) {
|
|
512
|
-
const dirName = `${Date.now()}-${
|
|
873
|
+
const dirName = `${Date.now()}-${crypto.randomUUID()}`;
|
|
513
874
|
const targetDir = `${this.baseDir}/${dirName}`;
|
|
514
875
|
await (0, import_promises.mkdir)(this.baseDir, { recursive: true });
|
|
515
876
|
const runtime = (0, import_core2.getRuntimeAdapter)();
|
|
@@ -704,9 +1065,19 @@ var LaunchpadServiceProvider = class extends import_core4.ServiceProvider {
|
|
|
704
1065
|
}
|
|
705
1066
|
};
|
|
706
1067
|
var LaunchpadOrbit = class {
|
|
1068
|
+
/**
|
|
1069
|
+
* Create a new LaunchpadOrbit instance.
|
|
1070
|
+
* @param ripple - Ripple instance for real-time telemetry communication.
|
|
1071
|
+
*/
|
|
707
1072
|
constructor(ripple) {
|
|
708
1073
|
this.ripple = ripple;
|
|
709
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Install the Launchpad orbit into PlanetCore.
|
|
1077
|
+
* Registers the service provider and sets up webhook routes for deployment.
|
|
1078
|
+
*
|
|
1079
|
+
* @param core - The PlanetCore instance.
|
|
1080
|
+
*/
|
|
710
1081
|
async install(core) {
|
|
711
1082
|
core.register(new LaunchpadServiceProvider());
|
|
712
1083
|
core.router.post("/launch", async (c) => {
|
|
@@ -780,12 +1151,7 @@ async function bootstrapLaunchpad() {
|
|
|
780
1151
|
PORT: 4e3,
|
|
781
1152
|
CACHE_DRIVER: "file"
|
|
782
1153
|
},
|
|
783
|
-
orbits: [
|
|
784
|
-
new import_stasis.OrbitCache(),
|
|
785
|
-
ripple,
|
|
786
|
-
new LaunchpadOrbit(ripple)
|
|
787
|
-
// 傳入實例
|
|
788
|
-
]
|
|
1154
|
+
orbits: [new import_stasis.OrbitCache(), ripple, new LaunchpadOrbit(ripple)]
|
|
789
1155
|
});
|
|
790
1156
|
await core.bootstrap();
|
|
791
1157
|
const liftoffConfig = core.liftoff();
|
|
@@ -802,13 +1168,25 @@ async function bootstrapLaunchpad() {
|
|
|
802
1168
|
}
|
|
803
1169
|
// Annotate the CommonJS export names for ESM import in node:
|
|
804
1170
|
0 && (module.exports = {
|
|
1171
|
+
DEFAULT_POOL_CONFIG,
|
|
1172
|
+
DEFAULT_REFURBISH_CONFIG,
|
|
805
1173
|
LaunchpadOrbit,
|
|
806
1174
|
Mission,
|
|
1175
|
+
MissionAssigned,
|
|
807
1176
|
MissionControl,
|
|
1177
|
+
MissionQueue,
|
|
1178
|
+
MissionQueueTimeout,
|
|
1179
|
+
MissionQueued,
|
|
808
1180
|
PayloadInjector,
|
|
1181
|
+
PoolExhausted,
|
|
1182
|
+
PoolExhaustedException,
|
|
809
1183
|
PoolManager,
|
|
1184
|
+
QueueTimeoutException,
|
|
810
1185
|
RefurbishUnit,
|
|
1186
|
+
RefurbishmentCompleted,
|
|
811
1187
|
Rocket,
|
|
1188
|
+
RocketIgnited,
|
|
1189
|
+
RocketSplashedDown,
|
|
812
1190
|
RocketStatus,
|
|
813
1191
|
bootstrapLaunchpad
|
|
814
1192
|
});
|