@elaraai/e3-core 0.0.2-beta.4 → 0.0.2-beta.40
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 +25 -22
- package/dist/src/dataflow/api-compat.d.ts +90 -0
- package/dist/src/dataflow/api-compat.d.ts.map +1 -0
- package/dist/src/dataflow/api-compat.js +139 -0
- package/dist/src/dataflow/api-compat.js.map +1 -0
- package/dist/src/dataflow/index.d.ts +18 -0
- package/dist/src/dataflow/index.d.ts.map +1 -0
- package/dist/src/dataflow/index.js +23 -0
- package/dist/src/dataflow/index.js.map +1 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +76 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +695 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -0
- package/dist/src/dataflow/orchestrator/index.d.ts +12 -0
- package/dist/src/dataflow/orchestrator/index.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/index.js +12 -0
- package/dist/src/dataflow/orchestrator/index.js.map +1 -0
- package/dist/src/dataflow/orchestrator/interfaces.d.ts +163 -0
- package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/interfaces.js +52 -0
- package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -0
- package/dist/src/dataflow/state-store/FileStateStore.d.ts +67 -0
- package/dist/src/dataflow/state-store/FileStateStore.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/FileStateStore.js +300 -0
- package/dist/src/dataflow/state-store/FileStateStore.js.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts +42 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.js +229 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.js.map +1 -0
- package/dist/src/dataflow/state-store/index.d.ts +13 -0
- package/dist/src/dataflow/state-store/index.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/index.js +13 -0
- package/dist/src/dataflow/state-store/index.js.map +1 -0
- package/dist/src/dataflow/state-store/interfaces.d.ts +159 -0
- package/dist/src/dataflow/state-store/interfaces.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/interfaces.js +6 -0
- package/dist/src/dataflow/state-store/interfaces.js.map +1 -0
- package/dist/src/dataflow/steps.d.ts +222 -0
- package/dist/src/dataflow/steps.d.ts.map +1 -0
- package/dist/src/dataflow/steps.js +707 -0
- package/dist/src/dataflow/steps.js.map +1 -0
- package/dist/src/dataflow/types.d.ts +127 -0
- package/dist/src/dataflow/types.d.ts.map +1 -0
- package/dist/src/dataflow/types.js +7 -0
- package/dist/src/dataflow/types.js.map +1 -0
- package/dist/src/dataflow.d.ts +113 -38
- package/dist/src/dataflow.d.ts.map +1 -1
- package/dist/src/dataflow.js +269 -416
- package/dist/src/dataflow.js.map +1 -1
- package/dist/src/dataset-refs.d.ts +124 -0
- package/dist/src/dataset-refs.d.ts.map +1 -0
- package/dist/src/dataset-refs.js +319 -0
- package/dist/src/dataset-refs.js.map +1 -0
- package/dist/src/errors.d.ts +39 -9
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +51 -8
- package/dist/src/errors.js.map +1 -1
- package/dist/src/execution/LocalTaskRunner.d.ts +73 -0
- package/dist/src/execution/LocalTaskRunner.d.ts.map +1 -0
- package/dist/src/execution/LocalTaskRunner.js +399 -0
- package/dist/src/execution/LocalTaskRunner.js.map +1 -0
- package/dist/src/execution/MockTaskRunner.d.ts +49 -0
- package/dist/src/execution/MockTaskRunner.d.ts.map +1 -0
- package/dist/src/execution/MockTaskRunner.js +54 -0
- package/dist/src/execution/MockTaskRunner.js.map +1 -0
- package/dist/src/execution/index.d.ts +16 -0
- package/dist/src/execution/index.d.ts.map +1 -0
- package/dist/src/execution/index.js +8 -0
- package/dist/src/execution/index.js.map +1 -0
- package/dist/src/execution/interfaces.d.ts +246 -0
- package/dist/src/execution/interfaces.d.ts.map +1 -0
- package/dist/src/execution/interfaces.js +6 -0
- package/dist/src/execution/interfaces.js.map +1 -0
- package/dist/src/execution/processHelpers.d.ts +20 -0
- package/dist/src/execution/processHelpers.d.ts.map +1 -0
- package/dist/src/execution/processHelpers.js +62 -0
- package/dist/src/execution/processHelpers.js.map +1 -0
- package/dist/src/executions.d.ts +71 -104
- package/dist/src/executions.d.ts.map +1 -1
- package/dist/src/executions.js +110 -476
- package/dist/src/executions.js.map +1 -1
- package/dist/src/index.d.ts +19 -9
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +48 -18
- package/dist/src/index.js.map +1 -1
- package/dist/src/objects.d.ts +8 -51
- package/dist/src/objects.d.ts.map +1 -1
- package/dist/src/objects.js +13 -230
- package/dist/src/objects.js.map +1 -1
- package/dist/src/packages.d.ts +22 -14
- package/dist/src/packages.d.ts.map +1 -1
- package/dist/src/packages.js +134 -88
- package/dist/src/packages.js.map +1 -1
- package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts +35 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.js +107 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.js.map +1 -0
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts +139 -0
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryStorage.js +439 -0
- package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -0
- package/dist/src/storage/in-memory/index.d.ts +12 -0
- package/dist/src/storage/in-memory/index.d.ts.map +1 -0
- package/dist/src/storage/in-memory/index.js +12 -0
- package/dist/src/storage/in-memory/index.js.map +1 -0
- package/dist/src/storage/index.d.ts +18 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +10 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/interfaces.d.ts +581 -0
- package/dist/src/storage/interfaces.d.ts.map +1 -0
- package/dist/src/storage/interfaces.js +6 -0
- package/dist/src/storage/interfaces.js.map +1 -0
- package/dist/src/storage/local/LocalBackend.d.ts +56 -0
- package/dist/src/storage/local/LocalBackend.d.ts.map +1 -0
- package/dist/src/storage/local/LocalBackend.js +145 -0
- package/dist/src/storage/local/LocalBackend.js.map +1 -0
- package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
- package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
- package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
- package/dist/src/storage/local/LocalLockService.d.ts +111 -0
- package/dist/src/storage/local/LocalLockService.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLockService.js +355 -0
- package/dist/src/storage/local/LocalLockService.js.map +1 -0
- package/dist/src/storage/local/LocalLogStore.d.ts +23 -0
- package/dist/src/storage/local/LocalLogStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLogStore.js +66 -0
- package/dist/src/storage/local/LocalLogStore.js.map +1 -0
- package/dist/src/storage/local/LocalObjectStore.d.ts +55 -0
- package/dist/src/storage/local/LocalObjectStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalObjectStore.js +300 -0
- package/dist/src/storage/local/LocalObjectStore.js.map +1 -0
- package/dist/src/storage/local/LocalRefStore.d.ts +50 -0
- package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRefStore.js +337 -0
- package/dist/src/storage/local/LocalRefStore.js.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.d.ts +55 -0
- package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.js +365 -0
- package/dist/src/storage/local/LocalRepoStore.js.map +1 -0
- package/dist/src/storage/local/gc.d.ts +92 -0
- package/dist/src/storage/local/gc.d.ts.map +1 -0
- package/dist/src/storage/local/gc.js +377 -0
- package/dist/src/storage/local/gc.js.map +1 -0
- package/dist/src/storage/local/index.d.ts +18 -0
- package/dist/src/storage/local/index.d.ts.map +1 -0
- package/dist/src/storage/local/index.js +18 -0
- package/dist/src/storage/local/index.js.map +1 -0
- package/dist/src/storage/local/localHelpers.d.ts +25 -0
- package/dist/src/storage/local/localHelpers.d.ts.map +1 -0
- package/dist/src/storage/local/localHelpers.js +69 -0
- package/dist/src/storage/local/localHelpers.js.map +1 -0
- package/dist/src/{repository.d.ts → storage/local/repository.d.ts} +8 -4
- package/dist/src/storage/local/repository.d.ts.map +1 -0
- package/dist/src/{repository.js → storage/local/repository.js} +31 -29
- package/dist/src/storage/local/repository.js.map +1 -0
- package/dist/src/tasks.d.ts +16 -10
- package/dist/src/tasks.d.ts.map +1 -1
- package/dist/src/tasks.js +35 -41
- package/dist/src/tasks.js.map +1 -1
- package/dist/src/test-helpers.d.ts +5 -4
- package/dist/src/test-helpers.d.ts.map +1 -1
- package/dist/src/test-helpers.js +9 -21
- package/dist/src/test-helpers.js.map +1 -1
- package/dist/src/transfer/InMemoryTransferBackend.d.ts +66 -0
- package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
- package/dist/src/transfer/InMemoryTransferBackend.js +166 -0
- package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
- package/dist/src/transfer/index.d.ts +8 -0
- package/dist/src/transfer/index.d.ts.map +1 -0
- package/dist/src/transfer/index.js +9 -0
- package/dist/src/transfer/index.js.map +1 -0
- package/dist/src/transfer/interfaces.d.ts +103 -0
- package/dist/src/transfer/interfaces.d.ts.map +1 -0
- package/dist/src/transfer/interfaces.js +6 -0
- package/dist/src/transfer/interfaces.js.map +1 -0
- package/dist/src/transfer/types.d.ts +79 -0
- package/dist/src/transfer/types.d.ts.map +1 -0
- package/dist/src/transfer/types.js +58 -0
- package/dist/src/transfer/types.js.map +1 -0
- package/dist/src/trees.d.ts +147 -59
- package/dist/src/trees.d.ts.map +1 -1
- package/dist/src/trees.js +372 -419
- package/dist/src/trees.js.map +1 -1
- package/dist/src/uuid.d.ts +26 -0
- package/dist/src/uuid.d.ts.map +1 -0
- package/dist/src/uuid.js +80 -0
- package/dist/src/uuid.js.map +1 -0
- package/dist/src/workspaceStatus.d.ts +6 -4
- package/dist/src/workspaceStatus.d.ts.map +1 -1
- package/dist/src/workspaceStatus.js +43 -49
- package/dist/src/workspaceStatus.js.map +1 -1
- package/dist/src/workspaces.d.ts +35 -47
- package/dist/src/workspaces.d.ts.map +1 -1
- package/dist/src/workspaces.js +194 -156
- package/dist/src/workspaces.js.map +1 -1
- package/package.json +4 -4
- package/dist/src/gc.d.ts +0 -54
- package/dist/src/gc.d.ts.map +0 -1
- package/dist/src/gc.js +0 -233
- package/dist/src/gc.js.map +0 -1
- package/dist/src/repository.d.ts.map +0 -1
- package/dist/src/repository.js.map +0 -1
- package/dist/src/workspaceLock.d.ts +0 -67
- package/dist/src/workspaceLock.d.ts.map +0 -1
- package/dist/src/workspaceLock.js +0 -217
- package/dist/src/workspaceLock.js.map +0 -1
package/dist/src/executions.js
CHANGED
|
@@ -3,23 +3,18 @@
|
|
|
3
3
|
* Licensed under BSL 1.1. See LICENSE for details.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Generic task execution APIs for e3 repositories.
|
|
7
7
|
*
|
|
8
|
-
* Provides APIs for:
|
|
8
|
+
* Provides storage-agnostic APIs for:
|
|
9
9
|
* - Computing execution identity (inputsHash)
|
|
10
10
|
* - Querying execution status and results
|
|
11
11
|
* - Reading execution logs
|
|
12
|
-
* -
|
|
12
|
+
* - Evaluating command IR
|
|
13
|
+
*
|
|
14
|
+
* Note: Local process execution is in execution/LocalTaskRunner.ts
|
|
13
15
|
*/
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import { spawn } from 'child_process';
|
|
17
|
-
import { createWriteStream } from 'fs';
|
|
18
|
-
import { tmpdir } from 'os';
|
|
19
|
-
import { decodeBeast2For, encodeBeast2For, variant, EastIR, IRType } from '@elaraai/east';
|
|
20
|
-
import { ExecutionStatusType, TaskObjectType, } from '@elaraai/e3-types';
|
|
21
|
-
import { computeHash, objectRead, objectWrite } from './objects.js';
|
|
22
|
-
import { ExecutionCorruptError, isNotFoundError } from './errors.js';
|
|
16
|
+
import { decodeBeast2For, EastIR, IRType } from '@elaraai/east';
|
|
17
|
+
import { computeHash } from './objects.js';
|
|
23
18
|
// ============================================================================
|
|
24
19
|
// Execution Identity
|
|
25
20
|
// ============================================================================
|
|
@@ -36,166 +31,149 @@ export function inputsHash(inputHashes) {
|
|
|
36
31
|
const data = inputHashes.join('\0');
|
|
37
32
|
return computeHash(new TextEncoder().encode(data));
|
|
38
33
|
}
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Execution Status
|
|
36
|
+
// ============================================================================
|
|
39
37
|
/**
|
|
40
|
-
* Get
|
|
38
|
+
* Get execution status for a specific execution.
|
|
41
39
|
*
|
|
42
|
-
* @param
|
|
40
|
+
* @param storage - Storage backend
|
|
41
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
43
42
|
* @param taskHash - Hash of the task object
|
|
44
43
|
* @param inHash - Combined hash of input hashes
|
|
45
|
-
* @
|
|
44
|
+
* @param executionId - Execution ID (UUIDv7)
|
|
45
|
+
* @returns ExecutionStatus or null if execution doesn't exist
|
|
46
|
+
* @throws {ExecutionCorruptError} If status file exists but cannot be decoded
|
|
46
47
|
*/
|
|
47
|
-
export function
|
|
48
|
-
return
|
|
48
|
+
export async function executionGet(storage, repo, taskHash, inHash, executionId) {
|
|
49
|
+
return storage.refs.executionGet(repo, taskHash, inHash, executionId);
|
|
49
50
|
}
|
|
50
|
-
// ============================================================================
|
|
51
|
-
// Execution Status
|
|
52
|
-
// ============================================================================
|
|
53
51
|
/**
|
|
54
|
-
* Get execution status.
|
|
52
|
+
* Get the latest execution status (lexicographically greatest executionId).
|
|
55
53
|
*
|
|
56
|
-
* @param
|
|
54
|
+
* @param storage - Storage backend
|
|
55
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
57
56
|
* @param taskHash - Hash of the task object
|
|
58
57
|
* @param inHash - Combined hash of input hashes
|
|
59
|
-
* @returns ExecutionStatus or null if
|
|
60
|
-
* @throws {ExecutionCorruptError} If status file exists but cannot be decoded
|
|
58
|
+
* @returns ExecutionStatus or null if no executions exist
|
|
61
59
|
*/
|
|
62
|
-
export async function
|
|
63
|
-
|
|
64
|
-
const statusPath = path.join(execDir, 'status.beast2');
|
|
65
|
-
let data;
|
|
66
|
-
try {
|
|
67
|
-
data = await fs.readFile(statusPath);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
if (isNotFoundError(err)) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
throw err;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
const decoder = decodeBeast2For(ExecutionStatusType);
|
|
77
|
-
return decoder(data);
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
throw new ExecutionCorruptError(taskHash, inHash, err instanceof Error ? err : new Error(String(err)));
|
|
81
|
-
}
|
|
60
|
+
export async function executionGetLatest(storage, repo, taskHash, inHash) {
|
|
61
|
+
return storage.refs.executionGetLatest(repo, taskHash, inHash);
|
|
82
62
|
}
|
|
83
63
|
/**
|
|
84
|
-
* Get output hash for a completed execution.
|
|
64
|
+
* Get the latest successful output hash for a completed execution.
|
|
65
|
+
* This is the primary cache lookup function.
|
|
85
66
|
*
|
|
86
|
-
* @param
|
|
67
|
+
* @param storage - Storage backend
|
|
68
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
87
69
|
* @param taskHash - Hash of the task object
|
|
88
70
|
* @param inHash - Combined hash of input hashes
|
|
89
|
-
* @returns Output hash or null if
|
|
71
|
+
* @returns Output hash or null if no successful execution exists
|
|
90
72
|
*/
|
|
91
|
-
export async function executionGetOutput(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
73
|
+
export async function executionGetOutput(storage, repo, taskHash, inHash) {
|
|
74
|
+
return storage.refs.executionGetLatestOutput(repo, taskHash, inHash);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* List all execution IDs for a (taskHash, inputsHash) pair.
|
|
78
|
+
*
|
|
79
|
+
* @param storage - Storage backend
|
|
80
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
81
|
+
* @param taskHash - Hash of the task object
|
|
82
|
+
* @param inHash - Combined hash of input hashes
|
|
83
|
+
* @returns Array of execution IDs (sorted lexicographically ascending)
|
|
84
|
+
*/
|
|
85
|
+
export async function executionListIds(storage, repo, taskHash, inHash) {
|
|
86
|
+
return storage.refs.executionListIds(repo, taskHash, inHash);
|
|
104
87
|
}
|
|
105
88
|
/**
|
|
106
89
|
* List all input hashes that have executions for a given task.
|
|
107
90
|
*
|
|
108
|
-
* @param
|
|
91
|
+
* @param storage - Storage backend
|
|
92
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
109
93
|
* @param taskHash - Hash of the task object
|
|
110
94
|
* @returns Array of input hashes
|
|
111
95
|
*/
|
|
112
|
-
export async function executionListForTask(
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const entries = await fs.readdir(taskDir);
|
|
116
|
-
// Filter to only valid hash directories (64 hex chars)
|
|
117
|
-
return entries.filter((e) => /^[a-f0-9]{64}$/.test(e));
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
96
|
+
export async function executionListForTask(storage, repo, taskHash) {
|
|
97
|
+
return storage.refs.executionListForTask(repo, taskHash);
|
|
122
98
|
}
|
|
123
99
|
/**
|
|
124
100
|
* List all executions in the repository.
|
|
125
101
|
*
|
|
126
|
-
* @param
|
|
102
|
+
* @param storage - Storage backend
|
|
103
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
127
104
|
* @returns Array of { taskHash, inputsHash } objects
|
|
128
105
|
*/
|
|
129
|
-
export async function executionList(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
106
|
+
export async function executionList(storage, repo) {
|
|
107
|
+
return storage.refs.executionList(repo);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Find the execution reference for a task in a workspace.
|
|
111
|
+
*
|
|
112
|
+
* This looks up the task's current input hashes from the workspace state
|
|
113
|
+
* and finds the matching execution. If no execution exists for the current
|
|
114
|
+
* inputs, falls back to the most recent execution.
|
|
115
|
+
*
|
|
116
|
+
* @param storage - Storage backend
|
|
117
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
118
|
+
* @param ws - Workspace name
|
|
119
|
+
* @param taskName - Task name
|
|
120
|
+
* @returns Execution reference or null if no executions exist
|
|
121
|
+
*/
|
|
122
|
+
export async function executionFindCurrent(storage, repo, ws, taskName) {
|
|
123
|
+
// Import here to avoid circular dependency
|
|
124
|
+
const { workspaceGetTaskHash, workspaceGetTask } = await import('./tasks.js');
|
|
125
|
+
const { workspaceGetDatasetHash } = await import('./trees.js');
|
|
126
|
+
const taskHash = await workspaceGetTaskHash(storage, repo, ws, taskName);
|
|
127
|
+
const task = await workspaceGetTask(storage, repo, ws, taskName);
|
|
128
|
+
// Get the current input hashes from the workspace
|
|
129
|
+
const currentInputHashes = [];
|
|
130
|
+
let allInputsAssigned = true;
|
|
131
|
+
for (const inputPath of task.inputs) {
|
|
132
|
+
const { refType, hash } = await workspaceGetDatasetHash(storage, repo, ws, inputPath);
|
|
133
|
+
if (refType !== 'value' || hash === null) {
|
|
134
|
+
allInputsAssigned = false;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
currentInputHashes.push(hash);
|
|
138
|
+
}
|
|
139
|
+
const executions = await executionListForTask(storage, repo, taskHash);
|
|
140
|
+
if (allInputsAssigned) {
|
|
141
|
+
const inHash = inputsHash(currentInputHashes);
|
|
142
|
+
if (executions.includes(inHash)) {
|
|
143
|
+
// Get the latest execution status to get the executionId
|
|
144
|
+
const status = await storage.refs.executionGetLatest(repo, taskHash, inHash);
|
|
145
|
+
if (status) {
|
|
146
|
+
// Extract executionId from the status (all variants have it)
|
|
147
|
+
const executionId = status.value.executionId;
|
|
148
|
+
return { taskHash, inputsHash: inHash, executionId, isCurrent: true };
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
// Fall back to most recent execution
|
|
153
|
+
if (executions.length > 0) {
|
|
154
|
+
const inHash = executions[0];
|
|
155
|
+
const status = await storage.refs.executionGetLatest(repo, taskHash, inHash);
|
|
156
|
+
if (status) {
|
|
157
|
+
const executionId = status.value.executionId;
|
|
158
|
+
return { taskHash, inputsHash: inHash, executionId, isCurrent: false };
|
|
159
|
+
}
|
|
151
160
|
}
|
|
152
|
-
return
|
|
161
|
+
return null;
|
|
153
162
|
}
|
|
154
163
|
/**
|
|
155
164
|
* Read execution logs with pagination support.
|
|
156
165
|
*
|
|
157
|
-
* @param
|
|
166
|
+
* @param storage - Storage backend
|
|
167
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
158
168
|
* @param taskHash - Hash of the task object
|
|
159
169
|
* @param inHash - Combined hash of input hashes
|
|
170
|
+
* @param executionId - Execution ID (UUIDv7)
|
|
160
171
|
* @param stream - Which log stream to read ('stdout' or 'stderr')
|
|
161
172
|
* @param options - Pagination options
|
|
162
173
|
* @returns Log chunk with data and metadata
|
|
163
174
|
*/
|
|
164
|
-
export async function executionReadLog(
|
|
165
|
-
|
|
166
|
-
const logPath = path.join(execDir, `${stream}.txt`);
|
|
167
|
-
const offset = options.offset ?? 0;
|
|
168
|
-
const limit = options.limit ?? 65536; // 64KB default
|
|
169
|
-
try {
|
|
170
|
-
const stat = await fs.stat(logPath);
|
|
171
|
-
const totalSize = stat.size;
|
|
172
|
-
// Open file and read chunk
|
|
173
|
-
const fd = await fs.open(logPath, 'r');
|
|
174
|
-
try {
|
|
175
|
-
const buffer = Buffer.alloc(Math.min(limit, totalSize - offset));
|
|
176
|
-
const { bytesRead } = await fd.read(buffer, 0, buffer.length, offset);
|
|
177
|
-
return {
|
|
178
|
-
data: buffer.slice(0, bytesRead).toString('utf-8'),
|
|
179
|
-
offset,
|
|
180
|
-
size: bytesRead,
|
|
181
|
-
totalSize,
|
|
182
|
-
complete: offset + bytesRead >= totalSize,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
finally {
|
|
186
|
-
await fd.close();
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
// Log file doesn't exist yet
|
|
191
|
-
return {
|
|
192
|
-
data: '',
|
|
193
|
-
offset: 0,
|
|
194
|
-
size: 0,
|
|
195
|
-
totalSize: 0,
|
|
196
|
-
complete: true,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
175
|
+
export async function executionReadLog(storage, repo, taskHash, inHash, executionId, stream, options = {}) {
|
|
176
|
+
return storage.logs.read(repo, taskHash, inHash, executionId, stream, options);
|
|
199
177
|
}
|
|
200
178
|
// ============================================================================
|
|
201
179
|
// Command IR Evaluation
|
|
@@ -205,14 +183,15 @@ export async function executionReadLog(repoPath, taskHash, inHash, stream, optio
|
|
|
205
183
|
*
|
|
206
184
|
* The IR is an East function: (inputs: Array<String>, output: String) -> Array<String>
|
|
207
185
|
*
|
|
208
|
-
* @param
|
|
186
|
+
* @param storage - Storage backend
|
|
187
|
+
* @param repo - Repository identifier (for local storage, the path to e3 repository directory)
|
|
209
188
|
* @param commandIrHash - Hash of the IR object
|
|
210
189
|
* @param inputPaths - Paths to staged input files
|
|
211
190
|
* @param outputPath - Path where output should be written
|
|
212
191
|
* @returns Array of strings to exec
|
|
213
192
|
*/
|
|
214
|
-
export async function evaluateCommandIr(
|
|
215
|
-
const irData = await
|
|
193
|
+
export async function evaluateCommandIr(storage, repo, commandIrHash, inputPaths, outputPath) {
|
|
194
|
+
const irData = await storage.objects.read(repo, commandIrHash);
|
|
216
195
|
try {
|
|
217
196
|
// Decode the IR from beast2 format
|
|
218
197
|
const decoder = decodeBeast2For(IRType);
|
|
@@ -237,349 +216,4 @@ export async function evaluateCommandIr(repoPath, commandIrHash, inputPaths, out
|
|
|
237
216
|
throw new Error(`Failed to evaluate command IR: ${err}`);
|
|
238
217
|
}
|
|
239
218
|
}
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// Process Identification (for crash detection)
|
|
242
|
-
// ============================================================================
|
|
243
|
-
/**
|
|
244
|
-
* Get the current system boot ID.
|
|
245
|
-
* Used for detecting stale locks/processes after system reboot.
|
|
246
|
-
*/
|
|
247
|
-
export async function getBootId() {
|
|
248
|
-
try {
|
|
249
|
-
const data = await fs.readFile('/proc/sys/kernel/random/boot_id', 'utf-8');
|
|
250
|
-
return data.trim();
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
// Not on Linux, use a placeholder
|
|
254
|
-
return 'unknown-boot-id';
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Get process start time from /proc/<pid>/stat.
|
|
259
|
-
* Returns the starttime field (field 22) which is jiffies since boot.
|
|
260
|
-
* Used together with boot ID to uniquely identify a process (handles PID reuse).
|
|
261
|
-
*/
|
|
262
|
-
export async function getPidStartTime(pid) {
|
|
263
|
-
try {
|
|
264
|
-
const data = await fs.readFile(`/proc/${pid}/stat`, 'utf-8');
|
|
265
|
-
// Fields are space-separated, but comm (field 2) can contain spaces and is in parens
|
|
266
|
-
// Find the closing paren, then split the rest
|
|
267
|
-
const closeParen = data.lastIndexOf(')');
|
|
268
|
-
const fields = data.slice(closeParen + 2).split(' ');
|
|
269
|
-
// After the closing paren, field index 0 is state (field 3), so starttime is at index 19
|
|
270
|
-
// (field 22 - 3 = 19)
|
|
271
|
-
return parseInt(fields[19], 10);
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
return 0;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Check if a process is still alive based on stored identification
|
|
279
|
-
*/
|
|
280
|
-
export async function isProcessAlive(pid, pidStartTime, bootId) {
|
|
281
|
-
// Different boot? Process is dead
|
|
282
|
-
const currentBootId = await getBootId();
|
|
283
|
-
if (currentBootId !== bootId)
|
|
284
|
-
return false;
|
|
285
|
-
// Check if PID exists and has same start time
|
|
286
|
-
const currentStartTime = await getPidStartTime(pid);
|
|
287
|
-
if (currentStartTime === 0)
|
|
288
|
-
return false; // PID doesn't exist
|
|
289
|
-
if (currentStartTime !== pidStartTime)
|
|
290
|
-
return false; // PID reused
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Execute a single task.
|
|
295
|
-
*
|
|
296
|
-
* This is the core execution primitive. It:
|
|
297
|
-
* 1. Computes the execution identity from task + inputs
|
|
298
|
-
* 2. Checks cache (unless force=true)
|
|
299
|
-
* 3. Marshals inputs to a scratch directory
|
|
300
|
-
* 4. Evaluates command IR to get exec args
|
|
301
|
-
* 5. Runs the command
|
|
302
|
-
* 6. Stores the output and updates status
|
|
303
|
-
*
|
|
304
|
-
* @param repoPath - Path to .e3 repository
|
|
305
|
-
* @param taskHash - Hash of the task object
|
|
306
|
-
* @param inputHashes - Array of input dataset hashes
|
|
307
|
-
* @param options - Execution options
|
|
308
|
-
* @returns Execution result
|
|
309
|
-
*/
|
|
310
|
-
export async function taskExecute(repoPath, taskHash, inputHashes, options = {}) {
|
|
311
|
-
const inHash = inputsHash(inputHashes);
|
|
312
|
-
const execDir = executionPath(repoPath, taskHash, inHash);
|
|
313
|
-
const startTime = Date.now();
|
|
314
|
-
// Step 1: Check cache (unless force)
|
|
315
|
-
if (!options.force) {
|
|
316
|
-
const existingOutput = await executionGetOutput(repoPath, taskHash, inHash);
|
|
317
|
-
if (existingOutput !== null) {
|
|
318
|
-
const status = await executionGet(repoPath, taskHash, inHash);
|
|
319
|
-
if (status && status.type === 'success') {
|
|
320
|
-
return {
|
|
321
|
-
inputsHash: inHash,
|
|
322
|
-
cached: true,
|
|
323
|
-
state: 'success',
|
|
324
|
-
outputHash: existingOutput,
|
|
325
|
-
exitCode: 0,
|
|
326
|
-
duration: 0,
|
|
327
|
-
error: null,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// Step 2: Read task object
|
|
333
|
-
let task;
|
|
334
|
-
try {
|
|
335
|
-
const taskData = await objectRead(repoPath, taskHash);
|
|
336
|
-
const decoder = decodeBeast2For(TaskObjectType);
|
|
337
|
-
task = decoder(Buffer.from(taskData));
|
|
338
|
-
}
|
|
339
|
-
catch (err) {
|
|
340
|
-
return {
|
|
341
|
-
inputsHash: inHash,
|
|
342
|
-
cached: false,
|
|
343
|
-
state: 'error',
|
|
344
|
-
outputHash: null,
|
|
345
|
-
exitCode: null,
|
|
346
|
-
duration: Date.now() - startTime,
|
|
347
|
-
error: `Failed to read task object: ${err}`,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
// Step 3: Create scratch directory
|
|
351
|
-
// Include PID to prevent collisions when multiple e3 processes run the same
|
|
352
|
-
// task concurrently (e.g., same task in different workspaces at same millisecond)
|
|
353
|
-
const scratchDir = path.join(tmpdir(), `e3-exec-${taskHash.slice(0, 8)}-${inHash.slice(0, 8)}-${process.pid}-${Date.now()}`);
|
|
354
|
-
await fs.mkdir(scratchDir, { recursive: true });
|
|
355
|
-
try {
|
|
356
|
-
// Step 4: Marshal inputs to scratch dir
|
|
357
|
-
const inputPaths = [];
|
|
358
|
-
for (let i = 0; i < inputHashes.length; i++) {
|
|
359
|
-
const inputPath = path.join(scratchDir, `input-${i}.beast2`);
|
|
360
|
-
const inputData = await objectRead(repoPath, inputHashes[i]);
|
|
361
|
-
await fs.writeFile(inputPath, inputData);
|
|
362
|
-
inputPaths.push(inputPath);
|
|
363
|
-
}
|
|
364
|
-
// Step 5: Evaluate command IR to get exec args
|
|
365
|
-
const outputPath = path.join(scratchDir, 'output.beast2');
|
|
366
|
-
let args;
|
|
367
|
-
try {
|
|
368
|
-
args = await evaluateCommandIr(repoPath, task.commandIr, inputPaths, outputPath);
|
|
369
|
-
}
|
|
370
|
-
catch (err) {
|
|
371
|
-
return {
|
|
372
|
-
inputsHash: inHash,
|
|
373
|
-
cached: false,
|
|
374
|
-
state: 'error',
|
|
375
|
-
outputHash: null,
|
|
376
|
-
exitCode: null,
|
|
377
|
-
duration: Date.now() - startTime,
|
|
378
|
-
error: `Failed to evaluate command IR: ${err}`,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
if (args.length === 0) {
|
|
382
|
-
return {
|
|
383
|
-
inputsHash: inHash,
|
|
384
|
-
cached: false,
|
|
385
|
-
state: 'error',
|
|
386
|
-
outputHash: null,
|
|
387
|
-
exitCode: null,
|
|
388
|
-
duration: Date.now() - startTime,
|
|
389
|
-
error: 'Command IR produced empty command',
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
// Step 6: Create execution directory
|
|
393
|
-
await fs.mkdir(execDir, { recursive: true });
|
|
394
|
-
// Step 7: Get boot ID for crash detection
|
|
395
|
-
const bootId = await getBootId();
|
|
396
|
-
// Step 8: Execute command
|
|
397
|
-
const result = await runCommand(args, execDir, inputHashes, bootId, options);
|
|
398
|
-
// Step 9: Handle result
|
|
399
|
-
if (result.exitCode === 0) {
|
|
400
|
-
// Success - read and store output
|
|
401
|
-
try {
|
|
402
|
-
const outputData = await fs.readFile(outputPath);
|
|
403
|
-
const outputHash = await objectWrite(repoPath, outputData);
|
|
404
|
-
// Write output ref
|
|
405
|
-
await fs.writeFile(path.join(execDir, 'output'), outputHash + '\n');
|
|
406
|
-
// Write success status
|
|
407
|
-
const status = variant('success', {
|
|
408
|
-
inputHashes,
|
|
409
|
-
outputHash,
|
|
410
|
-
startedAt: new Date(startTime),
|
|
411
|
-
completedAt: new Date(),
|
|
412
|
-
});
|
|
413
|
-
const encoder = encodeBeast2For(ExecutionStatusType);
|
|
414
|
-
await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
|
|
415
|
-
return {
|
|
416
|
-
inputsHash: inHash,
|
|
417
|
-
cached: false,
|
|
418
|
-
state: 'success',
|
|
419
|
-
outputHash,
|
|
420
|
-
exitCode: 0,
|
|
421
|
-
duration: Date.now() - startTime,
|
|
422
|
-
error: null,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
catch (err) {
|
|
426
|
-
// Output file missing or unreadable
|
|
427
|
-
const status = variant('error', {
|
|
428
|
-
inputHashes,
|
|
429
|
-
startedAt: new Date(startTime),
|
|
430
|
-
completedAt: new Date(),
|
|
431
|
-
message: `Failed to read output: ${err}`,
|
|
432
|
-
});
|
|
433
|
-
const encoder = encodeBeast2For(ExecutionStatusType);
|
|
434
|
-
await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
|
|
435
|
-
return {
|
|
436
|
-
inputsHash: inHash,
|
|
437
|
-
cached: false,
|
|
438
|
-
state: 'error',
|
|
439
|
-
outputHash: null,
|
|
440
|
-
exitCode: 0,
|
|
441
|
-
duration: Date.now() - startTime,
|
|
442
|
-
error: `Failed to read output: ${err}`,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
else {
|
|
447
|
-
// Failed - write failed status
|
|
448
|
-
const status = variant('failed', {
|
|
449
|
-
inputHashes,
|
|
450
|
-
startedAt: new Date(startTime),
|
|
451
|
-
completedAt: new Date(),
|
|
452
|
-
exitCode: BigInt(result?.exitCode ?? -1),
|
|
453
|
-
});
|
|
454
|
-
const encoder = encodeBeast2For(ExecutionStatusType);
|
|
455
|
-
await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
|
|
456
|
-
return {
|
|
457
|
-
inputsHash: inHash,
|
|
458
|
-
cached: false,
|
|
459
|
-
state: 'failed',
|
|
460
|
-
outputHash: null,
|
|
461
|
-
exitCode: result.exitCode,
|
|
462
|
-
duration: Date.now() - startTime,
|
|
463
|
-
error: result.error,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
finally {
|
|
468
|
-
// Cleanup scratch directory
|
|
469
|
-
try {
|
|
470
|
-
await fs.rm(scratchDir, { recursive: true, force: true });
|
|
471
|
-
}
|
|
472
|
-
catch {
|
|
473
|
-
// Ignore cleanup errors
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Run a command and capture output
|
|
479
|
-
*/
|
|
480
|
-
async function runCommand(args, execDir, inputHashes, bootId, options) {
|
|
481
|
-
const [cmd, ...cmdArgs] = args;
|
|
482
|
-
// Process Lifecycle Management
|
|
483
|
-
// ============================
|
|
484
|
-
// We use detached: true to create a new process group, allowing us to kill
|
|
485
|
-
// the entire process tree by signaling the negative PID (process group leader).
|
|
486
|
-
//
|
|
487
|
-
// LIMITATION: Process groups are flat, not hierarchical. If a task spawns a
|
|
488
|
-
// subprocess that creates its own process group (via setsid, daemonization,
|
|
489
|
-
// or another detached spawn), that subprocess will escape our kill signal.
|
|
490
|
-
// This is a fundamental Unix limitation - process groups were designed for
|
|
491
|
-
// terminal job control (Ctrl+C/Ctrl+Z), not process tree management.
|
|
492
|
-
//
|
|
493
|
-
// For most tasks (shell scripts, pipelines, normal child processes) this works
|
|
494
|
-
// fine. A task would have to intentionally call setsid() to escape.
|
|
495
|
-
//
|
|
496
|
-
// Potential improvements for hosted e3:
|
|
497
|
-
// - Linux cgroups: Hierarchical containment with no escape. Requires root or
|
|
498
|
-
// systemd integration (systemd-run --scope). Used by Docker/Kubernetes.
|
|
499
|
-
// - PR_SET_CHILD_SUBREAPER: Makes e3 adopt orphaned processes instead of init,
|
|
500
|
-
// allowing tracking and cleanup. Requires polling to detect orphans.
|
|
501
|
-
// - Firecracker/microVMs: Complete isolation with hardware virtualization.
|
|
502
|
-
// The VM boundary provides bulletproof containment. Best for multi-tenant.
|
|
503
|
-
const child = spawn(cmd, cmdArgs, {
|
|
504
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
505
|
-
detached: true,
|
|
506
|
-
});
|
|
507
|
-
// Set up event listeners IMMEDIATELY before any async work
|
|
508
|
-
// to avoid missing events if the process completes quickly
|
|
509
|
-
const resultPromise = new Promise((resolve) => {
|
|
510
|
-
child.on('error', (err) => {
|
|
511
|
-
resolve({ exitCode: null, error: `Failed to spawn: ${err.message}` });
|
|
512
|
-
});
|
|
513
|
-
child.on('close', (code) => {
|
|
514
|
-
resolve({ exitCode: code, error: code !== 0 ? `Exit code: ${code}` : null });
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
// Open log files for writing
|
|
518
|
-
const stdoutStream = createWriteStream(path.join(execDir, 'stdout.txt'));
|
|
519
|
-
const stderrStream = createWriteStream(path.join(execDir, 'stderr.txt'));
|
|
520
|
-
// Tee stdout
|
|
521
|
-
child.stdout?.on('data', (data) => {
|
|
522
|
-
stdoutStream.write(data);
|
|
523
|
-
if (options.onStdout) {
|
|
524
|
-
options.onStdout(data.toString('utf-8'));
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
// Tee stderr
|
|
528
|
-
child.stderr?.on('data', (data) => {
|
|
529
|
-
stderrStream.write(data);
|
|
530
|
-
if (options.onStderr) {
|
|
531
|
-
options.onStderr(data.toString('utf-8'));
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
// Helper to kill the entire process group (child and all its descendants).
|
|
535
|
-
// With detached: true, child.pid is the process group leader, so killing
|
|
536
|
-
// -child.pid sends the signal to all processes in that group.
|
|
537
|
-
const killProcessGroup = () => {
|
|
538
|
-
if (child.pid) {
|
|
539
|
-
try {
|
|
540
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
541
|
-
}
|
|
542
|
-
catch {
|
|
543
|
-
// Process may have already exited
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
// Handle timeout
|
|
548
|
-
let timeoutId;
|
|
549
|
-
if (options.timeout) {
|
|
550
|
-
timeoutId = setTimeout(killProcessGroup, options.timeout);
|
|
551
|
-
}
|
|
552
|
-
// Handle abort signal
|
|
553
|
-
if (options.signal) {
|
|
554
|
-
if (options.signal.aborted) {
|
|
555
|
-
// Already aborted before we started
|
|
556
|
-
killProcessGroup();
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
options.signal.addEventListener('abort', killProcessGroup, { once: true });
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
// Write running status with actual child PID (can be async now)
|
|
563
|
-
const pidStartTime = await getPidStartTime(child.pid);
|
|
564
|
-
const status = variant('running', {
|
|
565
|
-
inputHashes,
|
|
566
|
-
startedAt: new Date(),
|
|
567
|
-
pid: BigInt(child.pid ?? -1),
|
|
568
|
-
pidStartTime: BigInt(pidStartTime ?? -1),
|
|
569
|
-
bootId,
|
|
570
|
-
});
|
|
571
|
-
const encoder = encodeBeast2For(ExecutionStatusType);
|
|
572
|
-
await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
|
|
573
|
-
// Wait for process to complete
|
|
574
|
-
const result = await resultPromise;
|
|
575
|
-
// Cleanup
|
|
576
|
-
if (timeoutId)
|
|
577
|
-
clearTimeout(timeoutId);
|
|
578
|
-
if (options.signal) {
|
|
579
|
-
options.signal.removeEventListener('abort', killProcessGroup);
|
|
580
|
-
}
|
|
581
|
-
stdoutStream.end();
|
|
582
|
-
stderrStream.end();
|
|
583
|
-
return result;
|
|
584
|
-
}
|
|
585
219
|
//# sourceMappingURL=executions.js.map
|