@actiondock/core 2.5.1 → 2.7.0
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/dist/app/app.d.ts +14 -1
- package/dist/app/app.js +88 -222
- package/dist/app/static-index.d.ts +39 -0
- package/dist/app/static-index.js +164 -0
- package/dist/app/types.d.ts +5 -0
- package/dist/catalog/action-index.js +6 -4
- package/dist/catalog/package-catalog.js +2 -1
- package/dist/doctor/checks/index.d.ts +5 -0
- package/dist/doctor/checks/index.js +5 -0
- package/dist/doctor/checks/project-locate.d.ts +6 -0
- package/dist/doctor/checks/project-locate.js +36 -0
- package/dist/doctor/checks/project.d.ts +6 -0
- package/dist/doctor/checks/project.js +383 -0
- package/dist/doctor/checks/registry.d.ts +13 -0
- package/dist/doctor/checks/registry.js +207 -0
- package/dist/doctor/checks/runtime.d.ts +13 -0
- package/dist/doctor/checks/runtime.js +90 -0
- package/dist/doctor/checks/storage.d.ts +5 -0
- package/dist/doctor/checks/storage.js +34 -0
- package/dist/doctor/context.d.ts +35 -0
- package/dist/doctor/context.js +11 -0
- package/dist/doctor/doctor.d.ts +5 -0
- package/dist/doctor/doctor.js +33 -602
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +22 -0
- package/dist/execution/service.js +14 -9
- package/dist/export/templates.js +42 -30
- package/dist/host/host.d.ts +33 -0
- package/dist/host/host.js +222 -362
- package/dist/host/routing.d.ts +92 -0
- package/dist/host/routing.js +238 -0
- package/dist/host/types.d.ts +8 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/input/advice.d.ts +100 -0
- package/dist/input/advice.js +767 -0
- package/dist/input/describe.d.ts +106 -0
- package/dist/input/describe.js +176 -0
- package/dist/input/file-input.d.ts +54 -0
- package/dist/input/file-input.js +124 -0
- package/dist/input/flat-decode.d.ts +11 -0
- package/dist/input/flat-decode.js +13 -0
- package/dist/input/flat-errors.d.ts +72 -0
- package/dist/input/flat-errors.js +87 -0
- package/dist/input/flat-materializer.d.ts +29 -0
- package/dist/input/flat-materializer.js +179 -0
- package/dist/input/flat-parser.d.ts +50 -0
- package/dist/input/flat-parser.js +169 -0
- package/dist/input/flat-predicates.d.ts +34 -0
- package/dist/input/flat-predicates.js +50 -0
- package/dist/input/index.d.ts +14 -0
- package/dist/input/index.js +14 -0
- package/dist/input/input-resolver.d.ts +63 -0
- package/dist/input/input-resolver.js +254 -0
- package/dist/input/json-depth-scanner.d.ts +17 -0
- package/dist/input/json-depth-scanner.js +49 -0
- package/dist/input/metadata.d.ts +99 -0
- package/dist/input/metadata.js +99 -0
- package/dist/input/stdin-input.d.ts +26 -0
- package/dist/input/stdin-input.js +125 -0
- package/dist/input/utf8.d.ts +19 -0
- package/dist/input/utf8.js +36 -0
- package/dist/input/validation-mapper.d.ts +27 -0
- package/dist/input/validation-mapper.js +144 -0
- package/dist/ipc/host.js +38 -0
- package/dist/json/index.d.ts +1 -0
- package/dist/json/index.js +1 -0
- package/dist/json/value-validator.d.ts +88 -0
- package/dist/json/value-validator.js +330 -0
- package/dist/platform/types.d.ts +7 -0
- package/dist/process/control-arbiter.d.ts +107 -0
- package/dist/process/control-arbiter.js +251 -0
- package/dist/process/diagnostics-sink.d.ts +26 -0
- package/dist/process/diagnostics-sink.js +35 -0
- package/dist/process/index.d.ts +7 -0
- package/dist/process/index.js +7 -0
- package/dist/process/input-dispatcher.d.ts +88 -0
- package/dist/process/input-dispatcher.js +240 -0
- package/dist/process/managed-record.d.ts +78 -0
- package/dist/process/managed-record.js +1 -0
- package/dist/process/process-manager.d.ts +15 -56
- package/dist/process/process-manager.js +119 -707
- package/dist/process/reservation-table.d.ts +39 -0
- package/dist/process/reservation-table.js +80 -0
- package/dist/process/run-executor.d.ts +16 -0
- package/dist/process/run-executor.js +155 -0
- package/dist/process/terminal-output-cache.d.ts +66 -0
- package/dist/process/terminal-output-cache.js +132 -0
- package/dist/profile/client-actions.d.ts +56 -0
- package/dist/profile/client-actions.js +215 -0
- package/dist/profile/client-config.d.ts +20 -0
- package/dist/profile/client-config.js +51 -0
- package/dist/profile/client-health.d.ts +14 -0
- package/dist/profile/client-health.js +69 -0
- package/dist/profile/client-playbooks.d.ts +19 -0
- package/dist/profile/client-playbooks.js +36 -0
- package/dist/profile/client-query.d.ts +30 -0
- package/dist/profile/client-query.js +70 -0
- package/dist/profile/client-runs.d.ts +42 -0
- package/dist/profile/client-runs.js +58 -0
- package/dist/profile/client-state.d.ts +56 -0
- package/dist/profile/client-state.js +83 -0
- package/dist/profile/client-transport.d.ts +61 -0
- package/dist/profile/client-transport.js +156 -0
- package/dist/profile/client.d.ts +23 -142
- package/dist/profile/client.js +28 -588
- package/dist/profile/manager.js +1 -1
- package/dist/project/closure.d.ts +4 -4
- package/dist/project/closure.js +11 -5
- package/dist/project/init.js +2 -2
- package/dist/project/loader.d.ts +0 -25
- package/dist/project/loader.js +1 -154
- package/dist/project/transactions.d.ts +12 -14
- package/dist/project/transactions.js +60 -836
- package/dist/registry/registry.js +6 -4
- package/dist/registry/resolve.js +1 -1
- package/dist/runtime/action-registry.d.ts +45 -0
- package/dist/runtime/action-registry.js +108 -0
- package/dist/runtime/clock.d.ts +5 -17
- package/dist/runtime/clock.js +5 -15
- package/dist/runtime/context.d.ts +3 -0
- package/dist/runtime/context.js +17 -56
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/package-runner-factory.d.ts +82 -0
- package/dist/runtime/package-runner-factory.js +186 -0
- package/dist/runtime/run-persistence.d.ts +85 -0
- package/dist/runtime/run-persistence.js +111 -0
- package/dist/runtime/runner.d.ts +25 -45
- package/dist/runtime/runner.js +130 -354
- package/dist/runtime/standalone.d.ts +25 -2
- package/dist/runtime/standalone.js +305 -87
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/validator.d.ts +10 -0
- package/dist/schema/validator.js +48 -54
- package/dist/server/dispatcher.d.ts +8 -3
- package/dist/server/dispatcher.js +16 -40
- package/dist/server/routes/runs.js +6 -4
- package/dist/server/security.d.ts +1 -7
- package/dist/server/security.js +2 -13
- package/dist/storage/clock.d.ts +25 -0
- package/dist/storage/clock.js +23 -0
- package/dist/storage/data-dir-lock.d.ts +3 -36
- package/dist/storage/data-dir-lock.js +48 -881
- package/dist/storage/index.d.ts +8 -1
- package/dist/storage/index.js +8 -2
- package/dist/storage/lazy.js +1 -0
- package/dist/storage/lock-core.d.ts +162 -0
- package/dist/storage/lock-core.js +885 -0
- package/dist/storage/sqlite.d.ts +19 -6
- package/dist/storage/sqlite.js +81 -89
- package/dist/storage/types.d.ts +27 -5
- package/dist/storage/types.js +21 -0
- package/dist/storage/utils.d.ts +22 -0
- package/dist/storage/utils.js +34 -0
- package/dist/target/local.d.ts +1 -0
- package/dist/target/local.js +23 -17
- package/dist/target/remote-errors.d.ts +21 -0
- package/dist/target/remote-errors.js +51 -0
- package/dist/target/remote-polling.d.ts +35 -0
- package/dist/target/remote-polling.js +104 -0
- package/dist/target/remote.d.ts +4 -0
- package/dist/target/remote.js +108 -301
- package/dist/target/sse-parser.d.ts +35 -0
- package/dist/target/sse-parser.js +163 -0
- package/dist/target/target.js +2 -0
- package/dist/target/types.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/net.d.ts +10 -0
- package/dist/utils/net.js +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { decodeBytes, encodeBytes, } from "@actiondock/sdk";
|
|
3
|
-
import { ACCESS_DENIED, CONTROL_BUSY,
|
|
3
|
+
import { ACCESS_DENIED, CONTROL_BUSY, CONTROL_REVOKED, INPUT_VALIDATION_FAILED, NOT_FOUND, PROCESS_CANCELLED, PROCESS_LOST, PROCESS_QUARANTINED, PROCESS_SPAWN_ERROR, QUOTA_EXCEEDED, REQUEST_CONFLICT, SERVER_ERROR, UNSUPPORTED_CAPABILITY, INVALID_CURSOR, OUTPUT_UNAVAILABLE, ProcessError, } from "../errors.js";
|
|
4
4
|
import { parseCursor, compareCursorPos } from "./cursor.js";
|
|
5
5
|
import { ContextProcessAPI } from "./context-process.js";
|
|
6
|
+
import { ControlArbiter } from "./control-arbiter.js";
|
|
7
|
+
import { InputDispatcher } from "./input-dispatcher.js";
|
|
8
|
+
import { DiagnosticsSink } from "./diagnostics-sink.js";
|
|
6
9
|
import { MemoryProcessMetadataStore, } from "./metadata-store.js";
|
|
7
10
|
import { ProcessOutputLog } from "./output-log.js";
|
|
11
|
+
import { ReservationTable, } from "./reservation-table.js";
|
|
12
|
+
import { RunExecutor } from "./run-executor.js";
|
|
13
|
+
import { TerminalOutputCache } from "./terminal-output-cache.js";
|
|
8
14
|
/**
|
|
9
15
|
* 格式化生成作用域唯一隔离字符串。
|
|
10
16
|
*/
|
|
@@ -95,12 +101,6 @@ function toStoredProcessRecord(owner, info, startRequestId) {
|
|
|
95
101
|
startRequestId,
|
|
96
102
|
};
|
|
97
103
|
}
|
|
98
|
-
/**
|
|
99
|
-
* 格式化同步幂等预占键。
|
|
100
|
-
*/
|
|
101
|
-
function formatReservationKey(key, operation) {
|
|
102
|
-
return `${key.hostEpoch}:${key.scope}:${key.processId ?? ""}:${operation}:${key.requestId}`;
|
|
103
|
-
}
|
|
104
104
|
/**
|
|
105
105
|
* 校验正整数毫秒时长参数,非法时抛出参数错误。
|
|
106
106
|
*/
|
|
@@ -129,18 +129,21 @@ export class ProcessManager {
|
|
|
129
129
|
defaultLimits;
|
|
130
130
|
drainDeadlineMs;
|
|
131
131
|
terminalLogRetentionMs;
|
|
132
|
-
logger;
|
|
133
132
|
processes = new Map();
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
/** 已淘汰输出日志的墓碑缓存:防止因日志淘汰静默丢失输出数据,支撑缺口明确告知与不可用异常 */
|
|
137
|
-
evictedOutputTombstones = new Map();
|
|
133
|
+
/** 内部诊断日志汇聚器:未注入 logger 时保留最近的持久化与驱动错误,避免静默吞没异常 */
|
|
134
|
+
diagnosticsSink;
|
|
138
135
|
/** 同步幂等预占表:以复合键在异步落盘窗口内锁定并发重复请求 */
|
|
139
|
-
|
|
136
|
+
reservationTable;
|
|
137
|
+
/** 终态输出日志保留缓存:附带 TTL 过期回收与 LRU 淘汰,及墓碑登记 */
|
|
138
|
+
terminalOutputCache;
|
|
139
|
+
/** 一次性 run 执行器:仅依赖驱动、错误映射与输入校验,不触碰进程注册表 */
|
|
140
|
+
runExecutor;
|
|
141
|
+
/** 控制权仲裁器:独占控制权状态机全部推进路径的唯一持有者 */
|
|
142
|
+
controlArbiter;
|
|
143
|
+
/** 输入调度器:输入队列入队、串行调度与接管原语的唯一持有者 */
|
|
144
|
+
inputDispatcher;
|
|
140
145
|
initPromise;
|
|
141
146
|
isShutdown = false;
|
|
142
|
-
/** 内部诊断日志:未注入 logger 时保留最近的持久化与驱动错误,避免静默吞没异常 */
|
|
143
|
-
diagnostics = [];
|
|
144
147
|
constructor(options) {
|
|
145
148
|
this.hostEpoch = options.hostEpoch ?? `epoch-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
146
149
|
this.driver = options.driver;
|
|
@@ -162,7 +165,37 @@ export class ProcessManager {
|
|
|
162
165
|
};
|
|
163
166
|
this.drainDeadlineMs = options.drainDeadlineMs ?? 5000;
|
|
164
167
|
this.terminalLogRetentionMs = options.terminalLogRetentionMs ?? 10 * 60 * 1000;
|
|
165
|
-
this.
|
|
168
|
+
this.diagnosticsSink = new DiagnosticsSink({ logger: options.logger });
|
|
169
|
+
this.reservationTable = new ReservationTable();
|
|
170
|
+
this.terminalOutputCache = new TerminalOutputCache({
|
|
171
|
+
retentionMs: this.terminalLogRetentionMs,
|
|
172
|
+
maxHostBufferBytes: this.quotas.maxOutputBufferBytesPerHost,
|
|
173
|
+
});
|
|
174
|
+
this.runExecutor = new RunExecutor(this.driver);
|
|
175
|
+
this.controlArbiter = new ControlArbiter({
|
|
176
|
+
persistState: (processId, patch) => this.persistState(processId, patch),
|
|
177
|
+
persistGrantReceipt: (grant, wait, proc) => this.persistReceipt({
|
|
178
|
+
hostEpoch: this.hostEpoch,
|
|
179
|
+
scope: proc.scope,
|
|
180
|
+
processId: proc.info.id,
|
|
181
|
+
requestId: wait.requestId,
|
|
182
|
+
}, grant, hashRequestPayload({ waitMs: wait.waitMs, ttlMs: wait.ttlMs })),
|
|
183
|
+
refreshIdleTimer: (proc) => this.refreshIdleTimer(proc),
|
|
184
|
+
quarantineProcess: (owner, processId, token, reason) => this.quarantineProcess(owner, processId, token, reason),
|
|
185
|
+
countHostAcquireWaiters: () => this.countHostAcquireWaiters(),
|
|
186
|
+
maxWaitersPerProcess: this.quotas.maxWaitersPerProcess,
|
|
187
|
+
maxWaitersPerHost: this.quotas.maxWaitersPerHost,
|
|
188
|
+
});
|
|
189
|
+
this.inputDispatcher = new InputDispatcher({
|
|
190
|
+
persistReceipt: (key, receipt, payloadHash) => this.persistReceipt(key, receipt, payloadHash),
|
|
191
|
+
persistState: (processId, patch) => this.persistState(processId, patch),
|
|
192
|
+
quarantineProcess: (owner, processId, token, reason) => this.quarantineProcess(owner, processId, token, reason),
|
|
193
|
+
refreshIdleTimer: (proc) => this.refreshIdleTimer(proc),
|
|
194
|
+
recordDiagnostic: (message, err) => this.recordDiagnostic(message, err),
|
|
195
|
+
countHostPendingInputBytes: () => this.countHostPendingInputBytes(),
|
|
196
|
+
maxPendingQueueBytesPerProcess: this.quotas.maxPendingQueueBytesPerProcess,
|
|
197
|
+
maxPendingQueueBytesPerHost: this.quotas.maxPendingQueueBytesPerHost,
|
|
198
|
+
}, this.controlArbiter);
|
|
166
199
|
}
|
|
167
200
|
/**
|
|
168
201
|
* 初始化宿主环境,原子性收敛旧宿主遗留的非终态进程。
|
|
@@ -192,19 +225,13 @@ export class ProcessManager {
|
|
|
192
225
|
* 记录内部诊断信息:优先写入注入的 logger,缺失时保留在内存环形缓冲区。
|
|
193
226
|
*/
|
|
194
227
|
recordDiagnostic(message, err) {
|
|
195
|
-
|
|
196
|
-
const line = detail ? `${message} (${detail})` : message;
|
|
197
|
-
this.diagnostics.push(`${new Date().toISOString()} ${line}`);
|
|
198
|
-
if (this.diagnostics.length > 100) {
|
|
199
|
-
this.diagnostics.shift();
|
|
200
|
-
}
|
|
201
|
-
this.logger?.warn("[ProcessManager] " + line);
|
|
228
|
+
this.diagnosticsSink.record(message, err);
|
|
202
229
|
}
|
|
203
230
|
/**
|
|
204
231
|
* 获取最近的内部诊断日志快照(最近条目在前)。
|
|
205
232
|
*/
|
|
206
233
|
get recentDiagnostics() {
|
|
207
|
-
return
|
|
234
|
+
return this.diagnosticsSink.recent();
|
|
208
235
|
}
|
|
209
236
|
/**
|
|
210
237
|
* 异步落盘进程状态:捕获并记录持久化失败,杜绝静默吞没异常。
|
|
@@ -230,52 +257,16 @@ export class ProcessManager {
|
|
|
230
257
|
}
|
|
231
258
|
/**
|
|
232
259
|
* 同步预占幂等请求:跨 await 的检查与落盘窗口内锁定同复合键并发调用。
|
|
233
|
-
*
|
|
234
|
-
* 返回 RequestReservation 表示已有同请求进行中,调用方可等待其结果。
|
|
235
|
-
* 若已有请求负载或操作类型不同,立即抛出 REQUEST_CONFLICT 杜绝混用结果。
|
|
260
|
+
* 实现委托给独立持有的预占表,语义详见 ReservationTable.reserve。
|
|
236
261
|
*/
|
|
237
262
|
reserveRequest(key, payloadHash, operation) {
|
|
238
|
-
|
|
239
|
-
const existing = this.requestReservations.get(reservationKey);
|
|
240
|
-
if (existing) {
|
|
241
|
-
if (existing.payloadHash !== payloadHash || existing.operation !== operation) {
|
|
242
|
-
throw new ProcessError(REQUEST_CONFLICT, "Request conflict: identical requestId with different payload", { requestId: key.requestId });
|
|
243
|
-
}
|
|
244
|
-
return existing;
|
|
245
|
-
}
|
|
246
|
-
let resolveFn = () => { };
|
|
247
|
-
let rejectFn = () => { };
|
|
248
|
-
const promise = new Promise((res, rej) => {
|
|
249
|
-
resolveFn = res;
|
|
250
|
-
rejectFn = rej;
|
|
251
|
-
});
|
|
252
|
-
// 预占 promise 可能永远无人等待:预先挂接空捕获,避免拒绝时触发 unhandledRejection
|
|
253
|
-
promise.catch(() => { });
|
|
254
|
-
const reservation = {
|
|
255
|
-
payloadHash,
|
|
256
|
-
operation,
|
|
257
|
-
promise,
|
|
258
|
-
resolve: resolveFn,
|
|
259
|
-
reject: rejectFn,
|
|
260
|
-
};
|
|
261
|
-
this.requestReservations.set(reservationKey, reservation);
|
|
262
|
-
return undefined;
|
|
263
|
+
return this.reservationTable.reserve(key, payloadHash, operation);
|
|
263
264
|
}
|
|
264
265
|
commitReservation(key, operation, value) {
|
|
265
|
-
|
|
266
|
-
const reservation = this.requestReservations.get(reservationKey);
|
|
267
|
-
if (reservation) {
|
|
268
|
-
this.requestReservations.delete(reservationKey);
|
|
269
|
-
reservation.resolve(value);
|
|
270
|
-
}
|
|
266
|
+
this.reservationTable.commit(key, operation, value);
|
|
271
267
|
}
|
|
272
268
|
rejectReservation(key, operation, err) {
|
|
273
|
-
|
|
274
|
-
const reservation = this.requestReservations.get(reservationKey);
|
|
275
|
-
if (reservation) {
|
|
276
|
-
this.requestReservations.delete(reservationKey);
|
|
277
|
-
reservation.reject(err);
|
|
278
|
-
}
|
|
269
|
+
this.reservationTable.reject(key, operation, err);
|
|
279
270
|
}
|
|
280
271
|
/**
|
|
281
272
|
* 停止全部受管进程、清理全部定时器并置为关闭态,供宿主优雅退出使用。
|
|
@@ -319,10 +310,7 @@ export class ProcessManager {
|
|
|
319
310
|
proc.drainTimer = undefined;
|
|
320
311
|
}
|
|
321
312
|
}
|
|
322
|
-
|
|
323
|
-
this.requestReservations.delete(reservationKey);
|
|
324
|
-
res.reject(new ProcessError(PROCESS_CANCELLED, "Process manager has been shut down"));
|
|
325
|
-
}
|
|
313
|
+
this.reservationTable.rejectAll(new ProcessError(PROCESS_CANCELLED, "Process manager has been shut down"));
|
|
326
314
|
}
|
|
327
315
|
assertNotShutdown() {
|
|
328
316
|
if (this.isShutdown) {
|
|
@@ -602,75 +590,13 @@ export class ProcessManager {
|
|
|
602
590
|
this.commitReservation(key, "acquire", cached);
|
|
603
591
|
return cached;
|
|
604
592
|
}
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
593
|
+
// 控制权仲裁:空闲且无等待者时直接授予,否则 FIFO 排队等待
|
|
594
|
+
// 排队路径凭据由唤醒方落盘;原请求 resolve 后在此处结算预约,保持两阶段时序不变
|
|
595
|
+
const outcome = this.controlArbiter.tryAcquire(proc, { requestId: input.requestId, waitMs: input.waitMs, ttlMs: input.ttlMs }, call, runId);
|
|
596
|
+
const grant = await outcome.waiter;
|
|
597
|
+
if (outcome.granted) {
|
|
608
598
|
await this.metadataStore.recordRequest(key, grant, payloadHash);
|
|
609
|
-
this.commitReservation(key, "acquire", grant);
|
|
610
|
-
return grant;
|
|
611
599
|
}
|
|
612
|
-
// 控制权已被持有,检查等待队列配额并进入 FIFO 排队
|
|
613
|
-
if (proc.acquireWaiters.length >= this.quotas.maxWaitersPerProcess) {
|
|
614
|
-
throw new ProcessError(QUOTA_EXCEEDED, "Process acquire waiter quota exceeded", {
|
|
615
|
-
limit: this.quotas.maxWaitersPerProcess,
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
const totalHostWaiters = this.countHostAcquireWaiters();
|
|
619
|
-
if (totalHostWaiters >= this.quotas.maxWaitersPerHost) {
|
|
620
|
-
throw new ProcessError(QUOTA_EXCEEDED, "Host acquire waiter quota exceeded", {
|
|
621
|
-
limit: this.quotas.maxWaitersPerHost,
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
// 排队路径:由 wakeNextAcquireWaiter 在授予时落盘凭据并结算预占
|
|
625
|
-
const grant = await new Promise((resolve, reject) => {
|
|
626
|
-
let timer;
|
|
627
|
-
const waiter = {
|
|
628
|
-
requestId: input.requestId,
|
|
629
|
-
waitMs: input.waitMs,
|
|
630
|
-
ttlMs: input.ttlMs,
|
|
631
|
-
runId,
|
|
632
|
-
resolve: (granted) => {
|
|
633
|
-
cleanup();
|
|
634
|
-
resolve(granted);
|
|
635
|
-
},
|
|
636
|
-
reject: (err) => {
|
|
637
|
-
cleanup();
|
|
638
|
-
reject(err);
|
|
639
|
-
},
|
|
640
|
-
};
|
|
641
|
-
const cleanup = () => {
|
|
642
|
-
if (timer)
|
|
643
|
-
clearTimeout(timer);
|
|
644
|
-
const idx = proc.acquireWaiters.indexOf(waiter);
|
|
645
|
-
if (idx !== -1) {
|
|
646
|
-
proc.acquireWaiters.splice(idx, 1);
|
|
647
|
-
}
|
|
648
|
-
if (call?.signal && waiter.onAbort) {
|
|
649
|
-
call.signal.removeEventListener("abort", waiter.onAbort);
|
|
650
|
-
}
|
|
651
|
-
};
|
|
652
|
-
if (call?.signal) {
|
|
653
|
-
if (call.signal.aborted) {
|
|
654
|
-
reject(call.signal.reason ?? new ProcessError(PROCESS_CANCELLED, "Acquire cancelled"));
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
waiter.onAbort = () => {
|
|
658
|
-
cleanup();
|
|
659
|
-
reject(call.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Acquire cancelled"));
|
|
660
|
-
};
|
|
661
|
-
call.signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
662
|
-
}
|
|
663
|
-
timer = setTimeout(() => {
|
|
664
|
-
cleanup();
|
|
665
|
-
reject(new ProcessError(CONTROL_BUSY, "Timed out waiting for process control", {
|
|
666
|
-
waitMs: input.waitMs,
|
|
667
|
-
}));
|
|
668
|
-
}, input.waitMs);
|
|
669
|
-
if (typeof timer?.unref === "function") {
|
|
670
|
-
timer.unref();
|
|
671
|
-
}
|
|
672
|
-
proc.acquireWaiters.push(waiter);
|
|
673
|
-
});
|
|
674
600
|
this.commitReservation(key, "acquire", grant);
|
|
675
601
|
return grant;
|
|
676
602
|
}
|
|
@@ -686,36 +612,9 @@ export class ProcessManager {
|
|
|
686
612
|
checkPositiveDurationMs(ttlMs, "ttlMs");
|
|
687
613
|
await this.ensureInitialized();
|
|
688
614
|
const proc = await this.getOrLoadProcess(owner, id);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (proc.info.control !== "held" || !proc.currentGrant) {
|
|
693
|
-
throw new ProcessError(CONTROL_REVOKED, "Process control is not currently held");
|
|
694
|
-
}
|
|
695
|
-
if (proc.currentGrant.token !== token) {
|
|
696
|
-
throw new ProcessError(ACCESS_DENIED, "Invalid control token");
|
|
697
|
-
}
|
|
698
|
-
if (new Date(proc.currentGrant.expiresAt).getTime() <= Date.now()) {
|
|
699
|
-
throw new ProcessError(CONTROL_EXPIRED, "Control token has expired");
|
|
700
|
-
}
|
|
701
|
-
if (proc.ttlTimer) {
|
|
702
|
-
clearTimeout(proc.ttlTimer);
|
|
703
|
-
}
|
|
704
|
-
const expiresAt = new Date(Date.now() + ttlMs).toISOString();
|
|
705
|
-
proc.currentGrant.ttlMs = ttlMs;
|
|
706
|
-
proc.currentGrant.expiresAt = expiresAt;
|
|
707
|
-
proc.ttlTimer = setTimeout(() => {
|
|
708
|
-
this.handleGrantTtlExpired(proc);
|
|
709
|
-
}, ttlMs);
|
|
710
|
-
if (typeof proc.ttlTimer.unref === "function") {
|
|
711
|
-
proc.ttlTimer.unref();
|
|
712
|
-
}
|
|
713
|
-
// 延长控制权刷新 idleTimeout
|
|
714
|
-
this.refreshIdleTimer(proc);
|
|
715
|
-
return {
|
|
716
|
-
token: proc.currentGrant.token,
|
|
717
|
-
expiresAt,
|
|
718
|
-
};
|
|
615
|
+
// 提交前校验持有凭据有效性,随后由仲裁器清理旧定时器并设置新定时器
|
|
616
|
+
this.controlArbiter.validateHeldGrant(proc, token);
|
|
617
|
+
return this.controlArbiter.renewGrant(proc, ttlMs);
|
|
719
618
|
}
|
|
720
619
|
/**
|
|
721
620
|
* 显式释放控制令牌,允许后续控制者申请。
|
|
@@ -723,6 +622,7 @@ export class ProcessManager {
|
|
|
723
622
|
async release(owner, id, token, call) {
|
|
724
623
|
await this.ensureInitialized();
|
|
725
624
|
const proc = await this.getOrLoadProcess(owner, id);
|
|
625
|
+
// 提交前校验持有凭据有效性
|
|
726
626
|
if (proc.info.control === "quarantined") {
|
|
727
627
|
throw new ProcessError(PROCESS_QUARANTINED, "Process is in quarantined state");
|
|
728
628
|
}
|
|
@@ -736,18 +636,8 @@ export class ProcessManager {
|
|
|
736
636
|
if (proc.inputQueue.length > 0) {
|
|
737
637
|
throw new ProcessError(CONTROL_BUSY, "Cannot release control while operations are pending in queue");
|
|
738
638
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
proc.ttlTimer = undefined;
|
|
742
|
-
}
|
|
743
|
-
proc.currentGrant = undefined;
|
|
744
|
-
proc.info.control = "free";
|
|
745
|
-
await this.persistState(id, {
|
|
746
|
-
control: "free",
|
|
747
|
-
controlState: "free",
|
|
748
|
-
});
|
|
749
|
-
// 正常 release 唤醒下一个等待者
|
|
750
|
-
this.wakeNextAcquireWaiter(proc);
|
|
639
|
+
// 仲裁器完成清理定时器、凭据置空与唯一唤醒下一个等待者
|
|
640
|
+
this.controlArbiter.releaseGrant(proc);
|
|
751
641
|
}
|
|
752
642
|
/**
|
|
753
643
|
* 向受管进程输入流写入原始字节数据。
|
|
@@ -780,56 +670,17 @@ export class ProcessManager {
|
|
|
780
670
|
this.commitReservation(key, "write", cached);
|
|
781
671
|
return cached;
|
|
782
672
|
}
|
|
783
|
-
//
|
|
784
|
-
this.checkOperationAuth(proc, input.token);
|
|
785
|
-
if (proc.inputClosed) {
|
|
786
|
-
throw new ProcessError(INPUT_CLOSED, "Input stream is closed");
|
|
787
|
-
}
|
|
673
|
+
// 输入调度器同步阶段完成授权校验与队列容量预扣,异步阶段落盘入队
|
|
788
674
|
const rawBytes = decodeBytes(input.data);
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
throw new ProcessError(QUEUE_FULL, "Host pending input queue limit exceeded", {
|
|
799
|
-
limit: this.quotas.maxPendingQueueBytesPerHost,
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
// 同步占位预扣队列配额,杜绝并发 await 窗口穿透
|
|
803
|
-
proc.pendingInputBytes += dataSize;
|
|
804
|
-
let pendingBytesCommitted = false;
|
|
805
|
-
try {
|
|
806
|
-
const receipt = {
|
|
807
|
-
requestId: input.requestId,
|
|
808
|
-
state: "queued",
|
|
809
|
-
};
|
|
810
|
-
await this.metadataStore.recordRequest(key, receipt, payloadHash);
|
|
811
|
-
proc.inputQueue.push({
|
|
812
|
-
type: "write",
|
|
813
|
-
requestId: input.requestId,
|
|
814
|
-
token: input.token,
|
|
815
|
-
bytes: rawBytes,
|
|
816
|
-
receipt,
|
|
817
|
-
key,
|
|
818
|
-
payloadHash,
|
|
819
|
-
cancelEpoch: proc.cancelEpoch,
|
|
820
|
-
});
|
|
821
|
-
pendingBytesCommitted = true;
|
|
822
|
-
// 异步调度推进
|
|
823
|
-
queueMicrotask(() => void this.dispatchNext(proc));
|
|
824
|
-
const queued = { ...receipt };
|
|
825
|
-
this.commitReservation(key, "write", queued);
|
|
826
|
-
return queued;
|
|
827
|
-
}
|
|
828
|
-
finally {
|
|
829
|
-
if (!pendingBytesCommitted) {
|
|
830
|
-
proc.pendingInputBytes = Math.max(0, proc.pendingInputBytes - dataSize);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
675
|
+
const queued = await this.inputDispatcher.enqueueWrite(proc, {
|
|
676
|
+
requestId: input.requestId,
|
|
677
|
+
token: input.token,
|
|
678
|
+
data: rawBytes,
|
|
679
|
+
key,
|
|
680
|
+
payloadHash,
|
|
681
|
+
});
|
|
682
|
+
this.commitReservation(key, "write", queued);
|
|
683
|
+
return queued;
|
|
833
684
|
}
|
|
834
685
|
finally {
|
|
835
686
|
// 成功路径已在 commit 中移除预占;此处仅对异常路径释放并唤醒等待方
|
|
@@ -867,9 +718,7 @@ export class ProcessManager {
|
|
|
867
718
|
this.commitReservation(key, "control", cached);
|
|
868
719
|
return cached;
|
|
869
720
|
}
|
|
870
|
-
//
|
|
871
|
-
this.checkOperationAuth(proc, input.token);
|
|
872
|
-
// 校验能力支持
|
|
721
|
+
// 校验能力支持(能力归属管理器,在调度器入队前完成)
|
|
873
722
|
if (input.action.type === "input-eof" && !proc.info.capabilities.inputEOF) {
|
|
874
723
|
throw new ProcessError(UNSUPPORTED_CAPABILITY, "Driver does not support input-eof action");
|
|
875
724
|
}
|
|
@@ -881,23 +730,14 @@ export class ProcessManager {
|
|
|
881
730
|
(!proc.info.capabilities.resize || proc.info.io.mode !== "pty")) {
|
|
882
731
|
throw new ProcessError(UNSUPPORTED_CAPABILITY, "Driver does not support resize or IO mode is not pty");
|
|
883
732
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
state: "queued",
|
|
887
|
-
};
|
|
888
|
-
await this.metadataStore.recordRequest(key, receipt, payloadHash);
|
|
889
|
-
proc.inputQueue.push({
|
|
890
|
-
type: "control",
|
|
733
|
+
// 输入调度器完成授权校验、落盘与入队
|
|
734
|
+
const queued = await this.inputDispatcher.enqueueControl(proc, {
|
|
891
735
|
requestId: input.requestId,
|
|
892
736
|
token: input.token,
|
|
893
737
|
action: input.action,
|
|
894
|
-
receipt,
|
|
895
738
|
key,
|
|
896
739
|
payloadHash,
|
|
897
|
-
cancelEpoch: proc.cancelEpoch,
|
|
898
740
|
});
|
|
899
|
-
queueMicrotask(() => void this.dispatchNext(proc));
|
|
900
|
-
const queued = { ...receipt };
|
|
901
741
|
this.commitReservation(key, "control", queued);
|
|
902
742
|
return queued;
|
|
903
743
|
}
|
|
@@ -932,7 +772,7 @@ export class ProcessManager {
|
|
|
932
772
|
const proc = await this.getOrLoadProcess(owner, id);
|
|
933
773
|
if (proc.outputUnavailable) {
|
|
934
774
|
const requestedPos = parseCursor(input.cursor, proc.info.hostEpoch, id);
|
|
935
|
-
const tombstone = proc.outputTombstone ?? this.
|
|
775
|
+
const tombstone = proc.outputTombstone ?? this.terminalOutputCache.getTombstone(id);
|
|
936
776
|
if (tombstone) {
|
|
937
777
|
const tailPos = parseCursor(tombstone.tailCursor, proc.info.hostEpoch, id);
|
|
938
778
|
const cmp = compareCursorPos(requestedPos, tailPos);
|
|
@@ -1002,11 +842,7 @@ export class ProcessManager {
|
|
|
1002
842
|
return { ...proc.info };
|
|
1003
843
|
}
|
|
1004
844
|
// 撤销控制权与定时器
|
|
1005
|
-
|
|
1006
|
-
clearTimeout(proc.ttlTimer);
|
|
1007
|
-
proc.ttlTimer = undefined;
|
|
1008
|
-
}
|
|
1009
|
-
proc.currentGrant = undefined;
|
|
845
|
+
this.controlArbiter.disposeProcess(proc);
|
|
1010
846
|
if (proc.idleTimer) {
|
|
1011
847
|
clearTimeout(proc.idleTimer);
|
|
1012
848
|
proc.idleTimer = undefined;
|
|
@@ -1019,21 +855,13 @@ export class ProcessManager {
|
|
|
1019
855
|
clearTimeout(proc.drainTimer);
|
|
1020
856
|
proc.drainTimer = undefined;
|
|
1021
857
|
}
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
proc.inputQueue = [];
|
|
1031
|
-
proc.pendingInputBytes = 0;
|
|
1032
|
-
// 拒绝排队等待者
|
|
1033
|
-
while (proc.acquireWaiters.length > 0) {
|
|
1034
|
-
const waiter = proc.acquireWaiters.shift();
|
|
1035
|
-
waiter.reject(new ProcessError(CONTROL_REVOKED, "Process was stopped"));
|
|
1036
|
-
}
|
|
858
|
+
// 调度器同步原子接管输入队列:纪元递增、标记失败、登记落盘、清空与字节归零
|
|
859
|
+
this.inputDispatcher.takeoverQueue(proc, PROCESS_CANCELLED);
|
|
860
|
+
// 终态拒绝全部排队等待者
|
|
861
|
+
this.controlArbiter.revokeAllWaiters(proc, {
|
|
862
|
+
code: CONTROL_REVOKED,
|
|
863
|
+
message: "Process was stopped",
|
|
864
|
+
});
|
|
1037
865
|
proc.info.endReason = proc.info.endReason ?? "requested";
|
|
1038
866
|
proc.info.state = "stopping";
|
|
1039
867
|
proc.info.control = "closed";
|
|
@@ -1078,21 +906,13 @@ export class ProcessManager {
|
|
|
1078
906
|
proc.ttlTimer = undefined;
|
|
1079
907
|
}
|
|
1080
908
|
proc.currentGrant = undefined;
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
//
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
1089
|
-
proc.inputQueue = [];
|
|
1090
|
-
proc.pendingInputBytes = 0;
|
|
1091
|
-
// 拒绝排队等待者
|
|
1092
|
-
while (proc.acquireWaiters.length > 0) {
|
|
1093
|
-
const waiter = proc.acquireWaiters.shift();
|
|
1094
|
-
waiter.reject(new ProcessError(PROCESS_QUARANTINED, reason || "Process entered quarantined state"));
|
|
1095
|
-
}
|
|
909
|
+
// 调度器同步原子接管输入队列:纪元递增、标记失败、登记落盘、清空与字节归零
|
|
910
|
+
this.inputDispatcher.takeoverQueue(proc, CONTROL_REVOKED);
|
|
911
|
+
// 终态拒绝全部排队等待者
|
|
912
|
+
this.controlArbiter.revokeAllWaiters(proc, {
|
|
913
|
+
code: PROCESS_QUARANTINED,
|
|
914
|
+
message: reason || "Process entered quarantined state",
|
|
915
|
+
});
|
|
1096
916
|
await this.persistState(processId, {
|
|
1097
917
|
control: "quarantined",
|
|
1098
918
|
controlState: "quarantined",
|
|
@@ -1100,340 +920,13 @@ export class ProcessManager {
|
|
|
1100
920
|
}
|
|
1101
921
|
/**
|
|
1102
922
|
* 一次性运行外部命令,收集有限输出并支持协作式取消与超时回收。
|
|
923
|
+
* 实现委托给独立持有的一次性 run 执行器,不触碰受管进程注册表。
|
|
1103
924
|
*/
|
|
1104
925
|
async run(owner, input, call) {
|
|
1105
926
|
checkOwnerAuthorized(owner, owner);
|
|
1106
927
|
await this.ensureInitialized();
|
|
1107
928
|
this.assertNotShutdown();
|
|
1108
|
-
|
|
1109
|
-
if (ioMode !== "pipe") {
|
|
1110
|
-
throw new ProcessError(UNSUPPORTED_CAPABILITY, "Run requires IO mode to be 'pipe'");
|
|
1111
|
-
}
|
|
1112
|
-
const effectiveSpec = {
|
|
1113
|
-
...input.spec,
|
|
1114
|
-
io: {
|
|
1115
|
-
...input.spec.io,
|
|
1116
|
-
mode: "pipe",
|
|
1117
|
-
},
|
|
1118
|
-
};
|
|
1119
|
-
const runProcessId = `run-${randomUUID()}`;
|
|
1120
|
-
const chunks = [];
|
|
1121
|
-
let totalBytes = 0;
|
|
1122
|
-
let truncated = false;
|
|
1123
|
-
let timedOut = false;
|
|
1124
|
-
let cancelled = false;
|
|
1125
|
-
let resolveExit;
|
|
1126
|
-
let rejectError;
|
|
1127
|
-
const exitPromise = new Promise((resolve, reject) => {
|
|
1128
|
-
resolveExit = resolve;
|
|
1129
|
-
rejectError = reject;
|
|
1130
|
-
});
|
|
1131
|
-
let driverHandle;
|
|
1132
|
-
const callbacks = {
|
|
1133
|
-
onOutput: (stream, data) => {
|
|
1134
|
-
if (truncated)
|
|
1135
|
-
return;
|
|
1136
|
-
if (stream !== "stdout" && stream !== "stderr")
|
|
1137
|
-
return;
|
|
1138
|
-
const remaining = input.maxOutputBytes - totalBytes;
|
|
1139
|
-
if (remaining <= 0) {
|
|
1140
|
-
truncated = true;
|
|
1141
|
-
if (driverHandle) {
|
|
1142
|
-
void driverHandle.terminate(1000);
|
|
1143
|
-
}
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
if (data.byteLength > remaining) {
|
|
1147
|
-
const slice = data.subarray(0, remaining);
|
|
1148
|
-
chunks.push({
|
|
1149
|
-
stream,
|
|
1150
|
-
data: encodeBytes(slice),
|
|
1151
|
-
});
|
|
1152
|
-
totalBytes += slice.byteLength;
|
|
1153
|
-
truncated = true;
|
|
1154
|
-
if (driverHandle) {
|
|
1155
|
-
void driverHandle.terminate(1000);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
else {
|
|
1159
|
-
chunks.push({
|
|
1160
|
-
stream,
|
|
1161
|
-
data: encodeBytes(data),
|
|
1162
|
-
});
|
|
1163
|
-
totalBytes += data.byteLength;
|
|
1164
|
-
}
|
|
1165
|
-
},
|
|
1166
|
-
onExit: (exit) => {
|
|
1167
|
-
resolveExit(exit);
|
|
1168
|
-
},
|
|
1169
|
-
onError: (err) => {
|
|
1170
|
-
rejectError(err);
|
|
1171
|
-
},
|
|
1172
|
-
};
|
|
1173
|
-
// 对齐 start 防御顺序:派生前检查取消信号,已中止直接抛出取消错误不再派生进程
|
|
1174
|
-
if (call?.signal?.aborted) {
|
|
1175
|
-
throw call.signal.reason instanceof ProcessError
|
|
1176
|
-
? call.signal.reason
|
|
1177
|
-
: new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
|
|
1178
|
-
}
|
|
1179
|
-
driverHandle = await this.driver.spawn(runProcessId, effectiveSpec, callbacks);
|
|
1180
|
-
let timeoutTimer;
|
|
1181
|
-
if (input.timeoutMs > 0) {
|
|
1182
|
-
timeoutTimer = setTimeout(() => {
|
|
1183
|
-
timedOut = true;
|
|
1184
|
-
if (driverHandle) {
|
|
1185
|
-
void driverHandle.terminate(1000);
|
|
1186
|
-
}
|
|
1187
|
-
}, input.timeoutMs);
|
|
1188
|
-
if (typeof timeoutTimer?.unref === "function") {
|
|
1189
|
-
timeoutTimer.unref();
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
let onAbort;
|
|
1193
|
-
if (call?.signal) {
|
|
1194
|
-
if (call.signal.aborted) {
|
|
1195
|
-
if (timeoutTimer)
|
|
1196
|
-
clearTimeout(timeoutTimer);
|
|
1197
|
-
if (driverHandle)
|
|
1198
|
-
void driverHandle.terminate(1000);
|
|
1199
|
-
throw call.signal.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
|
|
1200
|
-
}
|
|
1201
|
-
onAbort = () => {
|
|
1202
|
-
cancelled = true;
|
|
1203
|
-
if (timeoutTimer)
|
|
1204
|
-
clearTimeout(timeoutTimer);
|
|
1205
|
-
if (driverHandle)
|
|
1206
|
-
void driverHandle.terminate(1000);
|
|
1207
|
-
};
|
|
1208
|
-
call.signal.addEventListener("abort", onAbort, { once: true });
|
|
1209
|
-
}
|
|
1210
|
-
try {
|
|
1211
|
-
const exit = await exitPromise;
|
|
1212
|
-
if (timeoutTimer)
|
|
1213
|
-
clearTimeout(timeoutTimer);
|
|
1214
|
-
if (call?.signal && onAbort) {
|
|
1215
|
-
call.signal.removeEventListener("abort", onAbort);
|
|
1216
|
-
}
|
|
1217
|
-
if (timedOut) {
|
|
1218
|
-
throw new ProcessError(PROCESS_TIMEOUT, `Process exceeded timeout of ${input.timeoutMs}ms`);
|
|
1219
|
-
}
|
|
1220
|
-
if (cancelled || call?.signal?.aborted) {
|
|
1221
|
-
throw call?.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
|
|
1222
|
-
}
|
|
1223
|
-
return {
|
|
1224
|
-
exit,
|
|
1225
|
-
chunks,
|
|
1226
|
-
truncated,
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
catch (err) {
|
|
1230
|
-
if (timeoutTimer)
|
|
1231
|
-
clearTimeout(timeoutTimer);
|
|
1232
|
-
if (call?.signal && onAbort) {
|
|
1233
|
-
call.signal.removeEventListener("abort", onAbort);
|
|
1234
|
-
}
|
|
1235
|
-
if (timedOut) {
|
|
1236
|
-
throw new ProcessError(PROCESS_TIMEOUT, `Process exceeded timeout of ${input.timeoutMs}ms`);
|
|
1237
|
-
}
|
|
1238
|
-
if (cancelled || call?.signal?.aborted) {
|
|
1239
|
-
throw call?.signal?.reason ?? new ProcessError(PROCESS_CANCELLED, "Process run was cancelled");
|
|
1240
|
-
}
|
|
1241
|
-
throw err;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
/**
|
|
1245
|
-
* 授予指定受管进程控制令牌。
|
|
1246
|
-
*/
|
|
1247
|
-
grantControl(proc, requestId, ttlMs, runId) {
|
|
1248
|
-
proc.controlEpoch += 1;
|
|
1249
|
-
const token = `tok_${proc.info.id}_${proc.controlEpoch}_${randomUUID().replace(/-/g, "")}`;
|
|
1250
|
-
const expiresAt = new Date(Date.now() + ttlMs).toISOString();
|
|
1251
|
-
proc.info.control = "held";
|
|
1252
|
-
proc.currentGrant = {
|
|
1253
|
-
token,
|
|
1254
|
-
epoch: proc.controlEpoch,
|
|
1255
|
-
ttlMs,
|
|
1256
|
-
expiresAt,
|
|
1257
|
-
runId,
|
|
1258
|
-
requestId,
|
|
1259
|
-
};
|
|
1260
|
-
proc.ttlTimer = setTimeout(() => {
|
|
1261
|
-
this.handleGrantTtlExpired(proc);
|
|
1262
|
-
}, ttlMs);
|
|
1263
|
-
if (typeof proc.ttlTimer.unref === "function") {
|
|
1264
|
-
proc.ttlTimer.unref();
|
|
1265
|
-
}
|
|
1266
|
-
void this.persistState(proc.info.id, {
|
|
1267
|
-
control: "held",
|
|
1268
|
-
controlState: "held",
|
|
1269
|
-
});
|
|
1270
|
-
return {
|
|
1271
|
-
token,
|
|
1272
|
-
expiresAt,
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
/**
|
|
1276
|
-
* 控制权到期未释放时自动转为隔离状态。
|
|
1277
|
-
*/
|
|
1278
|
-
handleGrantTtlExpired(proc) {
|
|
1279
|
-
if (proc.info.control !== "held" || !proc.currentGrant) {
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
void this.quarantineProcess(proc.owner, proc.info.id, proc.currentGrant.token, "Control token TTL expired");
|
|
1283
|
-
}
|
|
1284
|
-
/**
|
|
1285
|
-
* 唤醒排队等待控制权的下一个调用者。
|
|
1286
|
-
*/
|
|
1287
|
-
wakeNextAcquireWaiter(proc) {
|
|
1288
|
-
if (proc.acquireWaiters.length === 0) {
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
const next = proc.acquireWaiters.shift();
|
|
1292
|
-
const grant = this.grantControl(proc, next.requestId, next.ttlMs, next.runId);
|
|
1293
|
-
const key = {
|
|
1294
|
-
hostEpoch: this.hostEpoch,
|
|
1295
|
-
scope: proc.scope,
|
|
1296
|
-
processId: proc.info.id,
|
|
1297
|
-
requestId: next.requestId,
|
|
1298
|
-
};
|
|
1299
|
-
const payloadHash = hashRequestPayload({ waitMs: next.waitMs, ttlMs: next.ttlMs });
|
|
1300
|
-
void this.persistReceipt(key, grant, payloadHash);
|
|
1301
|
-
next.resolve(grant);
|
|
1302
|
-
}
|
|
1303
|
-
/**
|
|
1304
|
-
* 串行调度执行输入队列操作。
|
|
1305
|
-
*/
|
|
1306
|
-
async dispatchNext(proc) {
|
|
1307
|
-
if (proc.isDispatching || proc.inputQueue.length === 0) {
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
proc.isDispatching = true;
|
|
1311
|
-
try {
|
|
1312
|
-
while (proc.inputQueue.length > 0) {
|
|
1313
|
-
const op = proc.inputQueue[0];
|
|
1314
|
-
// dispatch 前校验 token、epoch 与授权
|
|
1315
|
-
if (proc.info.control !== "held" ||
|
|
1316
|
-
!proc.currentGrant ||
|
|
1317
|
-
proc.currentGrant.token !== op.token ||
|
|
1318
|
-
new Date(proc.currentGrant.expiresAt).getTime() <= Date.now() ||
|
|
1319
|
-
op.cancelEpoch !== proc.cancelEpoch) {
|
|
1320
|
-
if (op.cancelEpoch === proc.cancelEpoch) {
|
|
1321
|
-
op.receipt.state = "failed";
|
|
1322
|
-
op.receipt.errorCode =
|
|
1323
|
-
proc.info.control === "quarantined" ? PROCESS_QUARANTINED : CONTROL_REVOKED;
|
|
1324
|
-
await this.persistReceipt(op.key, op.receipt, op.payloadHash);
|
|
1325
|
-
}
|
|
1326
|
-
proc.inputQueue.shift();
|
|
1327
|
-
if (op.bytes && op.cancelEpoch === proc.cancelEpoch) {
|
|
1328
|
-
proc.pendingInputBytes -= op.bytes.byteLength;
|
|
1329
|
-
}
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
op.receipt.state = "dispatching";
|
|
1333
|
-
await this.persistReceipt(op.key, op.receipt, op.payloadHash);
|
|
1334
|
-
if (op.type === "write" && op.bytes) {
|
|
1335
|
-
try {
|
|
1336
|
-
if (!proc.handle) {
|
|
1337
|
-
throw new Error("Missing driver handle");
|
|
1338
|
-
}
|
|
1339
|
-
await proc.handle.write(op.bytes);
|
|
1340
|
-
// 写入返回后校验取消纪元与队首位置:已被 stop 或 quarantine 接管则放弃过期结算
|
|
1341
|
-
if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
|
|
1342
|
-
continue;
|
|
1343
|
-
}
|
|
1344
|
-
op.receipt.state = "completed";
|
|
1345
|
-
op.receipt.acceptedBytes = op.bytes.byteLength;
|
|
1346
|
-
this.refreshIdleTimer(proc);
|
|
1347
|
-
}
|
|
1348
|
-
catch (err) {
|
|
1349
|
-
// 写入返回异常时同样校验取消纪元:被接管则放弃过期结算
|
|
1350
|
-
if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
// 若写入出现不确定失败,结果标记为 unknown,进程转入 quarantined
|
|
1354
|
-
op.receipt.state = "unknown";
|
|
1355
|
-
op.receipt.errorCode = INPUT_OUTCOME_UNKNOWN;
|
|
1356
|
-
await this.persistReceipt(op.key, op.receipt, op.payloadHash);
|
|
1357
|
-
proc.inputQueue.shift();
|
|
1358
|
-
proc.pendingInputBytes -= op.bytes.byteLength;
|
|
1359
|
-
await this.quarantineProcess(proc.owner, proc.info.id, op.token, "Write failed with uncertain outcome");
|
|
1360
|
-
break;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
else if (op.type === "control" && op.action) {
|
|
1364
|
-
try {
|
|
1365
|
-
if (!proc.handle) {
|
|
1366
|
-
throw new Error("Missing driver handle");
|
|
1367
|
-
}
|
|
1368
|
-
if (op.action.type === "input-eof") {
|
|
1369
|
-
if (proc.handle.sendInputEOF) {
|
|
1370
|
-
await proc.handle.sendInputEOF();
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
else if (op.action.type === "interrupt-foreground") {
|
|
1374
|
-
if (proc.handle.interruptForeground) {
|
|
1375
|
-
await proc.handle.interruptForeground();
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
else if (op.action.type === "resize") {
|
|
1379
|
-
if (proc.handle.resize) {
|
|
1380
|
-
await proc.handle.resize(op.action.cols, op.action.rows);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
// 控制指令返回后校验取消纪元与队首位置:已被接管则放弃过期结算
|
|
1384
|
-
if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
|
|
1385
|
-
continue;
|
|
1386
|
-
}
|
|
1387
|
-
if (op.action.type === "input-eof") {
|
|
1388
|
-
proc.inputClosed = true;
|
|
1389
|
-
await this.persistState(proc.info.id, { inputClosed: true });
|
|
1390
|
-
}
|
|
1391
|
-
op.receipt.state = "completed";
|
|
1392
|
-
this.refreshIdleTimer(proc);
|
|
1393
|
-
}
|
|
1394
|
-
catch (err) {
|
|
1395
|
-
if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
|
|
1396
|
-
continue;
|
|
1397
|
-
}
|
|
1398
|
-
op.receipt.state = "failed";
|
|
1399
|
-
op.receipt.errorCode = err instanceof ProcessError ? err.code : "CONTROL_FAILED";
|
|
1400
|
-
if (err instanceof Error && err.message) {
|
|
1401
|
-
op.receipt.errorMessage = err.message;
|
|
1402
|
-
}
|
|
1403
|
-
this.recordDiagnostic(`Control action '${op.action.type}' failed for request '${op.requestId}'`, err);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
// 结算前再次校验:若结算窗口内被 stop 或 quarantine 接管则放弃改写
|
|
1407
|
-
if (op.cancelEpoch !== proc.cancelEpoch || proc.inputQueue[0] !== op) {
|
|
1408
|
-
continue;
|
|
1409
|
-
}
|
|
1410
|
-
await this.persistReceipt(op.key, op.receipt, op.payloadHash);
|
|
1411
|
-
proc.inputQueue.shift();
|
|
1412
|
-
if (op.bytes) {
|
|
1413
|
-
proc.pendingInputBytes -= op.bytes.byteLength;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
finally {
|
|
1418
|
-
proc.isDispatching = false;
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
/**
|
|
1422
|
-
* 操作提交前校验持有者令牌与授权。
|
|
1423
|
-
*/
|
|
1424
|
-
checkOperationAuth(proc, token) {
|
|
1425
|
-
if (proc.info.control === "quarantined") {
|
|
1426
|
-
throw new ProcessError(PROCESS_QUARANTINED, "Process is in quarantined state");
|
|
1427
|
-
}
|
|
1428
|
-
if (proc.info.control !== "held" || !proc.currentGrant) {
|
|
1429
|
-
throw new ProcessError(ACCESS_DENIED, "Process control is not currently held");
|
|
1430
|
-
}
|
|
1431
|
-
if (proc.currentGrant.token !== token) {
|
|
1432
|
-
throw new ProcessError(ACCESS_DENIED, "Invalid control token");
|
|
1433
|
-
}
|
|
1434
|
-
if (new Date(proc.currentGrant.expiresAt).getTime() <= Date.now()) {
|
|
1435
|
-
throw new ProcessError(CONTROL_EXPIRED, "Control token has expired");
|
|
1436
|
-
}
|
|
929
|
+
return this.runExecutor.execute(input, call);
|
|
1437
930
|
}
|
|
1438
931
|
/**
|
|
1439
932
|
* 启动空闲超时定时器。
|
|
@@ -1533,11 +1026,11 @@ export class ProcessManager {
|
|
|
1533
1026
|
proc.info.endReason = "natural";
|
|
1534
1027
|
}
|
|
1535
1028
|
proc.currentGrant = undefined;
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
}
|
|
1029
|
+
// 终态拒绝全部排队等待者(单一入口)
|
|
1030
|
+
this.controlArbiter.revokeAllWaiters(proc, {
|
|
1031
|
+
code: CONTROL_REVOKED,
|
|
1032
|
+
message: "Process has exited",
|
|
1033
|
+
});
|
|
1541
1034
|
void this.persistState(proc.info.id, {
|
|
1542
1035
|
state: "exited",
|
|
1543
1036
|
control: "closed",
|
|
@@ -1602,10 +1095,11 @@ export class ProcessManager {
|
|
|
1602
1095
|
proc.info.endReason = "natural";
|
|
1603
1096
|
proc.outputLog.closeOutput("natural");
|
|
1604
1097
|
proc.info.outputClosed = true;
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1098
|
+
// 终态拒绝全部排队等待者(单一入口)
|
|
1099
|
+
this.controlArbiter.revokeAllWaiters(proc, {
|
|
1100
|
+
code: CONTROL_REVOKED,
|
|
1101
|
+
message: `Process error: ${err.message}`,
|
|
1102
|
+
});
|
|
1609
1103
|
void this.persistState(proc.info.id, {
|
|
1610
1104
|
state: "failed",
|
|
1611
1105
|
control: "closed",
|
|
@@ -1646,8 +1140,8 @@ export class ProcessManager {
|
|
|
1646
1140
|
throw new ProcessError(PROCESS_LOST, `Process '${processId}' belonged to a previous host epoch '${record.hostEpoch}' and has been marked lost; current epoch is '${this.hostEpoch}'`, { processId, hostEpoch: record.hostEpoch, currentHostEpoch: this.hostEpoch });
|
|
1647
1141
|
}
|
|
1648
1142
|
const info = toSdkProcessInfo(record);
|
|
1649
|
-
const retainedLog = this.
|
|
1650
|
-
const tombstone = this.
|
|
1143
|
+
const retainedLog = this.terminalOutputCache.get(processId);
|
|
1144
|
+
const tombstone = this.terminalOutputCache.getTombstone(processId);
|
|
1651
1145
|
const isTerminalLoaded = info.state === "exited" || info.state === "failed" || info.state === "lost";
|
|
1652
1146
|
const isOutputUnavailable = !retainedLog && (Boolean(tombstone) || (isTerminalLoaded && Boolean(record.outputClosed)));
|
|
1653
1147
|
const outputLog = retainedLog ??
|
|
@@ -1686,84 +1180,11 @@ export class ProcessManager {
|
|
|
1686
1180
|
this.processes.set(processId, loaded);
|
|
1687
1181
|
return loaded;
|
|
1688
1182
|
}
|
|
1689
|
-
/**
|
|
1690
|
-
* 记录已淘汰输出日志的墓碑信息(保留最近 2048 条,防止内存无限积压)。
|
|
1691
|
-
*/
|
|
1692
|
-
recordTombstone(processId, tombstone) {
|
|
1693
|
-
if (this.evictedOutputTombstones.size >= 2048) {
|
|
1694
|
-
const oldestKey = this.evictedOutputTombstones.keys().next().value;
|
|
1695
|
-
if (oldestKey) {
|
|
1696
|
-
this.evictedOutputTombstones.delete(oldestKey);
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
this.evictedOutputTombstones.set(processId, tombstone);
|
|
1700
|
-
}
|
|
1701
|
-
/**
|
|
1702
|
-
* 清理已过期的终态输出日志条目。
|
|
1703
|
-
*/
|
|
1704
|
-
cleanExpiredTerminalLogs() {
|
|
1705
|
-
const now = Date.now();
|
|
1706
|
-
for (const [id, entry] of this.evictedOutputLogs.entries()) {
|
|
1707
|
-
if (now - entry.evictedAt > this.terminalLogRetentionMs) {
|
|
1708
|
-
this.recordTombstone(id, {
|
|
1709
|
-
tailCursor: entry.log.tailCursor,
|
|
1710
|
-
earliestCursor: entry.log.earliestCursor,
|
|
1711
|
-
evictedAt: now,
|
|
1712
|
-
});
|
|
1713
|
-
this.evictedOutputLogs.delete(id);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
1183
|
/**
|
|
1718
1184
|
* 统计终态保留日志当前在内存中实际占用的输出缓冲字节总数。
|
|
1719
1185
|
*/
|
|
1720
1186
|
countRetainedOutputBufferBytes() {
|
|
1721
|
-
this.
|
|
1722
|
-
let bytes = 0;
|
|
1723
|
-
for (const entry of this.evictedOutputLogs.values()) {
|
|
1724
|
-
bytes += entry.log.currentBytes;
|
|
1725
|
-
}
|
|
1726
|
-
return bytes;
|
|
1727
|
-
}
|
|
1728
|
-
/**
|
|
1729
|
-
* 将终态输出日志存入保留缓存,并根据宿主配额执行 LRU 与 TTL 淘汰。
|
|
1730
|
-
*/
|
|
1731
|
-
retainTerminalOutputLog(processId, log) {
|
|
1732
|
-
this.cleanExpiredTerminalLogs();
|
|
1733
|
-
// 若新加入条目会导致总输出配额超限或数量超限,按 LRU 顺序淘汰最旧条目
|
|
1734
|
-
while ((this.countRetainedOutputBufferBytes() + log.currentBytes > this.quotas.maxOutputBufferBytesPerHost ||
|
|
1735
|
-
this.evictedOutputLogs.size >= 512) &&
|
|
1736
|
-
this.evictedOutputLogs.size > 0) {
|
|
1737
|
-
const oldestKey = this.evictedOutputLogs.keys().next().value;
|
|
1738
|
-
if (!oldestKey)
|
|
1739
|
-
break;
|
|
1740
|
-
const oldestEntry = this.evictedOutputLogs.get(oldestKey);
|
|
1741
|
-
if (oldestEntry) {
|
|
1742
|
-
this.recordTombstone(oldestKey, {
|
|
1743
|
-
tailCursor: oldestEntry.log.tailCursor,
|
|
1744
|
-
earliestCursor: oldestEntry.log.earliestCursor,
|
|
1745
|
-
evictedAt: Date.now(),
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
this.evictedOutputLogs.delete(oldestKey);
|
|
1749
|
-
}
|
|
1750
|
-
this.evictedOutputLogs.set(processId, {
|
|
1751
|
-
log,
|
|
1752
|
-
evictedAt: Date.now(),
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
/**
|
|
1756
|
-
* 获取终态保留日志(附带过期清理与 LRU 触达更新)。
|
|
1757
|
-
*/
|
|
1758
|
-
getRetainedOutputLog(processId) {
|
|
1759
|
-
this.cleanExpiredTerminalLogs();
|
|
1760
|
-
const entry = this.evictedOutputLogs.get(processId);
|
|
1761
|
-
if (!entry)
|
|
1762
|
-
return undefined;
|
|
1763
|
-
// 触达刷新 LRU 顺序
|
|
1764
|
-
this.evictedOutputLogs.delete(processId);
|
|
1765
|
-
this.evictedOutputLogs.set(processId, entry);
|
|
1766
|
-
return entry.log;
|
|
1187
|
+
return this.terminalOutputCache.retainedBytes();
|
|
1767
1188
|
}
|
|
1768
1189
|
/**
|
|
1769
1190
|
* 终态驱逐与驱动释放:进程到达终态且输出已关闭后移除内存记录,
|
|
@@ -1780,7 +1201,7 @@ export class ProcessManager {
|
|
|
1780
1201
|
}
|
|
1781
1202
|
this.processes.delete(proc.info.id);
|
|
1782
1203
|
// 保留输出日志支撑后续游标读取(内存输出无法从持久层恢复)
|
|
1783
|
-
this.
|
|
1204
|
+
this.terminalOutputCache.retain(proc.info.id, proc.outputLog);
|
|
1784
1205
|
if (proc.handle && typeof this.driver.dispose === "function") {
|
|
1785
1206
|
try {
|
|
1786
1207
|
const disposed = this.driver.dispose(proc.handle);
|
|
@@ -1832,23 +1253,14 @@ export class ProcessManager {
|
|
|
1832
1253
|
});
|
|
1833
1254
|
}
|
|
1834
1255
|
// 终态保留输出日志实际占用字节数纳入宿主预算
|
|
1835
|
-
|
|
1256
|
+
const retainedBefore = this.countRetainedOutputBufferBytes();
|
|
1257
|
+
totalBuffer += retainedBefore;
|
|
1836
1258
|
// 若新进程申请的缓冲使总预算超限,优先淘汰已有的终态保留日志(LRU 策略)
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const oldest = this.evictedOutputLogs.get(oldestKey);
|
|
1843
|
-
if (oldest) {
|
|
1844
|
-
this.recordTombstone(oldestKey, {
|
|
1845
|
-
tailCursor: oldest.log.tailCursor,
|
|
1846
|
-
earliestCursor: oldest.log.earliestCursor,
|
|
1847
|
-
evictedAt: Date.now(),
|
|
1848
|
-
});
|
|
1849
|
-
totalBuffer -= oldest.log.currentBytes;
|
|
1850
|
-
}
|
|
1851
|
-
this.evictedOutputLogs.delete(oldestKey);
|
|
1259
|
+
// 淘汰门槛为宿主预算扣除活跃进程已占字节与新申请字节,淘汰释放的字节数同步回扣宿主预算
|
|
1260
|
+
if (totalBuffer + perProcBuf > this.quotas.maxOutputBufferBytesPerHost) {
|
|
1261
|
+
const activeBufferBytes = totalBuffer - retainedBefore;
|
|
1262
|
+
const evictedBytes = this.terminalOutputCache.evictLRUUntil(this.quotas.maxOutputBufferBytesPerHost - perProcBuf - activeBufferBytes);
|
|
1263
|
+
totalBuffer -= evictedBytes;
|
|
1852
1264
|
}
|
|
1853
1265
|
if (totalBuffer + perProcBuf > this.quotas.maxOutputBufferBytesPerHost) {
|
|
1854
1266
|
throw new ProcessError(QUOTA_EXCEEDED, "Host output buffer quota exceeded", {
|