@devilsdev/rag-pipeline-utils 2.2.1 → 2.3.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 +244 -11
- package/contracts/contract-schema.json +189 -0
- package/dist/index.cjs +19 -0
- package/dist/index.mjs +15 -1
- package/package.json +3 -2
- package/src/cli/commands/wizard.js +627 -0
- package/src/cli/input-sanitization-middleware.js +356 -0
- package/src/cli/templates/development.template.json +64 -0
- package/src/cli/templates/minimal.template.json +33 -0
- package/src/cli/templates/production.template.json +104 -0
- package/src/cli/templates/testing.template.json +45 -0
- package/src/config/connection-pool-config.json +259 -0
- package/src/config/memory-optimization.js +414 -0
- package/src/config/redaction-patterns.json +150 -0
- package/src/config/sanitization-rules.json +323 -0
- package/src/core/plugin-registry.js +266 -20
- package/src/dag/core/error-context.js +391 -0
- package/src/dag/core/execution-scheduler.js +350 -0
- package/src/dag/core/topology-validation.js +415 -0
- package/src/dag/dag-engine.js +26 -424
- package/src/dev/README.md +126 -0
- package/src/dev/dev-server.js +356 -0
- package/src/dev/examples/hot-reload-usage.js +355 -0
- package/src/dev/hot-reload.js +807 -0
- package/src/enterprise/sso-integration.js +67 -25
- package/src/index.js +19 -0
- package/src/observability/event-logger.js +62 -2
- package/src/plugins/optimized-openai-embedder.js +169 -0
- package/src/security/jwt-config.js +431 -0
- package/src/security/jwt-validator.js +701 -0
- package/src/utils/api-client-pool-integration.js +436 -0
- package/src/utils/batch-processor.js +710 -0
- package/src/utils/connection-pool.js +605 -0
- package/src/utils/contract-validator.js +460 -0
- package/src/utils/error-formatter.js +603 -0
- package/src/utils/examples/error-formatter-usage.js +351 -0
- package/src/utils/input-sanitizer.js +967 -0
- package/src/utils/memory-monitor.js +800 -0
- package/src/utils/secure-logger-config.js +352 -0
- package/src/utils/secure-logger.js +576 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DAG Execution Scheduler Module
|
|
3
|
+
*
|
|
4
|
+
* Provides execution scheduling, concurrency management, and timeout handling
|
|
5
|
+
* for DAG node execution. Handles retry logic, error reporting, and dependency
|
|
6
|
+
* resolution.
|
|
7
|
+
*
|
|
8
|
+
* @module dag/core/execution-scheduler
|
|
9
|
+
* @author Ali Kahwaji
|
|
10
|
+
* @since 2.2.3
|
|
11
|
+
* @version 1.0.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const ErrorContext = require('./error-context.js');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ExecutionScheduler class for managing DAG node execution
|
|
20
|
+
*
|
|
21
|
+
* Provides stateless execution scheduling with:
|
|
22
|
+
* - Concurrency control via semaphore
|
|
23
|
+
* - Timeout enforcement
|
|
24
|
+
* - Retry logic with exponential backoff
|
|
25
|
+
* - Dependency resolution
|
|
26
|
+
* - Error handling and reporting
|
|
27
|
+
*
|
|
28
|
+
* @class
|
|
29
|
+
* @since 2.2.3
|
|
30
|
+
*/
|
|
31
|
+
class ExecutionScheduler {
|
|
32
|
+
/**
|
|
33
|
+
* Create an ExecutionScheduler instance
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} options - Scheduler configuration
|
|
36
|
+
* @param {Function} options.getSinkIds - Function to get sink node IDs
|
|
37
|
+
* @param {Object} [options.logger] - Logger instance for debugging
|
|
38
|
+
*/
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.getSinkIds = options.getSinkIds;
|
|
41
|
+
this.logger = options.logger || console;
|
|
42
|
+
this.errorContext = new ErrorContext();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Schedule and execute all nodes with concurrency management
|
|
47
|
+
*
|
|
48
|
+
* Executes nodes in topological order, respecting dependencies and
|
|
49
|
+
* managing concurrency via semaphore. Each node is executed only after
|
|
50
|
+
* all its dependencies have completed or failed.
|
|
51
|
+
*
|
|
52
|
+
* @param {DAGNode[]} order - Topologically sorted array of nodes
|
|
53
|
+
* @param {Object} context - Execution context
|
|
54
|
+
* @param {number} [context.concurrency] - Maximum concurrent executions
|
|
55
|
+
* @param {Map} context.results - Results map (node ID -> result)
|
|
56
|
+
* @param {Map} context.errors - Errors map (node ID -> error)
|
|
57
|
+
* @param {Map} context.fwd - Forward adjacency map
|
|
58
|
+
* @param {boolean} [context.continueOnError] - Continue on non-critical errors
|
|
59
|
+
* @param {Set} context.requiredIds - Set of required node IDs
|
|
60
|
+
* @param {any} context.seed - Initial seed value
|
|
61
|
+
* @param {boolean} [context.gracefulDegradation] - Enable graceful degradation
|
|
62
|
+
* @param {string[]} [context.requiredNodes] - Array of required node IDs
|
|
63
|
+
* @param {boolean} [context.retryFailedNodes] - Enable retry for failed nodes
|
|
64
|
+
* @param {number} [context.maxRetries] - Maximum retry attempts
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const scheduler = new ExecutionScheduler({ getSinkIds });
|
|
69
|
+
* await scheduler.scheduleExecution(sortedNodes, {
|
|
70
|
+
* concurrency: 5,
|
|
71
|
+
* results: new Map(),
|
|
72
|
+
* errors: new Map(),
|
|
73
|
+
* fwd: adjacencyMap,
|
|
74
|
+
* requiredIds: new Set(['critical-node']),
|
|
75
|
+
* seed: initialInput
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* @performance O(V) where V = number of nodes
|
|
79
|
+
* @since 2.2.3
|
|
80
|
+
*/
|
|
81
|
+
async scheduleExecution(order, context) {
|
|
82
|
+
const { concurrency, results, errors, fwd } = context;
|
|
83
|
+
|
|
84
|
+
const semaphore = concurrency ? new Array(concurrency).fill(null) : null;
|
|
85
|
+
let semaphoreIndex = 0;
|
|
86
|
+
|
|
87
|
+
const executeWithSemaphore = async (node) => {
|
|
88
|
+
if (semaphore) {
|
|
89
|
+
// Wait for available slot
|
|
90
|
+
while (semaphore[semaphoreIndex % semaphore.length] !== null) {
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
92
|
+
}
|
|
93
|
+
const slotIndex = semaphoreIndex % semaphore.length;
|
|
94
|
+
semaphoreIndex++;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const result = await this.executeNode(node, context);
|
|
98
|
+
// Only set result if it's not undefined (failed optional nodes return undefined)
|
|
99
|
+
if (result !== undefined) {
|
|
100
|
+
results.set(node.id, result);
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
semaphore[slotIndex] = null;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
const result = await this.executeNode(node, context);
|
|
107
|
+
// Only set result if it's not the special failed-optional-node symbol
|
|
108
|
+
if (
|
|
109
|
+
typeof result !== 'symbol' ||
|
|
110
|
+
result.description !== 'failed-optional-node'
|
|
111
|
+
) {
|
|
112
|
+
results.set(node.id, result);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Execute nodes respecting dependencies
|
|
118
|
+
for (const node of order) {
|
|
119
|
+
// Check if all dependencies are satisfied
|
|
120
|
+
const canExecute = this.canExecuteNode(node, context);
|
|
121
|
+
|
|
122
|
+
if (canExecute) {
|
|
123
|
+
await executeWithSemaphore(node);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Execute a single DAG node with retry logic and error handling
|
|
130
|
+
*
|
|
131
|
+
* Executes node.run() with comprehensive error handling:
|
|
132
|
+
* - Validates node.run is a function
|
|
133
|
+
* - Determines node criticality
|
|
134
|
+
* - Prepares input from dependencies
|
|
135
|
+
* - Implements retry logic with configurable attempts
|
|
136
|
+
* - Reports errors with downstream impact analysis
|
|
137
|
+
*
|
|
138
|
+
* @param {DAGNode} node - Node to execute
|
|
139
|
+
* @param {Object} context - Execution context
|
|
140
|
+
* @param {Map} context.results - Results map
|
|
141
|
+
* @param {Map} context.errors - Errors map
|
|
142
|
+
* @param {Map} context.fwd - Forward adjacency map
|
|
143
|
+
* @param {boolean} [context.continueOnError] - Continue on non-critical errors
|
|
144
|
+
* @param {Set} context.requiredIds - Required node IDs
|
|
145
|
+
* @param {any} context.seed - Seed value for source nodes
|
|
146
|
+
* @param {boolean} [context.gracefulDegradation] - Enable graceful degradation
|
|
147
|
+
* @param {string[]} [context.requiredNodes] - Required node IDs array
|
|
148
|
+
* @param {boolean} [context.retryFailedNodes] - Enable retry
|
|
149
|
+
* @param {number} [context.maxRetries] - Max retry attempts
|
|
150
|
+
* @returns {Promise<any>} Node execution result or Symbol('failed-optional-node')
|
|
151
|
+
*
|
|
152
|
+
* @throws {Error} Node execution error (if node is critical)
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* const result = await scheduler.executeNode(myNode, {
|
|
156
|
+
* results: new Map(),
|
|
157
|
+
* errors: new Map(),
|
|
158
|
+
* fwd: adjacencyMap,
|
|
159
|
+
* requiredIds: new Set(),
|
|
160
|
+
* seed: initialData,
|
|
161
|
+
* retryFailedNodes: true,
|
|
162
|
+
* maxRetries: 3
|
|
163
|
+
* });
|
|
164
|
+
*
|
|
165
|
+
* @performance O(1) for single execution, O(k) for k retry attempts
|
|
166
|
+
* @complexity Cyclomatic complexity: 12 (acceptable for critical execution path)
|
|
167
|
+
* @since 2.2.3
|
|
168
|
+
*/
|
|
169
|
+
async executeNode(node, context) {
|
|
170
|
+
const {
|
|
171
|
+
results,
|
|
172
|
+
errors,
|
|
173
|
+
fwd,
|
|
174
|
+
continueOnError,
|
|
175
|
+
requiredIds,
|
|
176
|
+
seed,
|
|
177
|
+
gracefulDegradation,
|
|
178
|
+
requiredNodes,
|
|
179
|
+
} = context;
|
|
180
|
+
|
|
181
|
+
// Handle undefined node.run function
|
|
182
|
+
if (typeof node.run !== 'function') {
|
|
183
|
+
throw new Error(`Node ${node.id} has no run function`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Determine criticality - default is critical unless explicitly marked optional
|
|
187
|
+
// OR if gracefulDegradation is enabled and node is not in requiredNodes
|
|
188
|
+
// OR if there are multiple sink nodes (individual sink failures should be non-critical)
|
|
189
|
+
const sinkIds = this.getSinkIds(fwd);
|
|
190
|
+
const isMultipleSinks = sinkIds.length > 1;
|
|
191
|
+
const isSinkNode = sinkIds.includes(node.id);
|
|
192
|
+
|
|
193
|
+
const isNonCritical =
|
|
194
|
+
node?.optional === true ||
|
|
195
|
+
node?.isOptional === true ||
|
|
196
|
+
node?.critical === false ||
|
|
197
|
+
(gracefulDegradation &&
|
|
198
|
+
requiredNodes &&
|
|
199
|
+
!requiredNodes.includes(node.id)) ||
|
|
200
|
+
(isMultipleSinks && isSinkNode); // Sink nodes are non-critical when there are multiple sinks
|
|
201
|
+
|
|
202
|
+
// Prepare input based on dependencies
|
|
203
|
+
let input;
|
|
204
|
+
if (node.inputs.length === 0) {
|
|
205
|
+
input = seed;
|
|
206
|
+
} else if (node.inputs.length === 1) {
|
|
207
|
+
input = results.get(node.inputs[0].id);
|
|
208
|
+
} else {
|
|
209
|
+
input = node.inputs.map((inputNode) => {
|
|
210
|
+
// For graceful degradation, return undefined for failed optional dependencies
|
|
211
|
+
if (
|
|
212
|
+
!results.has(inputNode.id) &&
|
|
213
|
+
gracefulDegradation &&
|
|
214
|
+
requiredNodes &&
|
|
215
|
+
!requiredNodes.includes(inputNode.id)
|
|
216
|
+
) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
return results.get(inputNode.id);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Retry logic - use global options if available, otherwise node-specific
|
|
224
|
+
const { retryFailedNodes, maxRetries: globalMaxRetries } = context;
|
|
225
|
+
const nodeMaxRetries = retryFailedNodes
|
|
226
|
+
? Math.max(0, globalMaxRetries ?? 3)
|
|
227
|
+
: Math.max(0, node?.retry?.retries ?? 0);
|
|
228
|
+
const delayMs = Math.max(0, node?.retry?.delayMs ?? 0);
|
|
229
|
+
let attempt = 0;
|
|
230
|
+
let keepRetrying = true;
|
|
231
|
+
|
|
232
|
+
while (keepRetrying) {
|
|
233
|
+
try {
|
|
234
|
+
// Execute the node
|
|
235
|
+
const output = await node.run(input);
|
|
236
|
+
if (errors.has(node.id)) errors.delete(node.id);
|
|
237
|
+
return output;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
attempt += 1;
|
|
240
|
+
if (attempt <= nodeMaxRetries) {
|
|
241
|
+
if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
|
|
242
|
+
continue; // no error recorded yet
|
|
243
|
+
}
|
|
244
|
+
keepRetrying = false;
|
|
245
|
+
// FINAL failure path
|
|
246
|
+
const deps = [...(fwd.get(node.id) || [])];
|
|
247
|
+
const nodeErr = this.errorContext.createErrorContext(node.id, e, {
|
|
248
|
+
downstream: deps,
|
|
249
|
+
timestamp: Date.now(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Handle non-critical failures with warnings and no result nullification
|
|
253
|
+
if (isNonCritical || (continueOnError && !requiredIds.has(node.id))) {
|
|
254
|
+
console.warn(`Non-critical node failure: ${nodeErr.message}`);
|
|
255
|
+
errors.set(node.id, nodeErr);
|
|
256
|
+
// Don't set result for failed optional nodes - return special symbol so they don't appear in results
|
|
257
|
+
return Symbol('failed-optional-node');
|
|
258
|
+
}
|
|
259
|
+
errors.set(node.id, nodeErr);
|
|
260
|
+
throw nodeErr;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Execute function with optional timeout enforcement
|
|
267
|
+
*
|
|
268
|
+
* Uses Promise.race to enforce execution timeout. If timeout is not specified
|
|
269
|
+
* or is <= 0, executes without timeout constraint.
|
|
270
|
+
*
|
|
271
|
+
* @param {Function} runFn - Async function to execute
|
|
272
|
+
* @param {number} [timeout] - Timeout in milliseconds (0 or undefined = no timeout)
|
|
273
|
+
* @returns {Promise<void>}
|
|
274
|
+
*
|
|
275
|
+
* @throws {Error} 'Execution timeout' if execution exceeds timeout
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* await scheduler.executeWithTimeout(
|
|
279
|
+
* () => executeLongRunningTask(),
|
|
280
|
+
* 30000 // 30 second timeout
|
|
281
|
+
* );
|
|
282
|
+
*
|
|
283
|
+
* @performance O(1) overhead for timeout setup
|
|
284
|
+
* @since 2.2.3
|
|
285
|
+
*/
|
|
286
|
+
async executeWithTimeout(runFn, timeout) {
|
|
287
|
+
if (timeout && timeout > 0) {
|
|
288
|
+
await Promise.race([
|
|
289
|
+
runFn(),
|
|
290
|
+
new Promise((_, reject) => {
|
|
291
|
+
setTimeout(() => reject(new Error('Execution timeout')), timeout);
|
|
292
|
+
}),
|
|
293
|
+
]);
|
|
294
|
+
} else {
|
|
295
|
+
await runFn();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if a node can be executed based on dependency satisfaction
|
|
301
|
+
*
|
|
302
|
+
* A node can execute if all its input dependencies have either:
|
|
303
|
+
* - Completed successfully (have a result)
|
|
304
|
+
* - Failed (have an error)
|
|
305
|
+
* - Are optional (graceful degradation enabled and not in required nodes)
|
|
306
|
+
*
|
|
307
|
+
* @param {DAGNode} node - Node to check
|
|
308
|
+
* @param {Object} context - Execution context
|
|
309
|
+
* @param {Map} context.results - Results map
|
|
310
|
+
* @param {Map} context.errors - Errors map
|
|
311
|
+
* @param {boolean} [context.gracefulDegradation] - Enable graceful degradation
|
|
312
|
+
* @param {string[]} [context.requiredNodes] - Required node IDs
|
|
313
|
+
* @returns {boolean} True if node can execute, false otherwise
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* const canRun = scheduler.canExecuteNode(myNode, {
|
|
317
|
+
* results: new Map([['dep1', result1]]),
|
|
318
|
+
* errors: new Map(),
|
|
319
|
+
* gracefulDegradation: true,
|
|
320
|
+
* requiredNodes: ['critical-dep']
|
|
321
|
+
* });
|
|
322
|
+
*
|
|
323
|
+
* @performance O(d) where d = number of input dependencies
|
|
324
|
+
* @since 2.2.3
|
|
325
|
+
*/
|
|
326
|
+
canExecuteNode(node, context) {
|
|
327
|
+
const { results, errors, gracefulDegradation, requiredNodes } = context;
|
|
328
|
+
|
|
329
|
+
return node.inputs.every((dep) => {
|
|
330
|
+
// If dependency has result or error, it's satisfied
|
|
331
|
+
if (results.has(dep.id) || errors.has(dep.id)) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
// If graceful degradation is enabled and dependency is not required, it's satisfied
|
|
335
|
+
if (
|
|
336
|
+
gracefulDegradation &&
|
|
337
|
+
requiredNodes &&
|
|
338
|
+
!requiredNodes.includes(dep.id)
|
|
339
|
+
) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = ExecutionScheduler;
|
|
348
|
+
|
|
349
|
+
// Default export for convenience
|
|
350
|
+
module.exports.default = ExecutionScheduler;
|