@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Mission } from '../Domain/Mission'
|
|
2
|
+
import type { Rocket } from '../Domain/Rocket'
|
|
3
|
+
|
|
4
|
+
interface QueuedMission {
|
|
5
|
+
mission: Mission
|
|
6
|
+
resolve: (rocket: Rocket) => void
|
|
7
|
+
reject: (error: Error) => void
|
|
8
|
+
enqueuedAt: number
|
|
9
|
+
timeoutId: ReturnType<typeof setTimeout>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MissionQueue manages pending mission requests when pool is exhausted.
|
|
14
|
+
*
|
|
15
|
+
* Implements FIFO queue with timeout handling and backpressure support.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
* @since 1.3.0
|
|
19
|
+
*/
|
|
20
|
+
export class MissionQueue {
|
|
21
|
+
private queue: QueuedMission[] = []
|
|
22
|
+
private readonly maxSize: number
|
|
23
|
+
private readonly timeoutMs: number
|
|
24
|
+
|
|
25
|
+
constructor(maxSize = 50, timeoutMs = 30000) {
|
|
26
|
+
this.maxSize = maxSize
|
|
27
|
+
this.timeoutMs = timeoutMs
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get length(): number {
|
|
31
|
+
return this.queue.length
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get isFull(): boolean {
|
|
35
|
+
return this.queue.length >= this.maxSize
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 將任務加入隊列,返回 Promise 等待分配
|
|
40
|
+
*/
|
|
41
|
+
enqueue(mission: Mission): Promise<Rocket> {
|
|
42
|
+
if (this.isFull) {
|
|
43
|
+
return Promise.reject(
|
|
44
|
+
new PoolExhaustedException(`任務隊列已滿(${this.maxSize}),無法接受新任務`)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const timeoutId = setTimeout(() => {
|
|
50
|
+
this.removeFromQueue(mission.id)
|
|
51
|
+
reject(new QueueTimeoutException(`任務 ${mission.id} 等待超時`))
|
|
52
|
+
}, this.timeoutMs)
|
|
53
|
+
|
|
54
|
+
this.queue.push({
|
|
55
|
+
mission,
|
|
56
|
+
resolve,
|
|
57
|
+
reject,
|
|
58
|
+
enqueuedAt: Date.now(),
|
|
59
|
+
timeoutId,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
console.log(
|
|
63
|
+
`[MissionQueue] 任務 ${mission.id} 已加入隊列,當前隊列長度: ${this.queue.length}`
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 取出下一個等待的任務
|
|
70
|
+
*/
|
|
71
|
+
dequeue(): QueuedMission | null {
|
|
72
|
+
const item = this.queue.shift()
|
|
73
|
+
if (item) {
|
|
74
|
+
clearTimeout(item.timeoutId)
|
|
75
|
+
}
|
|
76
|
+
return item ?? null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 從隊列中移除指定任務
|
|
81
|
+
*/
|
|
82
|
+
private removeFromQueue(missionId: string): void {
|
|
83
|
+
const index = this.queue.findIndex((q) => q.mission.id === missionId)
|
|
84
|
+
if (index >= 0) {
|
|
85
|
+
const [removed] = this.queue.splice(index, 1)
|
|
86
|
+
clearTimeout(removed.timeoutId)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 清空隊列並拒絕所有等待的任務
|
|
92
|
+
*/
|
|
93
|
+
clear(reason: string): void {
|
|
94
|
+
for (const item of this.queue) {
|
|
95
|
+
clearTimeout(item.timeoutId)
|
|
96
|
+
item.reject(new Error(reason))
|
|
97
|
+
}
|
|
98
|
+
this.queue = []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 取得隊列統計資訊
|
|
103
|
+
*/
|
|
104
|
+
getStats(): {
|
|
105
|
+
length: number
|
|
106
|
+
maxSize: number
|
|
107
|
+
oldestWaitMs: number | null
|
|
108
|
+
} {
|
|
109
|
+
const oldestWaitMs = this.queue.length > 0 ? Date.now() - this.queue[0].enqueuedAt : null
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
length: this.queue.length,
|
|
113
|
+
maxSize: this.maxSize,
|
|
114
|
+
oldestWaitMs,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Pool 資源耗盡異常
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
* @since 1.3.0
|
|
124
|
+
*/
|
|
125
|
+
export class PoolExhaustedException extends Error {
|
|
126
|
+
constructor(message: string) {
|
|
127
|
+
super(message)
|
|
128
|
+
this.name = 'PoolExhaustedException'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 隊列等待超時異常
|
|
134
|
+
*
|
|
135
|
+
* @public
|
|
136
|
+
* @since 1.3.0
|
|
137
|
+
*/
|
|
138
|
+
export class QueueTimeoutException extends Error {
|
|
139
|
+
constructor(message: string) {
|
|
140
|
+
super(message)
|
|
141
|
+
this.name = 'QueueTimeoutException'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
IDockerAdapter,
|
|
3
|
+
IRocketRepository,
|
|
4
|
+
IRouterAdapter,
|
|
5
|
+
PoolConfig,
|
|
6
|
+
} from '../Domain/Interfaces'
|
|
7
|
+
import { DEFAULT_POOL_CONFIG } from '../Domain/Interfaces'
|
|
2
8
|
import type { Mission } from '../Domain/Mission'
|
|
3
9
|
import { Rocket } from '../Domain/Rocket'
|
|
10
|
+
import { RocketStatus } from '../Domain/RocketStatus'
|
|
11
|
+
import { MissionQueue, PoolExhaustedException } from './MissionQueue'
|
|
4
12
|
import type { RefurbishUnit } from './RefurbishUnit'
|
|
5
13
|
|
|
6
14
|
/**
|
|
@@ -13,51 +21,157 @@ import type { RefurbishUnit } from './RefurbishUnit'
|
|
|
13
21
|
* @since 3.0.0
|
|
14
22
|
*/
|
|
15
23
|
export class PoolManager {
|
|
24
|
+
private readonly config: PoolConfig
|
|
25
|
+
private readonly missionQueue: MissionQueue
|
|
26
|
+
private dynamicRocketCount = 0
|
|
27
|
+
|
|
16
28
|
constructor(
|
|
17
29
|
private dockerAdapter: IDockerAdapter,
|
|
18
30
|
private rocketRepository: IRocketRepository,
|
|
19
31
|
private refurbishUnit?: RefurbishUnit,
|
|
20
|
-
private router?: IRouterAdapter
|
|
21
|
-
|
|
32
|
+
private router?: IRouterAdapter,
|
|
33
|
+
config?: Partial<PoolConfig>
|
|
34
|
+
) {
|
|
35
|
+
this.config = { ...DEFAULT_POOL_CONFIG, ...config }
|
|
36
|
+
this.missionQueue = new MissionQueue(this.config.maxQueueSize, this.config.queueTimeoutMs)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 取得當前 Pool 狀態
|
|
41
|
+
*/
|
|
42
|
+
async getPoolStatus(): Promise<{
|
|
43
|
+
total: number
|
|
44
|
+
idle: number
|
|
45
|
+
orbiting: number
|
|
46
|
+
refurbishing: number
|
|
47
|
+
decommissioned: number
|
|
48
|
+
dynamicCount: number
|
|
49
|
+
maxRockets: number
|
|
50
|
+
queueLength: number
|
|
51
|
+
}> {
|
|
52
|
+
const rockets = await this.rocketRepository.findAll()
|
|
53
|
+
const statusCounts = {
|
|
54
|
+
total: rockets.length,
|
|
55
|
+
idle: 0,
|
|
56
|
+
orbiting: 0,
|
|
57
|
+
refurbishing: 0,
|
|
58
|
+
decommissioned: 0,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const rocket of rockets) {
|
|
62
|
+
switch (rocket.status) {
|
|
63
|
+
case RocketStatus.IDLE:
|
|
64
|
+
statusCounts.idle++
|
|
65
|
+
break
|
|
66
|
+
case RocketStatus.PREPARING:
|
|
67
|
+
case RocketStatus.ORBITING:
|
|
68
|
+
statusCounts.orbiting++
|
|
69
|
+
break
|
|
70
|
+
case RocketStatus.REFURBISHING:
|
|
71
|
+
statusCounts.refurbishing++
|
|
72
|
+
break
|
|
73
|
+
case RocketStatus.DECOMMISSIONED:
|
|
74
|
+
statusCounts.decommissioned++
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
...statusCounts,
|
|
81
|
+
dynamicCount: this.dynamicRocketCount,
|
|
82
|
+
maxRockets: this.config.maxRockets,
|
|
83
|
+
queueLength: this.missionQueue.length,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
22
86
|
|
|
23
87
|
/**
|
|
24
88
|
* 初始化發射場:預先準備指定數量的火箭
|
|
25
89
|
*/
|
|
26
|
-
async warmup(count
|
|
90
|
+
async warmup(count?: number): Promise<void> {
|
|
91
|
+
const targetCount = count ?? this.config.warmupCount
|
|
27
92
|
const currentRockets = await this.rocketRepository.findAll()
|
|
28
|
-
const
|
|
93
|
+
const activeRockets = currentRockets.filter((r) => r.status !== RocketStatus.DECOMMISSIONED)
|
|
94
|
+
const needed = Math.min(
|
|
95
|
+
targetCount - activeRockets.length,
|
|
96
|
+
this.config.maxRockets - activeRockets.length
|
|
97
|
+
)
|
|
29
98
|
|
|
30
99
|
if (needed <= 0) {
|
|
100
|
+
console.log(`[PoolManager] 無需熱機,當前已有 ${activeRockets.length} 架火箭`)
|
|
31
101
|
return
|
|
32
102
|
}
|
|
33
103
|
|
|
34
|
-
console.log(`[
|
|
104
|
+
console.log(`[PoolManager] 正在熱機,準備發射 ${needed} 架新火箭...`)
|
|
35
105
|
|
|
36
106
|
for (let i = 0; i < needed; i++) {
|
|
37
107
|
const containerId = await this.dockerAdapter.createBaseContainer()
|
|
38
|
-
const rocketId = `rocket-${
|
|
108
|
+
const rocketId = `rocket-${crypto.randomUUID()}`
|
|
39
109
|
const rocket = new Rocket(rocketId, containerId)
|
|
40
110
|
await this.rocketRepository.save(rocket)
|
|
41
111
|
}
|
|
112
|
+
|
|
113
|
+
console.log(`[PoolManager] 熱機完成,Pool 當前共 ${activeRockets.length + needed} 架火箭`)
|
|
42
114
|
}
|
|
43
115
|
|
|
44
116
|
/**
|
|
45
117
|
* 獲取一架可用的火箭並分配任務
|
|
118
|
+
*
|
|
119
|
+
* @throws {PoolExhaustedException} 當 Pool 耗盡且無法處理請求時
|
|
46
120
|
*/
|
|
47
121
|
async assignMission(mission: Mission): Promise<Rocket> {
|
|
122
|
+
// 1. 嘗試從池中獲取閒置火箭
|
|
48
123
|
let rocket = await this.rocketRepository.findIdle()
|
|
49
124
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
rocket = new Rocket(`rocket-dynamic-${Date.now()}`, containerId)
|
|
125
|
+
if (rocket) {
|
|
126
|
+
rocket.assignMission(mission)
|
|
127
|
+
await this.rocketRepository.save(rocket)
|
|
128
|
+
return rocket
|
|
55
129
|
}
|
|
56
130
|
|
|
57
|
-
|
|
58
|
-
await this.
|
|
131
|
+
// 2. 池子空了,根據策略處理
|
|
132
|
+
const poolStatus = await this.getPoolStatus()
|
|
133
|
+
const activeCount = poolStatus.total - poolStatus.decommissioned
|
|
134
|
+
|
|
135
|
+
console.log(
|
|
136
|
+
`[PoolManager] 無可用火箭,當前狀態: ` +
|
|
137
|
+
`總數=${activeCount}/${this.config.maxRockets}, ` +
|
|
138
|
+
`隊列=${this.missionQueue.length}/${this.config.maxQueueSize}`
|
|
139
|
+
)
|
|
59
140
|
|
|
60
|
-
|
|
141
|
+
switch (this.config.exhaustionStrategy) {
|
|
142
|
+
case 'reject':
|
|
143
|
+
throw new PoolExhaustedException(`Rocket Pool 已耗盡,無法處理任務 ${mission.id}`)
|
|
144
|
+
|
|
145
|
+
case 'dynamic': {
|
|
146
|
+
// 嘗試動態建立(有限制)
|
|
147
|
+
if (
|
|
148
|
+
activeCount < this.config.maxRockets &&
|
|
149
|
+
this.dynamicRocketCount < (this.config.dynamicLimit ?? 5)
|
|
150
|
+
) {
|
|
151
|
+
console.log(`[PoolManager] 動態建立後援火箭...`)
|
|
152
|
+
const containerId = await this.dockerAdapter.createBaseContainer()
|
|
153
|
+
rocket = new Rocket(`rocket-dynamic-${Date.now()}`, containerId)
|
|
154
|
+
this.dynamicRocketCount++
|
|
155
|
+
rocket.assignMission(mission)
|
|
156
|
+
await this.rocketRepository.save(rocket)
|
|
157
|
+
return rocket
|
|
158
|
+
}
|
|
159
|
+
// 超過動態限制,使用隊列策略
|
|
160
|
+
if (this.missionQueue.isFull) {
|
|
161
|
+
throw new PoolExhaustedException(`Rocket Pool 與隊列均已滿,無法處理任務 ${mission.id}`)
|
|
162
|
+
}
|
|
163
|
+
console.log(`[PoolManager] 任務 ${mission.id} 進入等待隊列`)
|
|
164
|
+
return this.missionQueue.enqueue(mission)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'queue':
|
|
168
|
+
default:
|
|
169
|
+
if (this.missionQueue.isFull) {
|
|
170
|
+
throw new PoolExhaustedException(`Rocket Pool 與隊列均已滿,無法處理任務 ${mission.id}`)
|
|
171
|
+
}
|
|
172
|
+
console.log(`[PoolManager] 任務 ${mission.id} 進入等待隊列`)
|
|
173
|
+
return this.missionQueue.enqueue(mission)
|
|
174
|
+
}
|
|
61
175
|
}
|
|
62
176
|
|
|
63
177
|
/**
|
|
@@ -68,11 +182,11 @@ export class PoolManager {
|
|
|
68
182
|
const rocket = allRockets.find((r) => r.currentMission?.id === missionId)
|
|
69
183
|
|
|
70
184
|
if (!rocket) {
|
|
71
|
-
console.warn(`[
|
|
185
|
+
console.warn(`[PoolManager] 找不到屬於任務 ${missionId} 的火箭`)
|
|
72
186
|
return
|
|
73
187
|
}
|
|
74
188
|
|
|
75
|
-
//
|
|
189
|
+
// 註銷域名映射
|
|
76
190
|
if (this.router && rocket.assignedDomain) {
|
|
77
191
|
this.router.unregister(rocket.assignedDomain)
|
|
78
192
|
}
|
|
@@ -80,11 +194,49 @@ export class PoolManager {
|
|
|
80
194
|
if (this.refurbishUnit) {
|
|
81
195
|
await this.refurbishUnit.refurbish(rocket)
|
|
82
196
|
} else {
|
|
83
|
-
// 回退邏輯:如果沒有提供回收單元,則直接標記完成
|
|
84
197
|
rocket.splashDown()
|
|
85
198
|
rocket.finishRefurbishment()
|
|
86
199
|
}
|
|
87
200
|
|
|
88
201
|
await this.rocketRepository.save(rocket)
|
|
202
|
+
|
|
203
|
+
// 火箭回歸後,檢查是否有等待的任務
|
|
204
|
+
await this.processQueue()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 處理等待隊列中的任務
|
|
209
|
+
*/
|
|
210
|
+
private async processQueue(): Promise<void> {
|
|
211
|
+
const queuedItem = this.missionQueue.dequeue()
|
|
212
|
+
if (!queuedItem) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const rocket = await this.rocketRepository.findIdle()
|
|
218
|
+
if (rocket) {
|
|
219
|
+
rocket.assignMission(queuedItem.mission)
|
|
220
|
+
await this.rocketRepository.save(rocket)
|
|
221
|
+
queuedItem.resolve(rocket)
|
|
222
|
+
console.log(`[PoolManager] 隊列任務 ${queuedItem.mission.id} 已分配火箭 ${rocket.id}`)
|
|
223
|
+
} else {
|
|
224
|
+
// 重新入隊(不太可能發生,但作為防護)
|
|
225
|
+
console.warn(`[PoolManager] 處理隊列時仍無可用火箭,任務重新入隊`)
|
|
226
|
+
this.missionQueue
|
|
227
|
+
.enqueue(queuedItem.mission)
|
|
228
|
+
.then(queuedItem.resolve)
|
|
229
|
+
.catch(queuedItem.reject)
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
queuedItem.reject(error as Error)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 取得隊列統計資訊
|
|
238
|
+
*/
|
|
239
|
+
getQueueStats() {
|
|
240
|
+
return this.missionQueue.getStats()
|
|
89
241
|
}
|
|
90
242
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { IDockerAdapter } from '../Domain/Interfaces'
|
|
1
|
+
import type { IDockerAdapter, RefurbishConfig } from '../Domain/Interfaces'
|
|
2
|
+
import { DEFAULT_REFURBISH_CONFIG } from '../Domain/Interfaces'
|
|
2
3
|
import type { Rocket } from '../Domain/Rocket'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -12,36 +13,47 @@ import type { Rocket } from '../Domain/Rocket'
|
|
|
12
13
|
* @since 3.0.0
|
|
13
14
|
*/
|
|
14
15
|
export class RefurbishUnit {
|
|
15
|
-
|
|
16
|
+
private readonly config: RefurbishConfig
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private docker: IDockerAdapter,
|
|
20
|
+
config?: Partial<RefurbishConfig>
|
|
21
|
+
) {
|
|
22
|
+
this.config = { ...DEFAULT_REFURBISH_CONFIG, ...config }
|
|
23
|
+
}
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* 執行火箭翻新邏輯
|
|
19
27
|
*/
|
|
20
28
|
async refurbish(rocket: Rocket): Promise<void> {
|
|
21
|
-
console.log(
|
|
29
|
+
console.log(
|
|
30
|
+
`[RefurbishUnit] 正在翻新火箭: ${rocket.id} ` +
|
|
31
|
+
`(策略: ${this.config.strategy}, 容器: ${rocket.containerId})`
|
|
32
|
+
)
|
|
22
33
|
|
|
23
34
|
// 1. 進入狀態機的回收階段
|
|
24
35
|
rocket.splashDown()
|
|
25
36
|
|
|
26
37
|
try {
|
|
27
38
|
// 2. 執行深度清理指令
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// - 清理暫存檔
|
|
31
|
-
const cleanupCommands = ['sh', '-c', 'rm -rf /app/* && pkill -f bun || true && rm -rf /tmp/*']
|
|
39
|
+
const commands = this.config.cleanupCommands ?? []
|
|
40
|
+
const fullCommand = commands.join(' && ')
|
|
32
41
|
|
|
33
|
-
const result = await this.docker.executeCommand(rocket.containerId,
|
|
42
|
+
const result = await this.docker.executeCommand(rocket.containerId, ['sh', '-c', fullCommand])
|
|
34
43
|
|
|
35
44
|
if (result.exitCode !== 0) {
|
|
36
45
|
console.error(`[RefurbishUnit] 清理失敗: ${result.stderr}`)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
|
|
47
|
+
if (this.config.failureAction === 'decommission') {
|
|
48
|
+
rocket.decommission()
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
// retry 策略在後續版本實現
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
// 3. 翻新完成,回歸池中
|
|
43
55
|
rocket.finishRefurbishment()
|
|
44
|
-
console.log(`[RefurbishUnit] 火箭 ${rocket.id} 翻新完成,已進入 IDLE
|
|
56
|
+
console.log(`[RefurbishUnit] 火箭 ${rocket.id} 翻新完成,已進入 IDLE 狀態`)
|
|
45
57
|
} catch (error) {
|
|
46
58
|
console.error(`[RefurbishUnit] 回收過程發生異常:`, error)
|
|
47
59
|
rocket.decommission()
|
package/src/Domain/Events.ts
CHANGED
|
@@ -50,3 +50,49 @@ export class RefurbishmentCompleted extends DomainEvent {
|
|
|
50
50
|
super()
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Event emitted when the rocket pool is exhausted.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
* @since 1.3.0
|
|
59
|
+
*/
|
|
60
|
+
export class PoolExhausted extends DomainEvent {
|
|
61
|
+
constructor(
|
|
62
|
+
public readonly totalRockets: number,
|
|
63
|
+
public readonly maxRockets: number,
|
|
64
|
+
public readonly queueLength: number
|
|
65
|
+
) {
|
|
66
|
+
super()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Event emitted when a mission is queued due to pool exhaustion.
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
* @since 1.3.0
|
|
75
|
+
*/
|
|
76
|
+
export class MissionQueued extends DomainEvent {
|
|
77
|
+
constructor(
|
|
78
|
+
public readonly missionId: string,
|
|
79
|
+
public readonly queuePosition: number
|
|
80
|
+
) {
|
|
81
|
+
super()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Event emitted when a queued mission times out.
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
* @since 1.3.0
|
|
90
|
+
*/
|
|
91
|
+
export class MissionQueueTimeout extends DomainEvent {
|
|
92
|
+
constructor(
|
|
93
|
+
public readonly missionId: string,
|
|
94
|
+
public readonly waitTimeMs: number
|
|
95
|
+
) {
|
|
96
|
+
super()
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/Domain/Interfaces.ts
CHANGED
|
@@ -80,3 +80,85 @@ export interface IRocketRepository {
|
|
|
80
80
|
findAll(): Promise<Rocket[]>
|
|
81
81
|
delete(id: string): Promise<void>
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pool 管理配置
|
|
86
|
+
*
|
|
87
|
+
* @public
|
|
88
|
+
* @since 1.3.0
|
|
89
|
+
*/
|
|
90
|
+
export interface PoolConfig {
|
|
91
|
+
/** 最大 Rocket 數量(硬限制) */
|
|
92
|
+
maxRockets: number
|
|
93
|
+
/** 初始預熱數量 */
|
|
94
|
+
warmupCount: number
|
|
95
|
+
/** 最大等待隊列長度 */
|
|
96
|
+
maxQueueSize: number
|
|
97
|
+
/** 隊列超時時間(毫秒) */
|
|
98
|
+
queueTimeoutMs: number
|
|
99
|
+
/** 資源不足時的策略:'queue' | 'reject' | 'dynamic' */
|
|
100
|
+
exhaustionStrategy: 'queue' | 'reject' | 'dynamic'
|
|
101
|
+
/** 動態建立的限制(僅當 exhaustionStrategy 為 'dynamic' 時) */
|
|
102
|
+
dynamicLimit?: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pool 管理預設配置
|
|
107
|
+
*
|
|
108
|
+
* @public
|
|
109
|
+
* @since 1.3.0
|
|
110
|
+
*/
|
|
111
|
+
export const DEFAULT_POOL_CONFIG: PoolConfig = {
|
|
112
|
+
maxRockets: 20,
|
|
113
|
+
warmupCount: 3,
|
|
114
|
+
maxQueueSize: 50,
|
|
115
|
+
queueTimeoutMs: 30000,
|
|
116
|
+
exhaustionStrategy: 'queue',
|
|
117
|
+
dynamicLimit: 5,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 容器清理策略配置
|
|
122
|
+
*
|
|
123
|
+
* @public
|
|
124
|
+
* @since 1.3.0
|
|
125
|
+
*/
|
|
126
|
+
export interface RefurbishConfig {
|
|
127
|
+
/** 清理策略:'deep-clean' | 'destroy-recreate' */
|
|
128
|
+
strategy: 'deep-clean' | 'destroy-recreate'
|
|
129
|
+
/** 深度清理命令(僅 deep-clean 策略) */
|
|
130
|
+
cleanupCommands?: string[]
|
|
131
|
+
/** 清理超時(毫秒) */
|
|
132
|
+
cleanupTimeoutMs: number
|
|
133
|
+
/** 清理失敗時的處理:'decommission' | 'retry' */
|
|
134
|
+
failureAction: 'decommission' | 'retry'
|
|
135
|
+
/** 最大重試次數(僅 retry 模式) */
|
|
136
|
+
maxRetries: number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 容器清理預設配置
|
|
141
|
+
*
|
|
142
|
+
* @public
|
|
143
|
+
* @since 1.3.0
|
|
144
|
+
*/
|
|
145
|
+
export const DEFAULT_REFURBISH_CONFIG: RefurbishConfig = {
|
|
146
|
+
strategy: 'deep-clean',
|
|
147
|
+
cleanupCommands: [
|
|
148
|
+
// 1. 殺掉所有使用者進程
|
|
149
|
+
'pkill -9 -u bun || true',
|
|
150
|
+
'pkill -9 -u root -f "bun|node" || true',
|
|
151
|
+
// 2. 清理應用目錄
|
|
152
|
+
'rm -rf /app/* /app/.* 2>/dev/null || true',
|
|
153
|
+
// 3. 清理暫存檔案
|
|
154
|
+
'rm -rf /tmp/* /var/tmp/* 2>/dev/null || true',
|
|
155
|
+
// 4. 清理 bun 暫存
|
|
156
|
+
'rm -rf /root/.bun/install/cache/tmp/* 2>/dev/null || true',
|
|
157
|
+
'rm -rf /home/bun/.bun/install/cache/tmp/* 2>/dev/null || true',
|
|
158
|
+
// 5. 清理日誌
|
|
159
|
+
'rm -rf /var/log/*.log 2>/dev/null || true',
|
|
160
|
+
],
|
|
161
|
+
cleanupTimeoutMs: 30000,
|
|
162
|
+
failureAction: 'decommission',
|
|
163
|
+
maxRetries: 2,
|
|
164
|
+
}
|
package/src/Domain/Rocket.ts
CHANGED
|
@@ -106,6 +106,24 @@ export class Rocket extends AggregateRoot<string> {
|
|
|
106
106
|
this._status = RocketStatus.DECOMMISSIONED
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* 替換底層容器(僅在 REFURBISHING 狀態時允許)
|
|
111
|
+
* 用於 destroy-recreate 策略
|
|
112
|
+
*
|
|
113
|
+
* @param newContainerId - 新容器 ID
|
|
114
|
+
* @throws {Error} 當火箭不在 REFURBISHING 狀態時
|
|
115
|
+
*
|
|
116
|
+
* @public
|
|
117
|
+
* @since 1.3.0
|
|
118
|
+
*/
|
|
119
|
+
public replaceContainer(newContainerId: string): void {
|
|
120
|
+
if (this._status !== RocketStatus.REFURBISHING) {
|
|
121
|
+
throw new Error(`無法替換容器:火箭 ${this.id} 不在 REFURBISHING 狀態`)
|
|
122
|
+
}
|
|
123
|
+
this._containerId = newContainerId
|
|
124
|
+
this._assignedDomain = null // 清除域名映射
|
|
125
|
+
}
|
|
126
|
+
|
|
109
127
|
public toJSON() {
|
|
110
128
|
return {
|
|
111
129
|
id: this.id,
|