@goodandready/dsh-cron 0.3.1 → 0.3.2
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/lib/chain-checkpoints.js +109 -11
- package/lib/client.js +4350 -4199
- package/lib/dag.js +330 -0
- package/lib/prompt-interpolation.js +29 -3
- package/lib/routes.js +22 -0
- package/lib/scheduler-execution.js +82 -15
- package/lib/task-create.js +11 -1
- package/lib/task-patch.js +13 -0
- package/lib/task-transfer.js +2 -0
- package/package.json +1 -1
package/lib/chain-checkpoints.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Chain Checkpoint
|
|
2
|
+
* Chain Checkpoint, Step Replay, and DAG Barrier Manager (#319, #328)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { sanitizeDependsOn, sanitizeJoinPolicy } from './dag.js';
|
|
6
|
+
|
|
5
7
|
export class ChainCheckpointManager {
|
|
6
8
|
constructor(store) {
|
|
7
9
|
this.store = store;
|
|
8
10
|
this.activeRetries = new Set();
|
|
11
|
+
this.triggeredBarriers = new Set();
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -48,6 +51,86 @@ export class ChainCheckpointManager {
|
|
|
48
51
|
return this.store.chainCheckpoints[chainRunId] || [];
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether all prerequisites for a task are satisfied (DAG Join Barrier, #328).
|
|
56
|
+
*
|
|
57
|
+
* @param {string} chainRunId
|
|
58
|
+
* @param {Object} task
|
|
59
|
+
* @returns {{ ready: boolean, pending?: boolean, failed?: boolean, outputs: Record<string, string>, latestOutput: string, waitingFor?: string[], failedDep?: string }}
|
|
60
|
+
*/
|
|
61
|
+
checkJoinBarrier(chainRunId, task) {
|
|
62
|
+
if (!task) return { ready: false, outputs: {}, latestOutput: '' };
|
|
63
|
+
const deps = sanitizeDependsOn(task.dependsOn);
|
|
64
|
+
if (!deps.length) {
|
|
65
|
+
return { ready: true, outputs: {}, latestOutput: '' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const snapshots = this.getChainSnapshots(chainRunId);
|
|
69
|
+
const snapMap = new Map();
|
|
70
|
+
for (const s of snapshots) {
|
|
71
|
+
if (s?.stepTaskId) snapMap.set(s.stepTaskId, s);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const policy = sanitizeJoinPolicy(task.joinPolicy);
|
|
75
|
+
const outputs = {};
|
|
76
|
+
let latestOutput = '';
|
|
77
|
+
const waitingFor = [];
|
|
78
|
+
let hasSuccess = false;
|
|
79
|
+
let failedDep = null;
|
|
80
|
+
|
|
81
|
+
for (const depId of deps) {
|
|
82
|
+
const snap = snapMap.get(depId);
|
|
83
|
+
if (!snap) {
|
|
84
|
+
waitingFor.push(depId);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const isOk = snap.status === 'completed' || snap.status === 'success';
|
|
88
|
+
if (isOk) {
|
|
89
|
+
hasSuccess = true;
|
|
90
|
+
outputs[depId] = String(snap.output || '');
|
|
91
|
+
latestOutput = String(snap.output || '');
|
|
92
|
+
} else {
|
|
93
|
+
failedDep = depId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (policy === 'any_success') {
|
|
98
|
+
if (hasSuccess) {
|
|
99
|
+
return { ready: true, outputs, latestOutput };
|
|
100
|
+
}
|
|
101
|
+
if (waitingFor.length === 0) {
|
|
102
|
+
return { ready: false, failed: true, failedDep: failedDep || deps[0], outputs, latestOutput };
|
|
103
|
+
}
|
|
104
|
+
return { ready: false, pending: true, waitingFor, outputs, latestOutput };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Default: 'all_success'
|
|
108
|
+
if (waitingFor.length > 0) {
|
|
109
|
+
return { ready: false, pending: true, waitingFor, outputs, latestOutput };
|
|
110
|
+
}
|
|
111
|
+
if (failedDep) {
|
|
112
|
+
return { ready: false, failed: true, failedDep, outputs, latestOutput };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { ready: true, outputs, latestOutput };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Barrier idempotency guard: returns true if barrier has not yet fired for this task in this run.
|
|
120
|
+
*/
|
|
121
|
+
canTriggerBarrier(chainRunId, taskId) {
|
|
122
|
+
if (!chainRunId || !taskId) return false;
|
|
123
|
+
return !this.triggeredBarriers.has(`${chainRunId}:${taskId}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Marks a barrier as triggered for this chain run to prevent duplicate executions.
|
|
128
|
+
*/
|
|
129
|
+
markBarrierTriggered(chainRunId, taskId) {
|
|
130
|
+
if (!chainRunId || !taskId) return;
|
|
131
|
+
this.triggeredBarriers.add(`${chainRunId}:${taskId}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
51
134
|
/**
|
|
52
135
|
* Checks whether a step can be retried in a chain run
|
|
53
136
|
*/
|
|
@@ -78,7 +161,7 @@ export class ChainCheckpointManager {
|
|
|
78
161
|
}
|
|
79
162
|
|
|
80
163
|
/**
|
|
81
|
-
* Retries chain execution from the specified failed step (#319)
|
|
164
|
+
* Retries chain execution from the specified failed step (#319, #328)
|
|
82
165
|
* Reuses upstream outputs without re-executing successful upstream steps.
|
|
83
166
|
*/
|
|
84
167
|
async retryChainFromStep(scheduler, chainRunId, stepTaskId) {
|
|
@@ -90,25 +173,40 @@ export class ChainCheckpointManager {
|
|
|
90
173
|
|
|
91
174
|
try {
|
|
92
175
|
const snapshots = this.getChainSnapshots(chainRunId);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
176
|
+
|
|
177
|
+
// Collect upstream outputs from checkpoints
|
|
178
|
+
const prevOutputs = {};
|
|
179
|
+
let prevOutput = '';
|
|
180
|
+
let prevTaskId = '';
|
|
181
|
+
|
|
182
|
+
for (const s of snapshots) {
|
|
183
|
+
if (s.stepTaskId !== stepTaskId && (s.status === 'success' || s.status === 'completed')) {
|
|
184
|
+
prevOutputs[s.stepTaskId] = String(s.output || '');
|
|
185
|
+
prevOutput = String(s.output || '');
|
|
186
|
+
prevTaskId = s.stepTaskId;
|
|
100
187
|
}
|
|
101
188
|
}
|
|
102
189
|
|
|
103
|
-
|
|
104
|
-
|
|
190
|
+
// Reset barrier locks for stepTaskId and downstream dependents so they can re-trigger
|
|
191
|
+
this.triggeredBarriers.delete(`${chainRunId}:${stepTaskId}`);
|
|
192
|
+
if (this.store?.tasks) {
|
|
193
|
+
for (const t of Object.values(this.store.tasks)) {
|
|
194
|
+
const deps = sanitizeDependsOn(t?.dependsOn);
|
|
195
|
+
if (deps.includes(stepTaskId)) {
|
|
196
|
+
this.triggeredBarriers.delete(`${chainRunId}:${t.id}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
105
200
|
|
|
106
201
|
// Execute only the failed step, passing reconstructed chain context
|
|
107
202
|
const runFn = (scheduler.runTask || scheduler.runNow).bind(scheduler);
|
|
108
203
|
const res = await runFn(stepTaskId, {
|
|
109
204
|
chainRunId,
|
|
205
|
+
prevOutputs,
|
|
110
206
|
chainPrevOutput: prevOutput,
|
|
207
|
+
prevOutput,
|
|
111
208
|
chainPrevTaskId: prevTaskId,
|
|
209
|
+
prevTaskId,
|
|
112
210
|
isRetry: true,
|
|
113
211
|
});
|
|
114
212
|
|