@elaraai/e3-core 0.0.2-beta.8 → 1.0.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/LICENSE.md +4 -0
- package/README.md +74 -35
- 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/api-compat.spec.d.ts +6 -0
- package/dist/src/dataflow/api-compat.spec.d.ts.map +1 -0
- package/dist/src/dataflow/api-compat.spec.js +182 -0
- package/dist/src/dataflow/api-compat.spec.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 +729 -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/InMemoryStateStore.spec.d.ts +6 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.spec.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.spec.js +114 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.spec.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/steps.spec.d.ts +6 -0
- package/dist/src/dataflow/steps.spec.d.ts.map +1 -0
- package/dist/src/dataflow/steps.spec.js +343 -0
- package/dist/src/dataflow/steps.spec.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-orchestration.spec.d.ts +6 -0
- package/dist/src/dataflow-orchestration.spec.d.ts.map +1 -0
- package/dist/src/dataflow-orchestration.spec.js +1025 -0
- package/dist/src/dataflow-orchestration.spec.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/dataflow.spec.d.ts +6 -0
- package/dist/src/dataflow.spec.d.ts.map +1 -0
- package/dist/src/dataflow.spec.js +663 -0
- package/dist/src/dataflow.spec.js.map +1 -0
- 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/errors.spec.d.ts +6 -0
- package/dist/src/errors.spec.d.ts.map +1 -0
- package/dist/src/errors.spec.js +276 -0
- package/dist/src/errors.spec.js.map +1 -0
- 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 +113 -481
- package/dist/src/executions.js.map +1 -1
- package/dist/src/executions.spec.d.ts +6 -0
- package/dist/src/executions.spec.d.ts.map +1 -0
- package/dist/src/executions.spec.js +387 -0
- package/dist/src/executions.spec.js.map +1 -0
- package/dist/src/formats.d.ts +18 -2
- package/dist/src/formats.d.ts.map +1 -1
- package/dist/src/formats.js +34 -2
- package/dist/src/formats.js.map +1 -1
- package/dist/src/gc.spec.d.ts +6 -0
- package/dist/src/gc.spec.d.ts.map +1 -0
- package/dist/src/gc.spec.js +512 -0
- package/dist/src/gc.spec.js.map +1 -0
- package/dist/src/index.d.ts +20 -10
- 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 +7 -53
- package/dist/src/objects.d.ts.map +1 -1
- package/dist/src/objects.js +13 -232
- package/dist/src/objects.js.map +1 -1
- package/dist/src/objects.spec.d.ts +6 -0
- package/dist/src/objects.spec.d.ts.map +1 -0
- package/dist/src/objects.spec.js +247 -0
- package/dist/src/objects.spec.js.map +1 -0
- package/dist/src/packages.d.ts +41 -14
- package/dist/src/packages.d.ts.map +1 -1
- package/dist/src/packages.js +151 -89
- package/dist/src/packages.js.map +1 -1
- package/dist/src/packages.spec.d.ts +6 -0
- package/dist/src/packages.spec.d.ts.map +1 -0
- package/dist/src/packages.spec.js +324 -0
- package/dist/src/packages.spec.js.map +1 -0
- 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/InMemoryRepoStore.spec.d.ts +6 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.spec.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.spec.js +187 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.spec.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 +364 -0
- package/dist/src/storage/local/LocalLockService.js.map +1 -0
- package/dist/src/storage/local/LocalLockService.spec.d.ts +6 -0
- package/dist/src/storage/local/LocalLockService.spec.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLockService.spec.js +148 -0
- package/dist/src/storage/local/LocalLockService.spec.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/LocalRepoStore.spec.d.ts +6 -0
- package/dist/src/storage/local/LocalRepoStore.spec.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.spec.js +255 -0
- package/dist/src/storage/local/LocalRepoStore.spec.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/storage/local/repository.spec.d.ts +6 -0
- package/dist/src/storage/local/repository.spec.d.ts.map +1 -0
- package/dist/src/storage/local/repository.spec.js +186 -0
- package/dist/src/storage/local/repository.spec.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/tasks.spec.d.ts +6 -0
- package/dist/src/tasks.spec.d.ts.map +1 -0
- package/dist/src/tasks.spec.js +105 -0
- package/dist/src/tasks.spec.js.map +1 -0
- 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 +75 -0
- package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
- package/dist/src/transfer/InMemoryTransferBackend.js +211 -0
- package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
- package/dist/src/transfer/index.d.ts +9 -0
- package/dist/src/transfer/index.d.ts.map +1 -0
- package/dist/src/transfer/index.js +11 -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/process.d.ts +55 -0
- package/dist/src/transfer/process.d.ts.map +1 -0
- package/dist/src/transfer/process.js +144 -0
- package/dist/src/transfer/process.js.map +1 -0
- package/dist/src/transfer/types.d.ts +106 -0
- package/dist/src/transfer/types.d.ts.map +1 -0
- package/dist/src/transfer/types.js +61 -0
- package/dist/src/transfer/types.js.map +1 -0
- package/dist/src/trees.d.ts +102 -63
- package/dist/src/trees.d.ts.map +1 -1
- package/dist/src/trees.js +319 -479
- package/dist/src/trees.js.map +1 -1
- package/dist/src/trees.spec.d.ts +6 -0
- package/dist/src/trees.spec.d.ts.map +1 -0
- package/dist/src/trees.spec.js +635 -0
- package/dist/src/trees.spec.js.map +1 -0
- 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 +46 -60
- package/dist/src/workspaceStatus.js.map +1 -1
- package/dist/src/workspaces.d.ts +46 -47
- package/dist/src/workspaces.d.ts.map +1 -1
- package/dist/src/workspaces.js +281 -221
- package/dist/src/workspaces.js.map +1 -1
- package/dist/src/workspaces.spec.d.ts +6 -0
- package/dist/src/workspaces.spec.d.ts.map +1 -0
- package/dist/src/workspaces.spec.js +273 -0
- package/dist/src/workspaces.spec.js.map +1 -0
- package/package.json +15 -15
- 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
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Licensed under BSL 1.1. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Local filesystem implementation of workspace locking.
|
|
7
|
+
*
|
|
8
|
+
* Provides exclusive locks on workspaces to prevent concurrent dataflow
|
|
9
|
+
* executions or writes that could corrupt workspace state. Uses Linux
|
|
10
|
+
* flock() for automatic lock release on process death.
|
|
11
|
+
*
|
|
12
|
+
* Lock mechanism:
|
|
13
|
+
* - Uses flock(LOCK_EX | LOCK_NB) via the `flock` command for kernel-managed locking
|
|
14
|
+
* - Lock is automatically released when the process dies (kernel handles this)
|
|
15
|
+
* - Lock state stored in beast2 format using LockStateType from e3-types
|
|
16
|
+
* - Holder stored as East text string (e.g., `.process (pid=1234, ...)`)
|
|
17
|
+
* - Stale lock detection via bootId comparison (handles system restarts)
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs/promises';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { spawn } from 'child_process';
|
|
22
|
+
import { encodeBeast2For, decodeBeast2For, printFor, parseInferred, variant, none, VariantType } from '@elaraai/east';
|
|
23
|
+
import { LockStateType, ProcessHolderType } from '@elaraai/e3-types';
|
|
24
|
+
import { WorkspaceLockError } from '../../errors.js';
|
|
25
|
+
import { getBootId, getPidStartTime, isProcessAlive } from '../../execution/processHelpers.js';
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Holder Encoding
|
|
28
|
+
// =============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Variant type for encoding holder as East text.
|
|
31
|
+
* The holder string stores `.process (...)` or other backend-specific variants.
|
|
32
|
+
*/
|
|
33
|
+
const HolderVariantType = VariantType({
|
|
34
|
+
process: ProcessHolderType,
|
|
35
|
+
});
|
|
36
|
+
/** Print a process holder to East text format */
|
|
37
|
+
const printProcessHolder = printFor(HolderVariantType);
|
|
38
|
+
/**
|
|
39
|
+
* Parse an East text holder string.
|
|
40
|
+
* Returns the parsed variant or null if parsing fails.
|
|
41
|
+
*/
|
|
42
|
+
function parseHolder(holderStr) {
|
|
43
|
+
try {
|
|
44
|
+
const [_type, value] = parseInferred(holderStr);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Lock File Helpers
|
|
53
|
+
// =============================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Get the lock file path for a workspace.
|
|
56
|
+
*/
|
|
57
|
+
export function workspaceLockPath(repoPath, workspace) {
|
|
58
|
+
return path.join(repoPath, 'workspaces', `${workspace}.lock`);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read lock state from a lock file.
|
|
62
|
+
* Returns null if file doesn't exist or is invalid.
|
|
63
|
+
*/
|
|
64
|
+
async function readLockState(lockPath) {
|
|
65
|
+
try {
|
|
66
|
+
const data = await fs.readFile(lockPath);
|
|
67
|
+
if (data.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
const decoder = decodeBeast2For(LockStateType);
|
|
70
|
+
return decoder(data);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Write lock state to a lock file in beast2 format.
|
|
78
|
+
*/
|
|
79
|
+
async function writeLockState(lockPath, state) {
|
|
80
|
+
const encoder = encodeBeast2For(LockStateType);
|
|
81
|
+
const data = encoder(state);
|
|
82
|
+
await fs.writeFile(lockPath, data);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Convert LockState to LockHolderInfo for error display.
|
|
86
|
+
*/
|
|
87
|
+
export function lockStateToHolderInfo(state) {
|
|
88
|
+
const info = {
|
|
89
|
+
acquiredAt: state.acquiredAt.toISOString(),
|
|
90
|
+
operation: state.operation.type,
|
|
91
|
+
};
|
|
92
|
+
// Parse the holder string to extract process-specific fields
|
|
93
|
+
const holder = parseHolder(state.holder);
|
|
94
|
+
if (holder?.type === 'process') {
|
|
95
|
+
info.pid = Number(holder.value.pid);
|
|
96
|
+
info.bootId = holder.value.bootId;
|
|
97
|
+
info.startTime = Number(holder.value.startTime);
|
|
98
|
+
info.command = holder.value.command;
|
|
99
|
+
}
|
|
100
|
+
return info;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if a lock holder is still alive.
|
|
104
|
+
* @param holderStr - East text-encoded holder string
|
|
105
|
+
*/
|
|
106
|
+
export async function isLockHolderAlive(holderStr) {
|
|
107
|
+
const holder = parseHolder(holderStr);
|
|
108
|
+
if (!holder)
|
|
109
|
+
return true; // Can't parse - assume alive (safer)
|
|
110
|
+
if (holder.type === 'process') {
|
|
111
|
+
return isProcessAlive(Number(holder.value.pid), Number(holder.value.startTime), holder.value.bootId);
|
|
112
|
+
}
|
|
113
|
+
// Unknown holder type - assume alive (safer default)
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Lock Acquisition
|
|
118
|
+
// =============================================================================
|
|
119
|
+
/**
|
|
120
|
+
* Acquire an exclusive lock on a workspace.
|
|
121
|
+
*
|
|
122
|
+
* Uses Linux flock() for kernel-managed locking. The lock is automatically
|
|
123
|
+
* released when the process exits (even on crash/kill).
|
|
124
|
+
*
|
|
125
|
+
* @param repoPath - Path to e3 repository
|
|
126
|
+
* @param workspace - Workspace name to lock
|
|
127
|
+
* @param operation - What operation is acquiring the lock
|
|
128
|
+
* @param options - Lock acquisition options
|
|
129
|
+
* @returns Lock handle - call release() when done
|
|
130
|
+
* @throws {WorkspaceLockError} If workspace is locked by another process
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const lock = await acquireWorkspaceLock(repoPath, 'production', { type: 'dataflow', value: null });
|
|
135
|
+
* try {
|
|
136
|
+
* await dataflowExecute(repoPath, 'production', { lock });
|
|
137
|
+
* } finally {
|
|
138
|
+
* await lock.release();
|
|
139
|
+
* }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export async function acquireWorkspaceLock(repoPath, workspace, operation, options = {}) {
|
|
143
|
+
const lockPath = workspaceLockPath(repoPath, workspace);
|
|
144
|
+
// Ensure workspaces directory exists
|
|
145
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
146
|
+
// Gather our process identification
|
|
147
|
+
const pid = process.pid;
|
|
148
|
+
const bootId = await getBootId();
|
|
149
|
+
const startTime = await getPidStartTime(pid);
|
|
150
|
+
const command = process.argv.join(' ');
|
|
151
|
+
const acquiredAt = new Date();
|
|
152
|
+
// Encode holder as East text: .process (pid=..., bootId="...", ...)
|
|
153
|
+
const holderVariant = variant('process', {
|
|
154
|
+
pid: BigInt(pid),
|
|
155
|
+
bootId,
|
|
156
|
+
startTime: BigInt(startTime),
|
|
157
|
+
command,
|
|
158
|
+
});
|
|
159
|
+
const holder = printProcessHolder(holderVariant);
|
|
160
|
+
const lockState = {
|
|
161
|
+
operation,
|
|
162
|
+
holder,
|
|
163
|
+
acquiredAt,
|
|
164
|
+
expiresAt: none,
|
|
165
|
+
};
|
|
166
|
+
// Try to acquire flock via subprocess
|
|
167
|
+
// The subprocess holds the lock and we communicate with it via stdin/signals
|
|
168
|
+
const flockProcess = await tryAcquireFlock(lockPath, lockState, options);
|
|
169
|
+
if (!flockProcess) {
|
|
170
|
+
// Failed to acquire - read lock state to report who has it
|
|
171
|
+
const existingState = await readLockState(lockPath);
|
|
172
|
+
const holderInfo = existingState ? lockStateToHolderInfo(existingState) : undefined;
|
|
173
|
+
throw new WorkspaceLockError(workspace, holderInfo);
|
|
174
|
+
}
|
|
175
|
+
// Lock acquired! Create handle
|
|
176
|
+
let released = false;
|
|
177
|
+
const handle = {
|
|
178
|
+
resource: workspace,
|
|
179
|
+
workspace,
|
|
180
|
+
lockPath,
|
|
181
|
+
async release() {
|
|
182
|
+
if (released)
|
|
183
|
+
return;
|
|
184
|
+
released = true;
|
|
185
|
+
// Kill the flock subprocess to release the lock
|
|
186
|
+
flockProcess.kill('SIGTERM');
|
|
187
|
+
// Clean up lock file (best effort)
|
|
188
|
+
try {
|
|
189
|
+
await fs.unlink(lockPath);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Ignore - file might already be gone
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
return handle;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Try to acquire flock using a subprocess.
|
|
200
|
+
*
|
|
201
|
+
* We spawn `flock --nonblock <lockfile> cat` which:
|
|
202
|
+
* 1. Tries to acquire exclusive lock (non-blocking)
|
|
203
|
+
* 2. If successful, runs `cat` which blocks reading stdin forever
|
|
204
|
+
* 3. We keep the subprocess alive to hold the lock
|
|
205
|
+
* 4. When we kill the subprocess, the lock is released
|
|
206
|
+
*
|
|
207
|
+
* Returns the subprocess if lock acquired, null if lock is held by another.
|
|
208
|
+
*/
|
|
209
|
+
async function tryAcquireFlock(lockPath, lockState, options) {
|
|
210
|
+
// First, check if there's a stale lock we can clean up
|
|
211
|
+
// (only for exclusive mode — shared locks don't need stale checking)
|
|
212
|
+
if (options.mode !== 'shared') {
|
|
213
|
+
await checkAndCleanStaleLock(lockPath);
|
|
214
|
+
}
|
|
215
|
+
const isShared = options.mode === 'shared';
|
|
216
|
+
const args = [];
|
|
217
|
+
if (isShared) {
|
|
218
|
+
args.push('--shared');
|
|
219
|
+
}
|
|
220
|
+
if (options.wait) {
|
|
221
|
+
args.push('--timeout', String((options.timeout ?? 30000) / 1000));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
args.push('--nonblock');
|
|
225
|
+
}
|
|
226
|
+
// Use 'sh -c "echo ready && cat"' as the inner command so that "ready"
|
|
227
|
+
// on stdout is a deterministic signal that flock acquired the lock and
|
|
228
|
+
// started the inner command. `cat` then blocks on stdin to keep the
|
|
229
|
+
// subprocess (and therefore the lock) alive until we kill it.
|
|
230
|
+
args.push(lockPath, 'sh', '-c', 'echo ready && cat');
|
|
231
|
+
const child = spawn('flock', args, {
|
|
232
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
233
|
+
detached: false,
|
|
234
|
+
});
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
let resolved = false;
|
|
237
|
+
// If flock fails to acquire, it exits with code 1
|
|
238
|
+
child.on('error', () => {
|
|
239
|
+
if (!resolved) {
|
|
240
|
+
resolved = true;
|
|
241
|
+
resolve(null);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
child.on('exit', () => {
|
|
245
|
+
if (!resolved) {
|
|
246
|
+
resolved = true;
|
|
247
|
+
// Exit code 1 means lock is held by another
|
|
248
|
+
resolve(null);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// When flock acquires the lock, the inner command prints "ready" to
|
|
252
|
+
// stdout. This is a deterministic signal — no timing assumptions.
|
|
253
|
+
child.stdout.on('data', (data) => {
|
|
254
|
+
if (!resolved && data.toString().includes('ready')) {
|
|
255
|
+
resolved = true;
|
|
256
|
+
// Write lock state before resolving so release() can safely unlink
|
|
257
|
+
writeLockState(lockPath, lockState)
|
|
258
|
+
.then(() => {
|
|
259
|
+
resolve(child);
|
|
260
|
+
})
|
|
261
|
+
.catch(() => {
|
|
262
|
+
// Can't write state — release the kernel lock and report failure
|
|
263
|
+
child.kill('SIGTERM');
|
|
264
|
+
resolve(null);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check if a lock file exists with stale lock state and clean it up.
|
|
272
|
+
* A lock is stale if the holder process no longer exists.
|
|
273
|
+
*/
|
|
274
|
+
async function checkAndCleanStaleLock(lockPath) {
|
|
275
|
+
const state = await readLockState(lockPath);
|
|
276
|
+
if (!state)
|
|
277
|
+
return;
|
|
278
|
+
// Check if the holder is still alive
|
|
279
|
+
const alive = await isLockHolderAlive(state.holder);
|
|
280
|
+
if (!alive) {
|
|
281
|
+
// Stale lock - try to remove it
|
|
282
|
+
try {
|
|
283
|
+
await fs.unlink(lockPath);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Ignore - another process might have cleaned it up
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get the lock state for a workspace.
|
|
292
|
+
*
|
|
293
|
+
* @param repoPath - Path to e3 repository
|
|
294
|
+
* @param workspace - Workspace name to check
|
|
295
|
+
* @returns Lock state if locked, null if not locked
|
|
296
|
+
*/
|
|
297
|
+
export async function getWorkspaceLockState(repoPath, workspace) {
|
|
298
|
+
const lockPath = workspaceLockPath(repoPath, workspace);
|
|
299
|
+
const state = await readLockState(lockPath);
|
|
300
|
+
if (!state)
|
|
301
|
+
return null;
|
|
302
|
+
// Check if the holder is still alive
|
|
303
|
+
const alive = await isLockHolderAlive(state.holder);
|
|
304
|
+
if (!alive) {
|
|
305
|
+
// Stale lock - clean it up and report as not locked
|
|
306
|
+
try {
|
|
307
|
+
await fs.unlink(lockPath);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Ignore
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return state;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get lock holder info for a workspace (for backwards compatibility).
|
|
318
|
+
*
|
|
319
|
+
* @param repoPath - Path to e3 repository
|
|
320
|
+
* @param workspace - Workspace name to check
|
|
321
|
+
* @returns Lock holder info if locked, null if not locked
|
|
322
|
+
* @deprecated Use getWorkspaceLockState for full lock information
|
|
323
|
+
*/
|
|
324
|
+
export async function getWorkspaceLockHolder(repoPath, workspace) {
|
|
325
|
+
const state = await getWorkspaceLockState(repoPath, workspace);
|
|
326
|
+
return state ? lockStateToHolderInfo(state) : null;
|
|
327
|
+
}
|
|
328
|
+
// =============================================================================
|
|
329
|
+
// LockService Interface Implementation
|
|
330
|
+
// =============================================================================
|
|
331
|
+
/**
|
|
332
|
+
* Local filesystem implementation of LockService.
|
|
333
|
+
*
|
|
334
|
+
* Uses flock() for kernel-managed locking with lock state
|
|
335
|
+
* stored in beast2 format using LockStateType.
|
|
336
|
+
* The `repo` parameter is the path to the e3 repository directory.
|
|
337
|
+
*/
|
|
338
|
+
export class LocalLockService {
|
|
339
|
+
async acquire(repo, resource, operation, options) {
|
|
340
|
+
const acquireOptions = {
|
|
341
|
+
wait: options?.wait ?? false,
|
|
342
|
+
timeout: options?.timeout,
|
|
343
|
+
mode: options?.mode ?? 'exclusive',
|
|
344
|
+
};
|
|
345
|
+
try {
|
|
346
|
+
const handle = await acquireWorkspaceLock(repo, resource, operation, acquireOptions);
|
|
347
|
+
return {
|
|
348
|
+
resource,
|
|
349
|
+
release: () => handle.release(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Lock couldn't be acquired
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
getState(repo, resource) {
|
|
358
|
+
return getWorkspaceLockState(repo, resource);
|
|
359
|
+
}
|
|
360
|
+
isHolderAlive(holder) {
|
|
361
|
+
return isLockHolderAlive(holder);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
//# sourceMappingURL=LocalLockService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalLockService.js","sourceRoot":"","sources":["../../../../src/storage/local/LocalLockService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalLockService.spec.d.ts","sourceRoot":"","sources":["../../../../src/storage/local/LocalLockService.spec.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Licensed under BSL 1.1. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Tests for LocalLockService - workspace locking mechanism
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
9
|
+
import assert from 'node:assert';
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import { variant, encodeBeast2For, printFor, VariantType } from '@elaraai/east';
|
|
14
|
+
import { LockStateType, ProcessHolderType } from '@elaraai/e3-types';
|
|
15
|
+
import { acquireWorkspaceLock, getWorkspaceLockHolder, workspaceLockPath, } from './LocalLockService.js';
|
|
16
|
+
import { WorkspaceLockError } from '../../errors.js';
|
|
17
|
+
describe('LocalLockService', () => {
|
|
18
|
+
let testDir;
|
|
19
|
+
let repoPath;
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'e3-lock-test-'));
|
|
22
|
+
repoPath = path.join(testDir, 'repo');
|
|
23
|
+
await fs.mkdir(path.join(repoPath, 'workspaces'), { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
describe('workspaceLockPath', () => {
|
|
29
|
+
it('returns correct path', () => {
|
|
30
|
+
const lockPath = workspaceLockPath('/repo', 'myws');
|
|
31
|
+
assert.strictEqual(lockPath, '/repo/workspaces/myws.lock');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('acquireWorkspaceLock', () => {
|
|
35
|
+
it('acquires lock on unlocked workspace', async () => {
|
|
36
|
+
const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
37
|
+
assert.strictEqual(lock.workspace, 'test-ws');
|
|
38
|
+
assert.ok(lock.lockPath.endsWith('test-ws.lock'));
|
|
39
|
+
await lock.release();
|
|
40
|
+
});
|
|
41
|
+
it('creates lock file with metadata', async () => {
|
|
42
|
+
const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
43
|
+
// Use getWorkspaceLockHolder to read the metadata (it handles beast2 format)
|
|
44
|
+
const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
|
|
45
|
+
assert.ok(holder);
|
|
46
|
+
assert.strictEqual(holder.pid, process.pid);
|
|
47
|
+
assert.ok(holder.bootId);
|
|
48
|
+
assert.ok(holder.acquiredAt);
|
|
49
|
+
assert.ok(holder.command);
|
|
50
|
+
assert.strictEqual(holder.operation, 'dataflow');
|
|
51
|
+
await lock.release();
|
|
52
|
+
});
|
|
53
|
+
it('removes lock file on release', async () => {
|
|
54
|
+
const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
55
|
+
const lockPath = workspaceLockPath(repoPath, 'test-ws');
|
|
56
|
+
// Lock file should exist
|
|
57
|
+
await fs.access(lockPath);
|
|
58
|
+
await lock.release();
|
|
59
|
+
// Lock file should be gone
|
|
60
|
+
await assert.rejects(fs.access(lockPath), { code: 'ENOENT' });
|
|
61
|
+
});
|
|
62
|
+
it('release is idempotent', async () => {
|
|
63
|
+
const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
64
|
+
await lock.release();
|
|
65
|
+
await lock.release(); // Should not throw
|
|
66
|
+
await lock.release(); // Should not throw
|
|
67
|
+
});
|
|
68
|
+
it('throws WorkspaceLockError when already locked', async () => {
|
|
69
|
+
const lock1 = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
70
|
+
try {
|
|
71
|
+
await assert.rejects(acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null)), (err) => {
|
|
72
|
+
assert.ok(err instanceof WorkspaceLockError);
|
|
73
|
+
assert.strictEqual(err.workspace, 'test-ws');
|
|
74
|
+
// Should have holder info since we wrote metadata
|
|
75
|
+
const holder = err.holder;
|
|
76
|
+
assert.ok(holder);
|
|
77
|
+
assert.strictEqual(holder.pid, process.pid);
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await lock1.release();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
it('allows acquiring lock after release', async () => {
|
|
86
|
+
const lock1 = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
87
|
+
await lock1.release();
|
|
88
|
+
const lock2 = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
89
|
+
assert.ok(lock2);
|
|
90
|
+
await lock2.release();
|
|
91
|
+
});
|
|
92
|
+
it('allows different workspaces to be locked independently', async () => {
|
|
93
|
+
const lock1 = await acquireWorkspaceLock(repoPath, 'ws1', variant('dataflow', null));
|
|
94
|
+
const lock2 = await acquireWorkspaceLock(repoPath, 'ws2', variant('dataflow', null));
|
|
95
|
+
assert.strictEqual(lock1.workspace, 'ws1');
|
|
96
|
+
assert.strictEqual(lock2.workspace, 'ws2');
|
|
97
|
+
await lock1.release();
|
|
98
|
+
await lock2.release();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('getWorkspaceLockHolder', () => {
|
|
102
|
+
it('returns null for unlocked workspace', async () => {
|
|
103
|
+
const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
|
|
104
|
+
assert.strictEqual(holder, null);
|
|
105
|
+
});
|
|
106
|
+
it('returns holder info for locked workspace', async () => {
|
|
107
|
+
const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
108
|
+
const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
|
|
109
|
+
assert.ok(holder);
|
|
110
|
+
assert.strictEqual(holder.pid, process.pid);
|
|
111
|
+
assert.ok(holder.acquiredAt);
|
|
112
|
+
await lock.release();
|
|
113
|
+
});
|
|
114
|
+
it('returns null after lock is released', async () => {
|
|
115
|
+
const lock = await acquireWorkspaceLock(repoPath, 'test-ws', variant('dataflow', null));
|
|
116
|
+
await lock.release();
|
|
117
|
+
const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
|
|
118
|
+
assert.strictEqual(holder, null);
|
|
119
|
+
});
|
|
120
|
+
it('cleans up stale lock with dead PID', async () => {
|
|
121
|
+
// Write a fake lock file in beast2 format with a non-existent PID
|
|
122
|
+
const lockPath = workspaceLockPath(repoPath, 'test-ws');
|
|
123
|
+
// Create holder as East text string
|
|
124
|
+
const HolderVariantType = VariantType({ process: ProcessHolderType });
|
|
125
|
+
const printHolder = printFor(HolderVariantType);
|
|
126
|
+
const holderString = printHolder(variant('process', {
|
|
127
|
+
pid: 99999999n, // Very unlikely to exist
|
|
128
|
+
bootId: 'fake-boot-id',
|
|
129
|
+
startTime: 0n,
|
|
130
|
+
command: 'fake command',
|
|
131
|
+
}));
|
|
132
|
+
const fakeLockState = {
|
|
133
|
+
operation: variant('dataflow', null),
|
|
134
|
+
holder: holderString,
|
|
135
|
+
acquiredAt: new Date(),
|
|
136
|
+
expiresAt: variant('none', null),
|
|
137
|
+
};
|
|
138
|
+
const encoder = encodeBeast2For(LockStateType);
|
|
139
|
+
await fs.writeFile(lockPath, encoder(fakeLockState));
|
|
140
|
+
// getWorkspaceLockHolder should detect this as stale and clean up
|
|
141
|
+
const holder = await getWorkspaceLockHolder(repoPath, 'test-ws');
|
|
142
|
+
assert.strictEqual(holder, null);
|
|
143
|
+
// Lock file should be cleaned up
|
|
144
|
+
await assert.rejects(fs.access(lockPath), { code: 'ENOENT' });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
//# sourceMappingURL=LocalLockService.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalLockService.spec.js","sourceRoot":"","sources":["../../../../src/storage/local/LocalLockService.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Licensed under BSL 1.1. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
import type { LogChunk, LogStore } from '../interfaces.js';
|
|
6
|
+
/**
|
|
7
|
+
* Local filesystem implementation of LogStore.
|
|
8
|
+
*
|
|
9
|
+
* Logs are stored as text files in the execution directory:
|
|
10
|
+
* executions/<taskHash>/<inputsHash>/<executionId>/stdout.txt
|
|
11
|
+
* executions/<taskHash>/<inputsHash>/<executionId>/stderr.txt
|
|
12
|
+
*
|
|
13
|
+
* The `repo` parameter is the path to the e3 repository directory.
|
|
14
|
+
*/
|
|
15
|
+
export declare class LocalLogStore implements LogStore {
|
|
16
|
+
private logPath;
|
|
17
|
+
append(repo: string, taskHash: string, inputsHash: string, executionId: string, stream: 'stdout' | 'stderr', data: string): Promise<void>;
|
|
18
|
+
read(repo: string, taskHash: string, inputsHash: string, executionId: string, stream: 'stdout' | 'stderr', options?: {
|
|
19
|
+
offset?: number;
|
|
20
|
+
limit?: number;
|
|
21
|
+
}): Promise<LogChunk>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=LocalLogStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalLogStore.d.ts","sourceRoot":"","sources":["../../../../src/storage/local/LocalLogStore.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG3D;;;;;;;;GAQG;AACH,qBAAa,aAAc,YAAW,QAAQ;IAC5C,OAAO,CAAC,OAAO;IAWT,MAAM,CACV,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;IAQV,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAC3B,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5C,OAAO,CAAC,QAAQ,CAAC;CAwCrB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Licensed under BSL 1.1. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { isNotFoundError } from '../../errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Local filesystem implementation of LogStore.
|
|
10
|
+
*
|
|
11
|
+
* Logs are stored as text files in the execution directory:
|
|
12
|
+
* executions/<taskHash>/<inputsHash>/<executionId>/stdout.txt
|
|
13
|
+
* executions/<taskHash>/<inputsHash>/<executionId>/stderr.txt
|
|
14
|
+
*
|
|
15
|
+
* The `repo` parameter is the path to the e3 repository directory.
|
|
16
|
+
*/
|
|
17
|
+
export class LocalLogStore {
|
|
18
|
+
logPath(repo, taskHash, inputsHash, executionId, stream) {
|
|
19
|
+
return path.join(repo, 'executions', taskHash, inputsHash, executionId, `${stream}.txt`);
|
|
20
|
+
}
|
|
21
|
+
async append(repo, taskHash, inputsHash, executionId, stream, data) {
|
|
22
|
+
const logFile = this.logPath(repo, taskHash, inputsHash, executionId, stream);
|
|
23
|
+
const dir = path.dirname(logFile);
|
|
24
|
+
await fs.mkdir(dir, { recursive: true });
|
|
25
|
+
await fs.appendFile(logFile, data);
|
|
26
|
+
}
|
|
27
|
+
async read(repo, taskHash, inputsHash, executionId, stream, options) {
|
|
28
|
+
const logFile = this.logPath(repo, taskHash, inputsHash, executionId, stream);
|
|
29
|
+
const offset = options?.offset ?? 0;
|
|
30
|
+
const limit = options?.limit ?? 65536; // 64KB default
|
|
31
|
+
try {
|
|
32
|
+
const stat = await fs.stat(logFile);
|
|
33
|
+
const totalSize = stat.size;
|
|
34
|
+
// Open file and read chunk
|
|
35
|
+
const fd = await fs.open(logFile, 'r');
|
|
36
|
+
try {
|
|
37
|
+
const buffer = Buffer.alloc(Math.min(limit, Math.max(0, totalSize - offset)));
|
|
38
|
+
const { bytesRead } = await fd.read(buffer, 0, buffer.length, offset);
|
|
39
|
+
return {
|
|
40
|
+
data: buffer.slice(0, bytesRead).toString('utf-8'),
|
|
41
|
+
offset,
|
|
42
|
+
size: bytesRead,
|
|
43
|
+
totalSize,
|
|
44
|
+
complete: offset + bytesRead >= totalSize,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await fd.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (isNotFoundError(err)) {
|
|
53
|
+
// Log file doesn't exist yet
|
|
54
|
+
return {
|
|
55
|
+
data: '',
|
|
56
|
+
offset: 0,
|
|
57
|
+
size: 0,
|
|
58
|
+
totalSize: 0,
|
|
59
|
+
complete: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=LocalLogStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalLogStore.js","sourceRoot":"","sources":["../../../../src/storage/local/LocalLogStore.ts"],"names":[],"mappings":"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"}
|