@devilsdev/rag-pipeline-utils 2.1.11 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ragrc.schema.json +218 -0
- package/README.md +651 -393
- package/bin/cli.js +8 -14
- package/dist/index.d.ts +237 -0
- package/dist/index.mjs +22 -7
- package/package.json +17 -13
- package/src/ai/adaptive-retrieval.js +191 -0
- package/src/ai/federated-learning.js +214 -0
- package/src/ai/federation/federated-learning-coordinator.js +345 -0
- package/src/ai/federation.js +76 -0
- package/src/ai/index-fixed.js +509 -0
- package/src/ai/index.js +33 -0
- package/src/ai/model-trainer.js +217 -0
- package/src/ai/model-training.js +205 -0
- package/src/ai/module-template.js +37 -0
- package/src/ai/multimodal/multi-modal-processor.js +345 -0
- package/src/ai/multimodal-processing.js +268 -0
- package/src/ai/multimodal.js +71 -0
- package/src/ai/orchestrator.js +70 -0
- package/src/ai/retrieval/adaptive-retrieval-engine.js +398 -0
- package/src/ai/retrieval-engine.js +69 -0
- package/src/ai/training/model-training-orchestrator.js +228 -0
- package/src/ci/workflow-analyzer.js +71 -0
- package/src/cli/commands/ai-ml.js +967 -0
- package/src/cli/commands/docs.js +228 -0
- package/src/cli/commands/dx.js +538 -0
- package/src/cli/commands/plugin-hub.js +875 -0
- package/src/cli/doctor-command.js +639 -0
- package/src/cli/enhanced-cli-commands.js +945 -0
- package/src/cli/interactive-wizard.js +997 -0
- package/src/cli/plugin-marketplace-commands.js +869 -0
- package/src/cli.js +17 -0
- package/src/config/enhanced-ragrc-schema.js +632 -0
- package/src/config/load-config.js +54 -0
- package/src/config/load-plugin-config.js +69 -0
- package/src/config/normalize-config.js +105 -0
- package/src/config/schema-validator.js +315 -0
- package/src/config/validate-plugin-schema.js +35 -0
- package/src/config/validate-schema.js +113 -0
- package/src/contracts/plugin-contracts.js +33 -0
- package/src/core/create-pipeline.js +99 -0
- package/src/core/dependency-injection.js +242 -0
- package/src/core/observability/event-logger.js +241 -0
- package/src/core/observability/instrumented-pipeline.js +609 -0
- package/src/core/observability/metrics.js +497 -0
- package/src/core/observability/tracing.js +781 -0
- package/src/core/performance/benchmark.js +516 -0
- package/src/core/performance/parallel-processor.js +240 -0
- package/src/core/performance/streaming-safeguards.js +611 -0
- package/src/core/pipeline-factory.js +60 -0
- package/src/core/plugin-contracts.js +89 -0
- package/src/core/plugin-marketplace/plugin-metadata.js +485 -0
- package/src/core/plugin-marketplace/plugin-publisher.js +574 -0
- package/src/core/plugin-marketplace/plugin-registry-format.js +419 -0
- package/src/core/plugin-marketplace/version-resolver.js +61 -0
- package/src/core/plugin-registry.js +265 -0
- package/src/dag/dag-engine.js +954 -0
- package/src/dag/dag-node.js +76 -0
- package/src/dag/enhanced-dag-engine.js +822 -0
- package/src/dx/index.js +40 -0
- package/src/dx/integration-templates.js +665 -0
- package/src/dx/performance-profiler.js +968 -0
- package/src/dx/realtime-debugger.js +755 -0
- package/src/dx/visual-pipeline-builder.js +578 -0
- package/src/ecosystem/plugin-analytics-dashboard.js +778 -0
- package/src/ecosystem/plugin-certification.js +580 -0
- package/src/ecosystem/plugin-hub.js +821 -0
- package/src/enterprise/audit-logging.js +967 -0
- package/src/enterprise/data-governance.js +408 -0
- package/src/enterprise/multi-tenancy.js +496 -0
- package/src/enterprise/sso-integration.js +903 -0
- package/src/evaluate/evaluator.js +52 -0
- package/src/evaluate/scoring.js +91 -0
- package/src/index.js +67 -0
- package/src/ingest.js +59 -0
- package/src/loader/csv-loader.js +38 -0
- package/src/loader/directory-loader.js +48 -0
- package/src/loader/html-loader.js +62 -0
- package/src/loader/markdown-loader.js +67 -0
- package/src/mocks/openai-embedder.js +119 -0
- package/src/mocks/openai-llm.js +77 -0
- package/src/mocks/pdf-loader.js +21 -0
- package/src/mocks/pinecone-retriever.js +106 -0
- package/src/observability/audit-logger.js +758 -0
- package/src/observability/dashboard-config.json +675 -0
- package/src/observability/event-logger.js +310 -0
- package/src/observability/metrics.js +691 -0
- package/src/observability/security-monitor.js +830 -0
- package/src/observability/slo-monitor.js +319 -0
- package/src/observability/telemetry.js +686 -0
- package/src/pipeline/pipeline.js +18 -0
- package/src/plugins/sample-embedder.js +53 -0
- package/src/plugins/sample-llm.js +30 -0
- package/src/plugins/sample-loader.js +24 -0
- package/src/plugins/sample-retriever.js +84 -0
- package/src/query.js +117 -0
- package/src/reranker/llm-reranker.js +85 -0
- package/src/scripts/script-utilities.js +36 -0
- package/src/security/plugin-sandbox.js +586 -0
- package/src/security/plugin-signature-verifier.js +414 -0
- package/src/security/validator.js +36 -0
- package/src/utils/ci/diagnostic-reporter.js +57 -0
- package/src/utils/logger.js +211 -0
- package/src/utils/performance-monitor.js +561 -0
- package/src/utils/plugin-scaffolder.js +447 -0
- package/src/utils/retry-policy.js +443 -0
- package/src/utils/retry.js +107 -0
- package/src/utils/safe-expression-evaluator.js +579 -0
- package/src/utils/structured-logger.js +361 -0
- package/src/utils/validate-plugin-contract.js +42 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enterprise Retry Policy System
|
|
3
|
+
* Provides exponential backoff, circuit breaker patterns, and retry budgets
|
|
4
|
+
*
|
|
5
|
+
* @author DevilsDev Team
|
|
6
|
+
* @since 2.1.8
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { logger } = require('./structured-logger.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Retry policy with exponential backoff and circuit breaker
|
|
13
|
+
*/
|
|
14
|
+
class RetryPolicy {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.maxRetries = options.maxRetries || 3;
|
|
17
|
+
this.baseDelay = options.baseDelay || 1000;
|
|
18
|
+
this.maxDelay = options.maxDelay || 30000;
|
|
19
|
+
this.multiplier = options.multiplier || 2;
|
|
20
|
+
this.jitter = options.jitter || 0.1;
|
|
21
|
+
this.timeout = options.timeout || 60000;
|
|
22
|
+
|
|
23
|
+
// Circuit breaker configuration
|
|
24
|
+
this.circuitBreaker = {
|
|
25
|
+
enabled: options.circuitBreaker?.enabled !== false,
|
|
26
|
+
failureThreshold: options.circuitBreaker?.failureThreshold || 5,
|
|
27
|
+
resetTimeout: options.circuitBreaker?.resetTimeout || 60000,
|
|
28
|
+
state: 'CLOSED', // CLOSED, OPEN, HALF_OPEN
|
|
29
|
+
failures: 0,
|
|
30
|
+
lastFailureTime: null,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Retry budget configuration
|
|
34
|
+
this.retryBudget = {
|
|
35
|
+
enabled: options.retryBudget?.enabled !== false,
|
|
36
|
+
maxRetriesPerMinute: options.retryBudget?.maxRetriesPerMinute || 60,
|
|
37
|
+
windowSize: options.retryBudget?.windowSize || 60000,
|
|
38
|
+
retries: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Strategy for determining retryable errors
|
|
42
|
+
this.retryCondition =
|
|
43
|
+
options.retryCondition || this.defaultRetryCondition.bind(this);
|
|
44
|
+
this.onRetry = options.onRetry || this.defaultOnRetry.bind(this);
|
|
45
|
+
this.onFailure = options.onFailure || this.defaultOnFailure.bind(this);
|
|
46
|
+
|
|
47
|
+
// Sleep function (injectable for testing)
|
|
48
|
+
this.sleep =
|
|
49
|
+
options.sleep ||
|
|
50
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Default retry condition - retry on network/timeout errors
|
|
55
|
+
* @param {Error} error - Error to evaluate
|
|
56
|
+
* @returns {boolean} Whether error is retryable
|
|
57
|
+
*/
|
|
58
|
+
defaultRetryCondition(error) {
|
|
59
|
+
// Retry on common transient errors
|
|
60
|
+
const retryableCodes = [
|
|
61
|
+
'ECONNRESET',
|
|
62
|
+
'ENOTFOUND',
|
|
63
|
+
'ECONNREFUSED',
|
|
64
|
+
'ETIMEDOUT',
|
|
65
|
+
];
|
|
66
|
+
const retryableMessages = ['timeout', 'network', 'connection', 'temporary'];
|
|
67
|
+
|
|
68
|
+
if (error.code && retryableCodes.includes(error.code)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const message = error.message.toLowerCase();
|
|
73
|
+
return retryableMessages.some((keyword) => message.includes(keyword));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Default retry callback
|
|
78
|
+
* @param {Error} error - Error that caused retry
|
|
79
|
+
* @param {number} attempt - Current attempt number
|
|
80
|
+
* @param {number} delay - Delay before next attempt
|
|
81
|
+
*/
|
|
82
|
+
defaultOnRetry(error, attempt, delay) {
|
|
83
|
+
logger.warn('Retrying operation', {
|
|
84
|
+
component: 'retry-policy',
|
|
85
|
+
attempt,
|
|
86
|
+
delay,
|
|
87
|
+
error: error.message,
|
|
88
|
+
errorCode: error.code,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default failure callback
|
|
94
|
+
* @param {Error} error - Final error after all retries
|
|
95
|
+
* @param {number} attempts - Total attempts made
|
|
96
|
+
*/
|
|
97
|
+
defaultOnFailure(error, attempts) {
|
|
98
|
+
logger.error('Operation failed after all retries', {
|
|
99
|
+
component: 'retry-policy',
|
|
100
|
+
attempts,
|
|
101
|
+
error: error.message,
|
|
102
|
+
errorCode: error.code,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check circuit breaker state
|
|
108
|
+
* @returns {boolean} Whether circuit is open (requests should fail fast)
|
|
109
|
+
*/
|
|
110
|
+
isCircuitOpen() {
|
|
111
|
+
if (!this.circuitBreaker.enabled) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { state, failures, failureThreshold, resetTimeout, lastFailureTime } =
|
|
116
|
+
this.circuitBreaker;
|
|
117
|
+
|
|
118
|
+
if (state === 'OPEN') {
|
|
119
|
+
// Check if we should transition to HALF_OPEN
|
|
120
|
+
if (Date.now() - lastFailureTime >= resetTimeout) {
|
|
121
|
+
this.circuitBreaker.state = 'HALF_OPEN';
|
|
122
|
+
logger.info('Circuit breaker transitioning to HALF_OPEN', {
|
|
123
|
+
component: 'circuit-breaker',
|
|
124
|
+
previousFailures: failures,
|
|
125
|
+
});
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return failures >= failureThreshold;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Record circuit breaker success
|
|
136
|
+
*/
|
|
137
|
+
recordCircuitSuccess() {
|
|
138
|
+
if (!this.circuitBreaker.enabled) return;
|
|
139
|
+
|
|
140
|
+
if (this.circuitBreaker.state === 'HALF_OPEN') {
|
|
141
|
+
this.circuitBreaker.state = 'CLOSED';
|
|
142
|
+
this.circuitBreaker.failures = 0;
|
|
143
|
+
logger.info('Circuit breaker closed after successful operation', {
|
|
144
|
+
component: 'circuit-breaker',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Record circuit breaker failure
|
|
151
|
+
*/
|
|
152
|
+
recordCircuitFailure() {
|
|
153
|
+
if (!this.circuitBreaker.enabled) return;
|
|
154
|
+
|
|
155
|
+
this.circuitBreaker.failures++;
|
|
156
|
+
this.circuitBreaker.lastFailureTime = Date.now();
|
|
157
|
+
|
|
158
|
+
if (this.circuitBreaker.failures >= this.circuitBreaker.failureThreshold) {
|
|
159
|
+
this.circuitBreaker.state = 'OPEN';
|
|
160
|
+
logger.warn('Circuit breaker opened due to excessive failures', {
|
|
161
|
+
component: 'circuit-breaker',
|
|
162
|
+
failures: this.circuitBreaker.failures,
|
|
163
|
+
threshold: this.circuitBreaker.failureThreshold,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check retry budget availability
|
|
170
|
+
* @returns {boolean} Whether retry budget allows retry
|
|
171
|
+
*/
|
|
172
|
+
checkRetryBudget() {
|
|
173
|
+
if (!this.retryBudget.enabled) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
const windowStart = now - this.retryBudget.windowSize;
|
|
179
|
+
|
|
180
|
+
// Clean old retries outside window
|
|
181
|
+
this.retryBudget.retries = this.retryBudget.retries.filter(
|
|
182
|
+
(time) => time > windowStart,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Check if we're under budget
|
|
186
|
+
const retriesInWindow = this.retryBudget.retries.length;
|
|
187
|
+
const budgetAvailable =
|
|
188
|
+
retriesInWindow < this.retryBudget.maxRetriesPerMinute;
|
|
189
|
+
|
|
190
|
+
if (!budgetAvailable) {
|
|
191
|
+
logger.warn('Retry budget exhausted', {
|
|
192
|
+
component: 'retry-budget',
|
|
193
|
+
retriesInWindow,
|
|
194
|
+
maxRetries: this.retryBudget.maxRetriesPerMinute,
|
|
195
|
+
windowSize: this.retryBudget.windowSize,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return budgetAvailable;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Record retry attempt in budget
|
|
204
|
+
*/
|
|
205
|
+
recordRetryAttempt() {
|
|
206
|
+
if (this.retryBudget.enabled) {
|
|
207
|
+
this.retryBudget.retries.push(Date.now());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Calculate delay for retry attempt with jitter
|
|
213
|
+
* @param {number} attempt - Current attempt number (0-based)
|
|
214
|
+
* @returns {number} Delay in milliseconds
|
|
215
|
+
*/
|
|
216
|
+
calculateDelay(attempt) {
|
|
217
|
+
const exponentialDelay =
|
|
218
|
+
this.baseDelay * Math.pow(this.multiplier, attempt);
|
|
219
|
+
const maxDelay = Math.min(exponentialDelay, this.maxDelay);
|
|
220
|
+
|
|
221
|
+
// Add jitter to prevent thundering herd
|
|
222
|
+
const jitterRange = maxDelay * this.jitter;
|
|
223
|
+
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
|
224
|
+
|
|
225
|
+
return Math.max(0, Math.floor(maxDelay + jitter));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Execute operation with retry logic
|
|
230
|
+
* @param {Function} operation - Operation to execute
|
|
231
|
+
* @param {object} context - Additional context for logging
|
|
232
|
+
* @returns {Promise} Operation result
|
|
233
|
+
*/
|
|
234
|
+
async execute(operation, context = {}) {
|
|
235
|
+
// Check circuit breaker
|
|
236
|
+
if (this.isCircuitOpen()) {
|
|
237
|
+
const error = new Error('Circuit breaker is open - failing fast');
|
|
238
|
+
error.code = 'CIRCUIT_OPEN';
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let lastError;
|
|
243
|
+
let attempt = 0;
|
|
244
|
+
|
|
245
|
+
const startTime = Date.now();
|
|
246
|
+
const operationTimeout = setTimeout(() => {
|
|
247
|
+
const timeoutError = new Error(
|
|
248
|
+
`Operation timeout after ${this.timeout}ms`,
|
|
249
|
+
);
|
|
250
|
+
timeoutError.code = 'OPERATION_TIMEOUT';
|
|
251
|
+
throw timeoutError;
|
|
252
|
+
}, this.timeout);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
while (attempt <= this.maxRetries) {
|
|
256
|
+
try {
|
|
257
|
+
const result = await operation();
|
|
258
|
+
|
|
259
|
+
// Record success for circuit breaker
|
|
260
|
+
this.recordCircuitSuccess();
|
|
261
|
+
|
|
262
|
+
logger.debug('Operation succeeded', {
|
|
263
|
+
component: 'retry-policy',
|
|
264
|
+
attempt,
|
|
265
|
+
duration: Date.now() - startTime,
|
|
266
|
+
...context,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return result;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
lastError = error;
|
|
272
|
+
attempt++;
|
|
273
|
+
|
|
274
|
+
// Check if error is retryable
|
|
275
|
+
if (!this.retryCondition(error)) {
|
|
276
|
+
logger.debug('Error is not retryable, failing immediately', {
|
|
277
|
+
component: 'retry-policy',
|
|
278
|
+
error: error.message,
|
|
279
|
+
errorCode: error.code,
|
|
280
|
+
...context,
|
|
281
|
+
});
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check if we have retries left
|
|
286
|
+
if (attempt > this.maxRetries) {
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check retry budget
|
|
291
|
+
if (!this.checkRetryBudget()) {
|
|
292
|
+
const budgetError = new Error('Retry budget exhausted');
|
|
293
|
+
budgetError.code = 'RETRY_BUDGET_EXHAUSTED';
|
|
294
|
+
throw budgetError;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Record retry attempt
|
|
298
|
+
this.recordRetryAttempt();
|
|
299
|
+
|
|
300
|
+
// Calculate delay and wait
|
|
301
|
+
const delay = this.calculateDelay(attempt - 1);
|
|
302
|
+
this.onRetry(error, attempt, delay);
|
|
303
|
+
|
|
304
|
+
await this.sleep(delay);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Record failure for circuit breaker
|
|
309
|
+
this.recordCircuitFailure();
|
|
310
|
+
|
|
311
|
+
// Call failure callback
|
|
312
|
+
this.onFailure(lastError, attempt);
|
|
313
|
+
|
|
314
|
+
// Enhance error with retry information
|
|
315
|
+
const enhancedError = new Error(
|
|
316
|
+
`Operation failed after ${attempt} attempts: ${lastError.message}`,
|
|
317
|
+
);
|
|
318
|
+
enhancedError.originalError = lastError;
|
|
319
|
+
enhancedError.attempts = attempt;
|
|
320
|
+
enhancedError.code = lastError.code || 'RETRY_EXHAUSTED';
|
|
321
|
+
|
|
322
|
+
throw enhancedError;
|
|
323
|
+
} finally {
|
|
324
|
+
clearTimeout(operationTimeout);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get retry policy metrics
|
|
330
|
+
* @returns {object} Policy metrics
|
|
331
|
+
*/
|
|
332
|
+
getMetrics() {
|
|
333
|
+
return {
|
|
334
|
+
circuitBreaker: {
|
|
335
|
+
state: this.circuitBreaker.state,
|
|
336
|
+
failures: this.circuitBreaker.failures,
|
|
337
|
+
lastFailureTime: this.circuitBreaker.lastFailureTime,
|
|
338
|
+
},
|
|
339
|
+
retryBudget: {
|
|
340
|
+
currentRetries: this.retryBudget.retries.length,
|
|
341
|
+
maxRetries: this.retryBudget.maxRetriesPerMinute,
|
|
342
|
+
windowSize: this.retryBudget.windowSize,
|
|
343
|
+
},
|
|
344
|
+
policy: {
|
|
345
|
+
maxRetries: this.maxRetries,
|
|
346
|
+
baseDelay: this.baseDelay,
|
|
347
|
+
maxDelay: this.maxDelay,
|
|
348
|
+
multiplier: this.multiplier,
|
|
349
|
+
jitter: this.jitter,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Reset retry policy state
|
|
356
|
+
*/
|
|
357
|
+
reset() {
|
|
358
|
+
this.circuitBreaker.state = 'CLOSED';
|
|
359
|
+
this.circuitBreaker.failures = 0;
|
|
360
|
+
this.circuitBreaker.lastFailureTime = null;
|
|
361
|
+
this.retryBudget.retries = [];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Create retry policy instance
|
|
367
|
+
* @param {object} options - Policy configuration
|
|
368
|
+
* @returns {RetryPolicy} Retry policy instance
|
|
369
|
+
*/
|
|
370
|
+
function createRetryPolicy(options = {}) {
|
|
371
|
+
return new RetryPolicy(options);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Default retry policy for most operations
|
|
376
|
+
*/
|
|
377
|
+
const defaultPolicy = createRetryPolicy({
|
|
378
|
+
maxRetries: 3,
|
|
379
|
+
baseDelay: 1000,
|
|
380
|
+
maxDelay: 10000,
|
|
381
|
+
multiplier: 2,
|
|
382
|
+
jitter: 0.1,
|
|
383
|
+
circuitBreaker: {
|
|
384
|
+
enabled: true,
|
|
385
|
+
failureThreshold: 5,
|
|
386
|
+
resetTimeout: 30000,
|
|
387
|
+
},
|
|
388
|
+
retryBudget: {
|
|
389
|
+
enabled: true,
|
|
390
|
+
maxRetriesPerMinute: 30,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Aggressive retry policy for critical operations
|
|
396
|
+
*/
|
|
397
|
+
const aggressivePolicy = createRetryPolicy({
|
|
398
|
+
maxRetries: 5,
|
|
399
|
+
baseDelay: 500,
|
|
400
|
+
maxDelay: 30000,
|
|
401
|
+
multiplier: 2.5,
|
|
402
|
+
jitter: 0.2,
|
|
403
|
+
circuitBreaker: {
|
|
404
|
+
enabled: true,
|
|
405
|
+
failureThreshold: 10,
|
|
406
|
+
resetTimeout: 60000,
|
|
407
|
+
},
|
|
408
|
+
retryBudget: {
|
|
409
|
+
enabled: true,
|
|
410
|
+
maxRetriesPerMinute: 60,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Conservative retry policy for non-critical operations
|
|
416
|
+
*/
|
|
417
|
+
const conservativePolicy = createRetryPolicy({
|
|
418
|
+
maxRetries: 2,
|
|
419
|
+
baseDelay: 2000,
|
|
420
|
+
maxDelay: 15000,
|
|
421
|
+
multiplier: 1.5,
|
|
422
|
+
jitter: 0.05,
|
|
423
|
+
circuitBreaker: {
|
|
424
|
+
enabled: true,
|
|
425
|
+
failureThreshold: 3,
|
|
426
|
+
resetTimeout: 45000,
|
|
427
|
+
},
|
|
428
|
+
retryBudget: {
|
|
429
|
+
enabled: true,
|
|
430
|
+
maxRetriesPerMinute: 15,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
module.exports = {
|
|
435
|
+
RetryPolicy,
|
|
436
|
+
createRetryPolicy,
|
|
437
|
+
defaultPolicy,
|
|
438
|
+
aggressivePolicy,
|
|
439
|
+
conservativePolicy,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// CommonJS/ESM interop
|
|
443
|
+
module.exports.default = module.exports;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 0.2.0
|
|
3
|
+
* Path: /src/utils/retry.js
|
|
4
|
+
* Description: Generic retry wrapper with exponential backoff and injectable sleep for testing
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { logger } = require('./logger.js');
|
|
9
|
+
|
|
10
|
+
// Keep a default sleeper that tests can override via options
|
|
11
|
+
const defaultSleep = (ms) =>
|
|
12
|
+
new Promise((res) => {
|
|
13
|
+
const t = setTimeout(res, ms);
|
|
14
|
+
if (typeof t.unref === 'function') t.unref();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retry a function with exponential backoff.
|
|
19
|
+
*
|
|
20
|
+
* @param {Function} fn - async or sync function to call; receives (attempt)
|
|
21
|
+
* @param {Object} [opts]
|
|
22
|
+
* @param {number} [opts.retries=3] - number of *retries* (total attempts = retries + 1)
|
|
23
|
+
* @param {number} [opts.baseDelay=100] - initial delay in ms
|
|
24
|
+
* @param {number} [opts.multiplier=2] - backoff multiplier
|
|
25
|
+
* @param {boolean} [opts.jitter=false] - add +/-50% jitter if true
|
|
26
|
+
* @param {Function} [opts.sleep=defaultSleep] - injected sleeper for tests
|
|
27
|
+
* @param {Function} [opts.onDelay] - callback (delayMs, attempt, error) before each sleep
|
|
28
|
+
* @returns {Promise<*>}
|
|
29
|
+
*/
|
|
30
|
+
async function retry(fn, opts = {}) {
|
|
31
|
+
const {
|
|
32
|
+
retries = 3,
|
|
33
|
+
baseDelay = 100,
|
|
34
|
+
multiplier = 2,
|
|
35
|
+
jitter = false,
|
|
36
|
+
sleep: injectedSleep,
|
|
37
|
+
onDelay,
|
|
38
|
+
} = opts;
|
|
39
|
+
|
|
40
|
+
const testNoopSleep = () => Promise.resolve();
|
|
41
|
+
const sleep =
|
|
42
|
+
injectedSleep ??
|
|
43
|
+
(process.env.NODE_ENV === 'test'
|
|
44
|
+
? testNoopSleep
|
|
45
|
+
: (ms) =>
|
|
46
|
+
new Promise((r) => {
|
|
47
|
+
const t = setTimeout(r, ms);
|
|
48
|
+
if (typeof t.unref === 'function') t.unref();
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
let attempt = 0; // 0-based attempt index
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line no-constant-condition
|
|
54
|
+
while (true) {
|
|
55
|
+
try {
|
|
56
|
+
return await fn(attempt);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (attempt >= retries) throw err;
|
|
59
|
+
|
|
60
|
+
// exponential backoff: base * multiplier^attempt
|
|
61
|
+
const backoff = baseDelay * Math.pow(multiplier, attempt);
|
|
62
|
+
const delay = jitter
|
|
63
|
+
? Math.round(backoff * (0.5 + Math.random())) // 50%..150%
|
|
64
|
+
: backoff;
|
|
65
|
+
|
|
66
|
+
if (typeof onDelay === 'function') {
|
|
67
|
+
onDelay(delay, attempt, err);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// IMPORTANT: call the *injected* sleeper so tests can spy on it
|
|
71
|
+
await sleep(delay);
|
|
72
|
+
|
|
73
|
+
attempt += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Legacy wrapper for backward compatibility
|
|
79
|
+
async function withRetry(
|
|
80
|
+
_fn,
|
|
81
|
+
{ retries = 3, initialDelay = 300, label = 'operation' } = {},
|
|
82
|
+
) {
|
|
83
|
+
return retry(_fn, {
|
|
84
|
+
retries,
|
|
85
|
+
baseDelay: initialDelay,
|
|
86
|
+
onDelay: (delay, attempt, err) => {
|
|
87
|
+
if (attempt > 0) {
|
|
88
|
+
logger.warn(
|
|
89
|
+
{ attempt: attempt + 1, label },
|
|
90
|
+
`Retrying ${label} (attempt ${attempt + 1})`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (attempt >= retries - 1) {
|
|
94
|
+
logger.error(
|
|
95
|
+
{ label, error: err.message },
|
|
96
|
+
`Failed ${label} after ${retries} retries`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
retry,
|
|
105
|
+
withRetry,
|
|
106
|
+
defaultSleep,
|
|
107
|
+
};
|