@xioflow/kernel 0.1.5 → 0.2.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/README.md +14 -3
- package/dist/domain.d.ts +40 -6
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +388 -24
- package/dist/domain.js.map +1 -1
- package/dist/driver/node-driver.d.ts +10 -24
- package/dist/driver/node-driver.d.ts.map +1 -1
- package/dist/driver/node-driver.js +201 -280
- package/dist/driver/node-driver.js.map +1 -1
- package/dist/driver/process-facts.d.ts +25 -0
- package/dist/driver/process-facts.d.ts.map +1 -0
- package/dist/driver/process-facts.js +164 -0
- package/dist/driver/process-facts.js.map +1 -0
- package/dist/driver/sampler.d.ts +51 -0
- package/dist/driver/sampler.d.ts.map +1 -0
- package/dist/driver/sampler.js +224 -0
- package/dist/driver/sampler.js.map +1 -0
- package/dist/driver/types.d.ts +21 -1
- package/dist/driver/types.d.ts.map +1 -1
- package/dist/recovery/engine.d.ts.map +1 -1
- package/dist/recovery/engine.js +103 -19
- package/dist/recovery/engine.js.map +1 -1
- package/dist/store/sqlite.d.ts +12 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +162 -12
- package/dist/store/sqlite.js.map +1 -1
- package/dist/supervisor/supervisor.d.ts +15 -0
- package/dist/supervisor/supervisor.d.ts.map +1 -1
- package/dist/supervisor/supervisor.js +619 -214
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +301 -11
- package/dist/testing/contract-suite.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ npm install @xioflow/kernel
|
|
|
42
42
|
import { ExecutionDomain, NodePlatformDriver, ProcessSupervisor } from '@xioflow/kernel';
|
|
43
43
|
|
|
44
44
|
// The domain directory will hold domain.db, domain.lock and artifacts/.
|
|
45
|
-
const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow', 'my-domain');
|
|
45
|
+
const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow/kernel', 'my-domain');
|
|
46
46
|
const supervisor = new ProcessSupervisor(domain, new NodePlatformDriver());
|
|
47
47
|
|
|
48
48
|
// Every operation belongs to a Run, and every Run to a Task. Register them first:
|
|
@@ -96,7 +96,7 @@ After a restart, reacquire the domain and run the recovery engine:
|
|
|
96
96
|
```js
|
|
97
97
|
import { ExecutionDomain, NodePlatformDriver, RecoveryEngine } from '@xioflow/kernel';
|
|
98
98
|
|
|
99
|
-
const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow', 'my-domain');
|
|
99
|
+
const domain = ExecutionDomain.acquire('/path/to/workspace/.xioflow/kernel', 'my-domain');
|
|
100
100
|
const report = await new RecoveryEngine(domain, new NodePlatformDriver()).recover();
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -150,10 +150,21 @@ Releases are tag-driven. A tag always produces a GitHub Release; the npm upload
|
|
|
150
150
|
|
|
151
151
|
1. Bump `version` in `package.json`, commit, and push to `main`.
|
|
152
152
|
2. Push the matching tag, e.g. `git tag v0.1.5 && git push origin v0.1.5`.
|
|
153
|
-
3. `.github/workflows/release.yml` re-runs typecheck, tests and the pack smoke test, refuses a tag that does not match `package.json`, creates the GitHub Release, and — only when the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED` is `true` — publishes with `npm publish --provenance` and verifies the attestation.
|
|
153
|
+
3. `.github/workflows/release.yml` re-runs typecheck, tests and the pack smoke test, refuses a tag that does not match `package.json`, creates the GitHub Release with the packed tarball, `SHA256SUMS` and a build provenance attestation attached, and — only when the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED` is `true` — publishes with `npm publish --provenance` and verifies the attestation.
|
|
154
154
|
|
|
155
155
|
One-time setup, in this order: (1) npmjs.com → package settings → Trusted Publisher → GitHub Actions: organization `Xio-Shark`, repository `xioflow`, workflow filename `release.yml`, environment left empty; (2) set the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED=true`. Until both exist the publish job is skipped and the reason is printed in the `verify` job's log. npm's registry index can lag several minutes behind an upload, so the verification step polls and may need a job re-run.
|
|
156
156
|
|
|
157
|
+
To attach assets to an existing tag without publishing to npm, run the workflow manually: `gh workflow run release.yml -f tag=v0.1.5`. An asset that is already attached is kept if identical and fails the run if it differs; published assets are never overwritten.
|
|
158
|
+
|
|
159
|
+
### Verifying a release
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
gh release download v0.1.5 --repo Xio-Shark/xioflow
|
|
163
|
+
shasum -a 256 -c SHA256SUMS
|
|
164
|
+
gh attestation verify xioflow-kernel-0.1.5.tgz --repo Xio-Shark/xioflow
|
|
165
|
+
npm audit signatures # inside a project that installed @xioflow/kernel from npm
|
|
166
|
+
```
|
|
167
|
+
|
|
157
168
|
## License
|
|
158
169
|
|
|
159
170
|
MIT
|
package/dist/domain.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SqliteStore } from './store/sqlite.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { PlatformDriver } from './driver/types.js';
|
|
3
|
+
import { Operation, OwnerRecord, ResourceBudget, DomainBudget, DomainLockedError, ResourceConflictError, EpochFencedError, TerminationReason, AdjudicationRecord } from './types.js';
|
|
4
|
+
export { DomainLockedError, ResourceConflictError, EpochFencedError, AdjudicationRecord };
|
|
4
5
|
export declare class ExecutionDomain {
|
|
5
6
|
readonly domainPath: string;
|
|
6
7
|
readonly domainId: string;
|
|
@@ -9,8 +10,11 @@ export declare class ExecutionDomain {
|
|
|
9
10
|
private lockFd;
|
|
10
11
|
private ownerRecord;
|
|
11
12
|
private domainBudget?;
|
|
13
|
+
private driver?;
|
|
12
14
|
private inMemoryLockedResources;
|
|
15
|
+
private inMemoryAllocatedOps;
|
|
13
16
|
private inMemoryBudgets;
|
|
17
|
+
private waitQueue;
|
|
14
18
|
private heartbeatInterval;
|
|
15
19
|
private isFenced;
|
|
16
20
|
private constructor();
|
|
@@ -34,22 +38,52 @@ export declare class ExecutionDomain {
|
|
|
34
38
|
* 刷新租约心跳,若失败则触发代际栅栏与写凭证销毁
|
|
35
39
|
*/
|
|
36
40
|
renewHeartbeat(): void;
|
|
41
|
+
private getActiveOperationIds;
|
|
42
|
+
private diagnoseConflict;
|
|
37
43
|
/**
|
|
38
44
|
* 申请占用资源(支持隔离检查与配额校验)
|
|
39
45
|
*/
|
|
40
46
|
allocateResources(operationId: string, resources: string[], waitedDurationMs?: number, budget?: ResourceBudget): void;
|
|
41
47
|
/**
|
|
42
|
-
*
|
|
48
|
+
* 具备 FIFO 排队等待与诊断的异步资源分配 (P0-5, P0-13)
|
|
43
49
|
*/
|
|
44
|
-
allocateResourcesWithWait(operationId: string, resources: string[], maxWaitMs?: number, budget?: ResourceBudget): Promise<void>;
|
|
50
|
+
allocateResourcesWithWait(operationId: string, resources: string[], maxWaitMs?: number, budget?: ResourceBudget, isCancelled?: () => boolean): Promise<void>;
|
|
45
51
|
/**
|
|
46
|
-
*
|
|
52
|
+
* 取消在等待队列中的操作 (立即唤醒以完成取消协议)
|
|
47
53
|
*/
|
|
48
|
-
|
|
54
|
+
cancelWait(operationId: string): boolean;
|
|
55
|
+
private processWaitQueue;
|
|
56
|
+
setDriver(driver: PlatformDriver): void;
|
|
57
|
+
/**
|
|
58
|
+
* 内部私有资源释放(走代际栅栏并记录 RESOURCES_RELEASED 事件,§0.2 裁决 1 / N6)
|
|
59
|
+
* @internal 仅供 supervisor、recovery engine 与 adjudicate 等内核内部组件调用
|
|
60
|
+
*/
|
|
61
|
+
internalReleaseResources(operationId: string, resources?: string[]): void;
|
|
62
|
+
/**
|
|
63
|
+
* 人工裁决协议:对 indeterminate 状态进行有审计记录的唯一收口 (ARCHITECTURE §3.6 / P0-7)
|
|
64
|
+
*/
|
|
65
|
+
adjudicate(opId: string, verdict: 'confirmed_stopped' | 'abandon_with_residuals', actor: string, note?: string): Promise<AdjudicationRecord>;
|
|
66
|
+
/**
|
|
67
|
+
* Run 完成与取消协议 (ARCHITECTURE §3.3)
|
|
68
|
+
*/
|
|
69
|
+
reportRunCancelled(runId: string, reason?: TerminationReason): void;
|
|
70
|
+
reportRunSucceeded(runId: string): void;
|
|
71
|
+
reportRunFailed(runId: string, reason?: TerminationReason): void;
|
|
49
72
|
/**
|
|
50
73
|
* 意图登记协议:先持久化意图与占用,再允许驱动调用
|
|
51
74
|
*/
|
|
52
75
|
registerOperationIntent(op: Operation): void;
|
|
76
|
+
/**
|
|
77
|
+
* 产物回收防线 (ARCHITECTURE §4.3 / N7)
|
|
78
|
+
* 只回收已终态且未被引用的产物,严格拒绝回收未终结或 indeterminate 状态的产物。
|
|
79
|
+
*/
|
|
80
|
+
pruneArtifacts(filter?: {
|
|
81
|
+
olderThanMs?: number;
|
|
82
|
+
prefix?: string;
|
|
83
|
+
}): {
|
|
84
|
+
deleted: string[];
|
|
85
|
+
retained: string[];
|
|
86
|
+
};
|
|
53
87
|
isClosed(): boolean;
|
|
54
88
|
close(): void;
|
|
55
89
|
}
|
package/dist/domain.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAEL,SAAS,EAET,WAAW,EACX,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"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"}
|
package/dist/domain.js
CHANGED
|
@@ -12,10 +12,15 @@ export class ExecutionDomain {
|
|
|
12
12
|
lockFd = null;
|
|
13
13
|
ownerRecord;
|
|
14
14
|
domainBudget;
|
|
15
|
+
driver;
|
|
15
16
|
// 内存隔离表:resourceId -> operationId
|
|
16
17
|
inMemoryLockedResources = new Map();
|
|
18
|
+
// 内存活跃操作(独立于资源租约,用于 domain:max_concurrent_ops)
|
|
19
|
+
inMemoryAllocatedOps = new Set();
|
|
17
20
|
// 内存活跃操作预算
|
|
18
21
|
inMemoryBudgets = new Map();
|
|
22
|
+
// FIFO 等待队列
|
|
23
|
+
waitQueue = [];
|
|
19
24
|
heartbeatInterval = null;
|
|
20
25
|
isFenced = false;
|
|
21
26
|
constructor(domainPath, domainId, lockFilePath, lockFd) {
|
|
@@ -42,7 +47,7 @@ export class ExecutionDomain {
|
|
|
42
47
|
this.heartbeatInterval = null;
|
|
43
48
|
}
|
|
44
49
|
try {
|
|
45
|
-
this.store.
|
|
50
|
+
this.store.fence();
|
|
46
51
|
}
|
|
47
52
|
catch { }
|
|
48
53
|
}
|
|
@@ -56,6 +61,7 @@ export class ExecutionDomain {
|
|
|
56
61
|
fs.mkdirSync(domainPath, { recursive: true });
|
|
57
62
|
}
|
|
58
63
|
const lockFilePath = path.join(domainPath, 'domain.lock');
|
|
64
|
+
const dbPath = path.join(domainPath, 'domain.db');
|
|
59
65
|
let fd = null;
|
|
60
66
|
try {
|
|
61
67
|
// 尝试独占创建锁文件
|
|
@@ -74,11 +80,24 @@ export class ExecutionDomain {
|
|
|
74
80
|
}
|
|
75
81
|
if (lockMeta) {
|
|
76
82
|
const isAlive = ExecutionDomain.checkProcessAlive(lockMeta.ownerPid);
|
|
77
|
-
|
|
83
|
+
let leaseExpired = false;
|
|
84
|
+
// 结合 owners 表判定租约是否过期 (P0-11)
|
|
85
|
+
if (fs.existsSync(dbPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const tempStore = new SqliteStore(dbPath);
|
|
88
|
+
const owner = tempStore.getOwner(domainId);
|
|
89
|
+
if (owner && new Date(owner.expiresAt).getTime() < Date.now()) {
|
|
90
|
+
leaseExpired = true;
|
|
91
|
+
}
|
|
92
|
+
tempStore.close();
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
}
|
|
96
|
+
if (isAlive && !leaseExpired) {
|
|
78
97
|
throw new DomainLockedError(lockMeta.domainId, lockMeta.ownerPid, lockMeta.acquiredAt);
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
|
-
//
|
|
100
|
+
// 旧进程已死亡或租约已过期,安全覆盖锁
|
|
82
101
|
try {
|
|
83
102
|
fs.unlinkSync(lockFilePath);
|
|
84
103
|
}
|
|
@@ -96,7 +115,22 @@ export class ExecutionDomain {
|
|
|
96
115
|
hostname: os.hostname(),
|
|
97
116
|
};
|
|
98
117
|
fs.writeSync(fd, JSON.stringify(metadata, null, 2));
|
|
99
|
-
|
|
118
|
+
try {
|
|
119
|
+
return new ExecutionDomain(domainPath, domainId, lockFilePath, fd);
|
|
120
|
+
}
|
|
121
|
+
catch (ctorErr) {
|
|
122
|
+
if (fd !== null) {
|
|
123
|
+
try {
|
|
124
|
+
fs.closeSync(fd);
|
|
125
|
+
}
|
|
126
|
+
catch { }
|
|
127
|
+
try {
|
|
128
|
+
fs.unlinkSync(lockFilePath);
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
}
|
|
132
|
+
throw ctorErr;
|
|
133
|
+
}
|
|
100
134
|
}
|
|
101
135
|
static checkProcessAlive(pid) {
|
|
102
136
|
try {
|
|
@@ -125,6 +159,7 @@ export class ExecutionDomain {
|
|
|
125
159
|
rebuildIsolationFromStore() {
|
|
126
160
|
this.inMemoryLockedResources.clear();
|
|
127
161
|
this.inMemoryBudgets.clear();
|
|
162
|
+
this.inMemoryAllocatedOps.clear();
|
|
128
163
|
// 1. 读取持久化未释放的 resource_leases
|
|
129
164
|
const leases = this.store.getPersistedResourceLeases(this.domainId);
|
|
130
165
|
for (const lease of leases) {
|
|
@@ -133,9 +168,10 @@ export class ExecutionDomain {
|
|
|
133
168
|
this.inMemoryBudgets.set(lease.operationId, lease.budget);
|
|
134
169
|
}
|
|
135
170
|
}
|
|
136
|
-
// 2.
|
|
171
|
+
// 2. 核对未终结操作声明的所有资源与活跃操作集合 (独立于 resource_leases)
|
|
137
172
|
const unfinishedOps = this.store.getUnfinishedOperations(this.domainId);
|
|
138
173
|
for (const op of unfinishedOps) {
|
|
174
|
+
this.inMemoryAllocatedOps.add(op.id);
|
|
139
175
|
for (const res of op.requiredResources) {
|
|
140
176
|
this.inMemoryLockedResources.set(res, op.id);
|
|
141
177
|
}
|
|
@@ -181,12 +217,36 @@ export class ExecutionDomain {
|
|
|
181
217
|
this.heartbeatInterval = null;
|
|
182
218
|
}
|
|
183
219
|
try {
|
|
184
|
-
this.store.
|
|
220
|
+
this.store.fence();
|
|
185
221
|
}
|
|
186
222
|
catch { }
|
|
187
223
|
throw err;
|
|
188
224
|
}
|
|
189
225
|
}
|
|
226
|
+
getActiveOperationIds() {
|
|
227
|
+
const set = new Set(this.inMemoryAllocatedOps);
|
|
228
|
+
const unfinishedOps = this.store.getUnfinishedOperations(this.domainId);
|
|
229
|
+
for (const op of unfinishedOps) {
|
|
230
|
+
set.add(op.id);
|
|
231
|
+
}
|
|
232
|
+
return set;
|
|
233
|
+
}
|
|
234
|
+
diagnoseConflict(operationId, resources, waitedDurationMs = 0, budget) {
|
|
235
|
+
for (const res of resources) {
|
|
236
|
+
const existingOwner = this.inMemoryLockedResources.get(res);
|
|
237
|
+
if (existingOwner && existingOwner !== operationId) {
|
|
238
|
+
return new ResourceConflictError(res, existingOwner, operationId, waitedDurationMs);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (this.domainBudget && this.domainBudget.maxConcurrentOps) {
|
|
242
|
+
const activeOps = this.getActiveOperationIds();
|
|
243
|
+
if (activeOps.size >= this.domainBudget.maxConcurrentOps && !activeOps.has(operationId)) {
|
|
244
|
+
const holder = Array.from(activeOps)[0] || 'domain-concurrency-cap';
|
|
245
|
+
return new ResourceConflictError('domain:max_concurrent_ops', holder, operationId, waitedDurationMs);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return new ResourceConflictError(resources[0] || 'domain:resource', 'unknown', operationId, waitedDurationMs);
|
|
249
|
+
}
|
|
190
250
|
/**
|
|
191
251
|
* 申请占用资源(支持隔离检查与配额校验)
|
|
192
252
|
*/
|
|
@@ -195,20 +255,22 @@ export class ExecutionDomain {
|
|
|
195
255
|
// 1. 检查资源冲突
|
|
196
256
|
for (const res of resources) {
|
|
197
257
|
const existingOwner = this.inMemoryLockedResources.get(res);
|
|
258
|
+
// 注(N8):现有持有人等于当前 opId 仅当同一调用内部阶段重入(如子操作或多阶段申请)时合法;
|
|
259
|
+
// 在飞 opId 的重复提交已由 supervisor 入口守卫拦截,绝不可能在此被当作合法持有人放行。
|
|
198
260
|
if (existingOwner && existingOwner !== operationId) {
|
|
199
261
|
throw new ResourceConflictError(res, existingOwner, operationId, waitedDurationMs);
|
|
200
262
|
}
|
|
201
263
|
}
|
|
202
|
-
// 2.
|
|
203
|
-
if (this.domainBudget) {
|
|
204
|
-
const activeOps =
|
|
205
|
-
if (this.domainBudget.maxConcurrentOps &&
|
|
206
|
-
activeOps.size >= this.domainBudget.maxConcurrentOps &&
|
|
264
|
+
// 2. 检查域级配额(并发数独立于 resource_leases)
|
|
265
|
+
if (this.domainBudget && this.domainBudget.maxConcurrentOps) {
|
|
266
|
+
const activeOps = this.getActiveOperationIds();
|
|
267
|
+
if (activeOps.size >= this.domainBudget.maxConcurrentOps &&
|
|
207
268
|
!activeOps.has(operationId)) {
|
|
208
269
|
const holder = Array.from(activeOps)[0] || 'domain-concurrency-cap';
|
|
209
270
|
throw new ResourceConflictError('domain:max_concurrent_ops', holder, operationId, waitedDurationMs);
|
|
210
271
|
}
|
|
211
272
|
}
|
|
273
|
+
this.inMemoryAllocatedOps.add(operationId);
|
|
212
274
|
for (const res of resources) {
|
|
213
275
|
this.inMemoryLockedResources.set(res, operationId);
|
|
214
276
|
}
|
|
@@ -217,39 +279,157 @@ export class ExecutionDomain {
|
|
|
217
279
|
}
|
|
218
280
|
}
|
|
219
281
|
/**
|
|
220
|
-
*
|
|
282
|
+
* 具备 FIFO 排队等待与诊断的异步资源分配 (P0-5, P0-13)
|
|
221
283
|
*/
|
|
222
|
-
async allocateResourcesWithWait(operationId, resources, maxWaitMs = 0, budget) {
|
|
284
|
+
async allocateResourcesWithWait(operationId, resources, maxWaitMs = 0, budget, isCancelled) {
|
|
285
|
+
this.assertNotFenced();
|
|
286
|
+
if (isCancelled && isCancelled()) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
223
289
|
const startTime = Date.now();
|
|
224
|
-
|
|
290
|
+
// 如果队列为空,尝试直接分配
|
|
291
|
+
if (this.waitQueue.length === 0) {
|
|
225
292
|
try {
|
|
226
|
-
this.allocateResources(operationId, resources,
|
|
293
|
+
this.allocateResources(operationId, resources, 0, budget);
|
|
227
294
|
return;
|
|
228
295
|
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
if (!(err instanceof ResourceConflictError)) {
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
if (maxWaitMs <= 0) {
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// 队列中已有排队者:若不愿等待则直接抛出诊断
|
|
307
|
+
if (maxWaitMs <= 0) {
|
|
308
|
+
throw this.diagnoseConflict(operationId, resources, 0, budget);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// 必须入队等待 (严格 FIFO)
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
let timer;
|
|
314
|
+
const item = {
|
|
315
|
+
operationId,
|
|
316
|
+
resources,
|
|
317
|
+
budget,
|
|
318
|
+
resolve: () => {
|
|
319
|
+
if (timer)
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
resolve();
|
|
322
|
+
},
|
|
323
|
+
reject: (err) => {
|
|
324
|
+
if (timer)
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
reject(err);
|
|
327
|
+
},
|
|
328
|
+
isCancelled,
|
|
329
|
+
startTime,
|
|
330
|
+
maxWaitMs,
|
|
331
|
+
timer: undefined,
|
|
332
|
+
};
|
|
333
|
+
timer = setTimeout(() => {
|
|
334
|
+
const idx = this.waitQueue.indexOf(item);
|
|
335
|
+
if (idx !== -1) {
|
|
336
|
+
this.waitQueue.splice(idx, 1);
|
|
337
|
+
}
|
|
338
|
+
if (item.isCancelled && item.isCancelled()) {
|
|
339
|
+
item.resolve();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const elapsed = Date.now() - item.startTime;
|
|
343
|
+
const err = this.diagnoseConflict(item.operationId, item.resources, elapsed, item.budget);
|
|
344
|
+
item.reject(err);
|
|
345
|
+
}, maxWaitMs);
|
|
346
|
+
item.timer = timer;
|
|
347
|
+
this.waitQueue.push(item);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* 取消在等待队列中的操作 (立即唤醒以完成取消协议)
|
|
352
|
+
*/
|
|
353
|
+
cancelWait(operationId) {
|
|
354
|
+
const idx = this.waitQueue.findIndex((item) => item.operationId === operationId);
|
|
355
|
+
if (idx !== -1) {
|
|
356
|
+
const item = this.waitQueue[idx];
|
|
357
|
+
this.waitQueue.splice(idx, 1);
|
|
358
|
+
if (item.timer)
|
|
359
|
+
clearTimeout(item.timer);
|
|
360
|
+
item.resolve();
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
processWaitQueue() {
|
|
366
|
+
if (this.waitQueue.length === 0 || this.isFenced) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const reservedResources = new Set();
|
|
370
|
+
let concurrencyBlocked = false;
|
|
371
|
+
let i = 0;
|
|
372
|
+
while (i < this.waitQueue.length) {
|
|
373
|
+
const item = this.waitQueue[i];
|
|
374
|
+
// 1. 检查取消
|
|
375
|
+
if (item.isCancelled && item.isCancelled()) {
|
|
376
|
+
this.waitQueue.splice(i, 1);
|
|
377
|
+
item.resolve();
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// 2. 检查是否与更早等待者的资源发生争抢
|
|
381
|
+
const hasConflictWithEarlierWaiters = item.resources.some((r) => reservedResources.has(r));
|
|
382
|
+
if (hasConflictWithEarlierWaiters || concurrencyBlocked) {
|
|
383
|
+
for (const res of item.resources) {
|
|
384
|
+
reservedResources.add(res);
|
|
385
|
+
}
|
|
386
|
+
i++;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
// 3. 尝试分配
|
|
390
|
+
try {
|
|
391
|
+
const elapsed = Date.now() - item.startTime;
|
|
392
|
+
this.allocateResources(item.operationId, item.resources, elapsed, item.budget);
|
|
393
|
+
// 分配成功:移出等待队列并唤醒
|
|
394
|
+
this.waitQueue.splice(i, 1);
|
|
395
|
+
item.resolve();
|
|
396
|
+
}
|
|
229
397
|
catch (err) {
|
|
230
398
|
if (err instanceof ResourceConflictError) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
399
|
+
for (const res of item.resources) {
|
|
400
|
+
reservedResources.add(res);
|
|
401
|
+
}
|
|
402
|
+
if (err.resourceId === 'domain:max_concurrent_ops') {
|
|
403
|
+
concurrencyBlocked = true;
|
|
234
404
|
}
|
|
235
|
-
|
|
405
|
+
i++;
|
|
236
406
|
}
|
|
237
407
|
else {
|
|
238
|
-
|
|
408
|
+
this.waitQueue.splice(i, 1);
|
|
409
|
+
item.reject(err);
|
|
239
410
|
}
|
|
240
411
|
}
|
|
241
412
|
}
|
|
242
413
|
}
|
|
414
|
+
setDriver(driver) {
|
|
415
|
+
this.driver = driver;
|
|
416
|
+
}
|
|
243
417
|
/**
|
|
244
|
-
*
|
|
418
|
+
* 内部私有资源释放(走代际栅栏并记录 RESOURCES_RELEASED 事件,§0.2 裁决 1 / N6)
|
|
419
|
+
* @internal 仅供 supervisor、recovery engine 与 adjudicate 等内核内部组件调用
|
|
245
420
|
*/
|
|
246
|
-
|
|
421
|
+
internalReleaseResources(operationId, resources) {
|
|
422
|
+
this.assertNotFenced();
|
|
423
|
+
this.store.verifyEpochFencing(this.domainId);
|
|
424
|
+
const releasedList = [];
|
|
425
|
+
this.inMemoryAllocatedOps.delete(operationId);
|
|
247
426
|
this.inMemoryBudgets.delete(operationId);
|
|
248
|
-
if (resources) {
|
|
427
|
+
if (resources && resources.length > 0) {
|
|
249
428
|
for (const res of resources) {
|
|
250
429
|
if (this.inMemoryLockedResources.get(res) === operationId) {
|
|
251
430
|
this.inMemoryLockedResources.delete(res);
|
|
252
431
|
this.store.releaseResourceLease(operationId, res);
|
|
432
|
+
releasedList.push(res);
|
|
253
433
|
}
|
|
254
434
|
}
|
|
255
435
|
}
|
|
@@ -257,10 +437,115 @@ export class ExecutionDomain {
|
|
|
257
437
|
for (const [res, owner] of Array.from(this.inMemoryLockedResources.entries())) {
|
|
258
438
|
if (owner === operationId) {
|
|
259
439
|
this.inMemoryLockedResources.delete(res);
|
|
440
|
+
releasedList.push(res);
|
|
260
441
|
}
|
|
261
442
|
}
|
|
262
443
|
this.store.releaseResourceLease(operationId);
|
|
263
444
|
}
|
|
445
|
+
this.store.recordEventAndTransitionState({
|
|
446
|
+
domainId: this.domainId,
|
|
447
|
+
operationId,
|
|
448
|
+
type: 'RESOURCES_RELEASED',
|
|
449
|
+
payload: {
|
|
450
|
+
operationId,
|
|
451
|
+
resources: releasedList,
|
|
452
|
+
},
|
|
453
|
+
timestamp: new Date().toISOString(),
|
|
454
|
+
});
|
|
455
|
+
this.processWaitQueue();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* 人工裁决协议:对 indeterminate 状态进行有审计记录的唯一收口 (ARCHITECTURE §3.6 / P0-7)
|
|
459
|
+
*/
|
|
460
|
+
async adjudicate(opId, verdict, actor, note) {
|
|
461
|
+
this.assertNotFenced();
|
|
462
|
+
this.store.verifyEpochFencing(this.domainId);
|
|
463
|
+
const op = this.store.getOperation(opId);
|
|
464
|
+
if (!op) {
|
|
465
|
+
throw new Error(`Operation "${opId}" not found`);
|
|
466
|
+
}
|
|
467
|
+
if (op.status !== 'done' || op.result?.status !== 'indeterminate') {
|
|
468
|
+
throw new Error(`Cannot adjudicate operation "${opId}": status must be 'indeterminate' (current status: '${op.status}', result: '${op.result?.status}')`);
|
|
469
|
+
}
|
|
470
|
+
// 2. 事实补全:驱动再做一次身份核验与残留扫描
|
|
471
|
+
const residualPids = [];
|
|
472
|
+
const pid = op.processIdentity?.pid;
|
|
473
|
+
const pgid = op.processIdentity?.pgid;
|
|
474
|
+
if (pid) {
|
|
475
|
+
try {
|
|
476
|
+
process.kill(pid, 0);
|
|
477
|
+
residualPids.push(pid);
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
480
|
+
}
|
|
481
|
+
if (pgid !== undefined) {
|
|
482
|
+
if (this.driver && this.driver.getGroupEvidence) {
|
|
483
|
+
try {
|
|
484
|
+
const members = await this.driver.getGroupEvidence(pgid);
|
|
485
|
+
for (const m of members) {
|
|
486
|
+
if (!residualPids.includes(m.pid)) {
|
|
487
|
+
residualPids.push(m.pid);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch { }
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
try {
|
|
495
|
+
process.kill(-pgid, 0);
|
|
496
|
+
if (pid && !residualPids.includes(pid)) {
|
|
497
|
+
residualPids.push(pid);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch { }
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (verdict === 'confirmed_stopped' && residualPids.length > 0) {
|
|
504
|
+
throw new Error(`Cannot adjudicate operation "${opId}" as 'confirmed_stopped': residual processes are still alive: [${residualPids.join(', ')}]. Use 'abandon_with_residuals' or ensure processes are terminated first.`);
|
|
505
|
+
}
|
|
506
|
+
const record = {
|
|
507
|
+
operationId: opId,
|
|
508
|
+
verdict,
|
|
509
|
+
actor,
|
|
510
|
+
note,
|
|
511
|
+
residualPids: residualPids.length > 0 ? residualPids : undefined,
|
|
512
|
+
decidedAt: new Date().toISOString(),
|
|
513
|
+
};
|
|
514
|
+
// 3. 事务提交:写入 AdjudicationRecord 与 journal 事件 OPERATION_ADJUDICATED
|
|
515
|
+
this.store.recordEventAndTransitionState({
|
|
516
|
+
domainId: this.domainId,
|
|
517
|
+
runId: op.runId,
|
|
518
|
+
operationId: opId,
|
|
519
|
+
type: 'OPERATION_ADJUDICATED',
|
|
520
|
+
payload: {
|
|
521
|
+
...record,
|
|
522
|
+
processIdentity: op.processIdentity,
|
|
523
|
+
},
|
|
524
|
+
timestamp: record.decidedAt,
|
|
525
|
+
});
|
|
526
|
+
const updatedResult = {
|
|
527
|
+
...op.result,
|
|
528
|
+
adjudication: record,
|
|
529
|
+
};
|
|
530
|
+
this.store.updateOperationResult(opId, updatedResult);
|
|
531
|
+
// 释放租约 (走内部私有释放 API,带 epoch 栅栏与 RESOURCES_RELEASED 事件)
|
|
532
|
+
this.internalReleaseResources(opId, op.requiredResources);
|
|
533
|
+
return record;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Run 完成与取消协议 (ARCHITECTURE §3.3)
|
|
537
|
+
*/
|
|
538
|
+
reportRunCancelled(runId, reason = 'user_cancelled') {
|
|
539
|
+
this.assertNotFenced();
|
|
540
|
+
this.store.reportRunCancelled(runId, reason);
|
|
541
|
+
}
|
|
542
|
+
reportRunSucceeded(runId) {
|
|
543
|
+
this.assertNotFenced();
|
|
544
|
+
this.store.reportRunSucceeded(runId);
|
|
545
|
+
}
|
|
546
|
+
reportRunFailed(runId, reason = 'completed') {
|
|
547
|
+
this.assertNotFenced();
|
|
548
|
+
this.store.reportRunFailed(runId, reason);
|
|
264
549
|
}
|
|
265
550
|
/**
|
|
266
551
|
* 意图登记协议:先持久化意图与占用,再允许驱动调用
|
|
@@ -275,14 +560,93 @@ export class ExecutionDomain {
|
|
|
275
560
|
}
|
|
276
561
|
catch (err) {
|
|
277
562
|
// 写入失败回滚内存状态
|
|
278
|
-
this.
|
|
563
|
+
this.internalReleaseResources(op.id, op.requiredResources);
|
|
279
564
|
throw err;
|
|
280
565
|
}
|
|
281
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* 产物回收防线 (ARCHITECTURE §4.3 / N7)
|
|
569
|
+
* 只回收已终态且未被引用的产物,严格拒绝回收未终结或 indeterminate 状态的产物。
|
|
570
|
+
*/
|
|
571
|
+
pruneArtifacts(filter) {
|
|
572
|
+
this.assertNotFenced();
|
|
573
|
+
const artifactsDir = path.join(this.domainPath, 'artifacts');
|
|
574
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
575
|
+
return { deleted: [], retained: [] };
|
|
576
|
+
}
|
|
577
|
+
const protectedFiles = new Set();
|
|
578
|
+
const allOps = this.store.getAllOperations(this.domainId);
|
|
579
|
+
for (const op of allOps) {
|
|
580
|
+
const isIndet = op.result?.status === 'indeterminate' || op.result?.kind === 'indeterminate';
|
|
581
|
+
const isDone = op.status === 'done';
|
|
582
|
+
if (!isDone || isIndet) {
|
|
583
|
+
// 未终结或 indeterminate:保护所有可能关联的产物
|
|
584
|
+
protectedFiles.add(`${op.id}-stdout.log`);
|
|
585
|
+
protectedFiles.add(`${op.id}-stderr.log`);
|
|
586
|
+
if (op.outputRef)
|
|
587
|
+
protectedFiles.add(path.basename(op.outputRef));
|
|
588
|
+
if (op.result?.stdoutRef)
|
|
589
|
+
protectedFiles.add(path.basename(op.result.stdoutRef));
|
|
590
|
+
if (op.result?.stderrRef)
|
|
591
|
+
protectedFiles.add(path.basename(op.result.stderrRef));
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
// 已终态:仅保护仍被引用的产物
|
|
595
|
+
if (op.outputRef)
|
|
596
|
+
protectedFiles.add(path.basename(op.outputRef));
|
|
597
|
+
if (op.result?.stdoutRef)
|
|
598
|
+
protectedFiles.add(path.basename(op.result.stdoutRef));
|
|
599
|
+
if (op.result?.stderrRef)
|
|
600
|
+
protectedFiles.add(path.basename(op.result.stderrRef));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const files = fs.readdirSync(artifactsDir);
|
|
604
|
+
const deleted = [];
|
|
605
|
+
const retained = [];
|
|
606
|
+
const now = Date.now();
|
|
607
|
+
for (const file of files) {
|
|
608
|
+
const filePath = path.join(artifactsDir, file);
|
|
609
|
+
if (protectedFiles.has(file)) {
|
|
610
|
+
retained.push(filePath);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (filter?.prefix && !file.startsWith(filter.prefix)) {
|
|
614
|
+
retained.push(filePath);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (filter?.olderThanMs !== undefined) {
|
|
618
|
+
try {
|
|
619
|
+
const stat = fs.statSync(filePath);
|
|
620
|
+
if (now - stat.mtimeMs < filter.olderThanMs) {
|
|
621
|
+
retained.push(filePath);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
retained.push(filePath);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
fs.unlinkSync(filePath);
|
|
632
|
+
deleted.push(filePath);
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
retained.push(filePath);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return { deleted, retained };
|
|
639
|
+
}
|
|
282
640
|
isClosed() {
|
|
283
641
|
return this.lockFd === null;
|
|
284
642
|
}
|
|
285
643
|
close() {
|
|
644
|
+
for (const item of this.waitQueue) {
|
|
645
|
+
if (item.timer)
|
|
646
|
+
clearTimeout(item.timer);
|
|
647
|
+
item.reject(new Error(`Domain ${this.domainId} closed while waiting for resources`));
|
|
648
|
+
}
|
|
649
|
+
this.waitQueue = [];
|
|
286
650
|
if (this.heartbeatInterval) {
|
|
287
651
|
clearInterval(this.heartbeatInterval);
|
|
288
652
|
this.heartbeatInterval = null;
|