@unrdf/self-healing-workflows 26.4.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/README.md +284 -0
- package/examples/basic-usage.mjs +99 -0
- package/examples/recovery-strategies.mjs +142 -0
- package/package.json +46 -0
- package/src/circuit-breaker.mjs +262 -0
- package/src/error-classifier.mjs +203 -0
- package/src/health-monitor.mjs +301 -0
- package/src/index.mjs +46 -0
- package/src/recovery-actions.mjs +272 -0
- package/src/retry-strategy.mjs +241 -0
- package/src/schemas.mjs +185 -0
- package/src/self-healing-engine.mjs +354 -0
- package/test/self-healing.test.mjs +772 -0
- package/vitest.config.mjs +20 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Circuit breaker pattern implementation
|
|
3
|
+
* @module @unrdf/self-healing-workflows/circuit-breaker
|
|
4
|
+
* @description Implements circuit breaker pattern for fault tolerance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CircuitBreakerConfigSchema
|
|
9
|
+
} from './schemas.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Circuit breaker for fault tolerance
|
|
13
|
+
*/
|
|
14
|
+
export class CircuitBreaker {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new circuit breaker
|
|
17
|
+
* @param {Object} [config] - Circuit breaker configuration
|
|
18
|
+
* @param {number} [config.failureThreshold=5] - Failures before opening
|
|
19
|
+
* @param {number} [config.successThreshold=2] - Successes before closing
|
|
20
|
+
* @param {number} [config.timeout=60000] - Timeout in ms
|
|
21
|
+
* @param {number} [config.resetTimeout=30000] - Reset timeout in ms
|
|
22
|
+
* @param {number} [config.monitoringPeriod=10000] - Monitoring window in ms
|
|
23
|
+
*/
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.config = CircuitBreakerConfigSchema.parse(config);
|
|
26
|
+
this.state = 'closed';
|
|
27
|
+
this.failures = 0;
|
|
28
|
+
this.successes = 0;
|
|
29
|
+
this.lastFailureTime = null;
|
|
30
|
+
this.nextAttemptTime = null;
|
|
31
|
+
this.stats = {
|
|
32
|
+
totalRequests: 0,
|
|
33
|
+
successfulRequests: 0,
|
|
34
|
+
failedRequests: 0,
|
|
35
|
+
rejectedRequests: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Executes an operation through the circuit breaker
|
|
41
|
+
* @param {Function} operation - Async operation to execute
|
|
42
|
+
* @param {Object} [options] - Execution options
|
|
43
|
+
* @param {Function} [options.fallback] - Fallback function
|
|
44
|
+
* @returns {Promise<any>} Operation result
|
|
45
|
+
* @throws {Error} If circuit is open or operation fails
|
|
46
|
+
* @example
|
|
47
|
+
* const breaker = new CircuitBreaker({ failureThreshold: 3 });
|
|
48
|
+
* const result = await breaker.execute(async () => {
|
|
49
|
+
* return await fetch('https://api.example.com');
|
|
50
|
+
* });
|
|
51
|
+
*/
|
|
52
|
+
async execute(operation, options = {}) {
|
|
53
|
+
this.stats.totalRequests++;
|
|
54
|
+
|
|
55
|
+
// Check if circuit is open
|
|
56
|
+
if (this.state === 'open') {
|
|
57
|
+
// Check if reset timeout has elapsed
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (this.nextAttemptTime && now >= this.nextAttemptTime) {
|
|
60
|
+
this.state = 'half-open';
|
|
61
|
+
this.successes = 0;
|
|
62
|
+
} else {
|
|
63
|
+
this.stats.rejectedRequests++;
|
|
64
|
+
|
|
65
|
+
// Use fallback if available
|
|
66
|
+
if (options.fallback) {
|
|
67
|
+
return options.fallback();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const error = new Error('Circuit breaker is OPEN');
|
|
71
|
+
error.state = this.state;
|
|
72
|
+
error.nextAttemptTime = this.nextAttemptTime;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Execute operation with timeout
|
|
79
|
+
const result = await this.executeWithTimeout(operation);
|
|
80
|
+
|
|
81
|
+
this.onSuccess();
|
|
82
|
+
return result;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.onFailure();
|
|
85
|
+
|
|
86
|
+
// Use fallback if available
|
|
87
|
+
if (options.fallback) {
|
|
88
|
+
return options.fallback();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Executes operation with timeout
|
|
97
|
+
* @param {Function} operation - Operation to execute
|
|
98
|
+
* @returns {Promise<any>} Operation result
|
|
99
|
+
* @throws {Error} If operation times out
|
|
100
|
+
*/
|
|
101
|
+
async executeWithTimeout(operation) {
|
|
102
|
+
const { timeout } = this.config;
|
|
103
|
+
|
|
104
|
+
return Promise.race([
|
|
105
|
+
operation(),
|
|
106
|
+
new Promise((_, reject) => {
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
reject(new Error(`Operation timed out after ${timeout}ms`));
|
|
109
|
+
}, timeout);
|
|
110
|
+
})
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Handles successful operation
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
onSuccess() {
|
|
119
|
+
this.stats.successfulRequests++;
|
|
120
|
+
this.failures = 0;
|
|
121
|
+
|
|
122
|
+
if (this.state === 'half-open') {
|
|
123
|
+
this.successes++;
|
|
124
|
+
if (this.successes >= this.config.successThreshold) {
|
|
125
|
+
this.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handles failed operation
|
|
132
|
+
* @returns {void}
|
|
133
|
+
*/
|
|
134
|
+
onFailure() {
|
|
135
|
+
this.stats.failedRequests++;
|
|
136
|
+
this.failures++;
|
|
137
|
+
this.lastFailureTime = Date.now();
|
|
138
|
+
|
|
139
|
+
if (this.state === 'half-open') {
|
|
140
|
+
this.open();
|
|
141
|
+
} else if (this.failures >= this.config.failureThreshold) {
|
|
142
|
+
this.open();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Opens the circuit breaker
|
|
148
|
+
* @returns {void}
|
|
149
|
+
*/
|
|
150
|
+
open() {
|
|
151
|
+
this.state = 'open';
|
|
152
|
+
this.nextAttemptTime = Date.now() + this.config.resetTimeout;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Closes the circuit breaker
|
|
157
|
+
* @returns {void}
|
|
158
|
+
*/
|
|
159
|
+
close() {
|
|
160
|
+
this.state = 'closed';
|
|
161
|
+
this.failures = 0;
|
|
162
|
+
this.successes = 0;
|
|
163
|
+
this.nextAttemptTime = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resets the circuit breaker to initial state
|
|
168
|
+
* @returns {void}
|
|
169
|
+
*/
|
|
170
|
+
reset() {
|
|
171
|
+
this.state = 'closed';
|
|
172
|
+
this.failures = 0;
|
|
173
|
+
this.successes = 0;
|
|
174
|
+
this.lastFailureTime = null;
|
|
175
|
+
this.nextAttemptTime = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Gets current circuit breaker state
|
|
180
|
+
* @returns {string} Current state (closed, open, half-open)
|
|
181
|
+
*/
|
|
182
|
+
getState() {
|
|
183
|
+
return this.state;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets circuit breaker statistics
|
|
188
|
+
* @returns {Object} Statistics object
|
|
189
|
+
*/
|
|
190
|
+
getStats() {
|
|
191
|
+
const { totalRequests, successfulRequests, failedRequests, rejectedRequests } = this.stats;
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...this.stats,
|
|
195
|
+
successRate: totalRequests > 0 ? successfulRequests / totalRequests : 0,
|
|
196
|
+
failureRate: totalRequests > 0 ? failedRequests / totalRequests : 0,
|
|
197
|
+
rejectionRate: totalRequests > 0 ? rejectedRequests / totalRequests : 0,
|
|
198
|
+
currentState: this.state,
|
|
199
|
+
failures: this.failures,
|
|
200
|
+
successes: this.successes
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Checks if circuit is allowing requests
|
|
206
|
+
* @returns {boolean} True if requests are allowed
|
|
207
|
+
*/
|
|
208
|
+
isAllowingRequests() {
|
|
209
|
+
if (this.state === 'closed') {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (this.state === 'half-open') {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Open state - check if reset timeout elapsed
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
return this.nextAttemptTime && now >= this.nextAttemptTime;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Wraps a function with circuit breaker
|
|
224
|
+
* @param {Function} fn - Function to wrap
|
|
225
|
+
* @param {Object} [options] - Execution options
|
|
226
|
+
* @returns {Function} Wrapped function
|
|
227
|
+
*/
|
|
228
|
+
wrap(fn, options = {}) {
|
|
229
|
+
return async (...args) => {
|
|
230
|
+
return this.execute(() => fn(...args), options);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Updates circuit breaker configuration
|
|
236
|
+
* @param {Object} updates - Configuration updates
|
|
237
|
+
* @returns {void}
|
|
238
|
+
*/
|
|
239
|
+
updateConfig(updates) {
|
|
240
|
+
this.config = CircuitBreakerConfigSchema.parse({
|
|
241
|
+
...this.config,
|
|
242
|
+
...updates
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Gets current configuration
|
|
248
|
+
* @returns {Object} Current configuration
|
|
249
|
+
*/
|
|
250
|
+
getConfig() {
|
|
251
|
+
return { ...this.config };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Creates a new circuit breaker instance
|
|
257
|
+
* @param {Object} [config] - Circuit breaker configuration
|
|
258
|
+
* @returns {CircuitBreaker} Circuit breaker instance
|
|
259
|
+
*/
|
|
260
|
+
export function createCircuitBreaker(config) {
|
|
261
|
+
return new CircuitBreaker(config);
|
|
262
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Error classification and pattern matching
|
|
3
|
+
* @module @unrdf/self-healing-workflows/classifier
|
|
4
|
+
* @description Classifies errors by category and severity for recovery decisions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ErrorPatternSchema
|
|
9
|
+
} from './schemas.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default error patterns for common scenarios
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_PATTERNS = [
|
|
15
|
+
{
|
|
16
|
+
name: 'NetworkError',
|
|
17
|
+
category: 'network',
|
|
18
|
+
severity: 'medium',
|
|
19
|
+
pattern: /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|network/i
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'ResourceError',
|
|
23
|
+
category: 'resource',
|
|
24
|
+
severity: 'high',
|
|
25
|
+
pattern: /ENOMEM|ENOSPC|out of memory|disk full|resource unavailable/i
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'ValidationError',
|
|
29
|
+
category: 'validation',
|
|
30
|
+
severity: 'low',
|
|
31
|
+
pattern: /\bvalidation\b|\binvalid\b|schema|parse error/i
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'DependencyError',
|
|
35
|
+
category: 'dependency',
|
|
36
|
+
severity: 'high',
|
|
37
|
+
pattern: /service unavailable|503|502|dependency failed/i
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'BusinessLogicError',
|
|
41
|
+
category: 'business-logic',
|
|
42
|
+
severity: 'critical',
|
|
43
|
+
pattern: /business rule|constraint violation|invariant/i
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'TimeoutError',
|
|
47
|
+
category: 'timeout',
|
|
48
|
+
severity: 'medium',
|
|
49
|
+
pattern: /\btimeout\b|timed out|deadline exceeded/i
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Error classifier for pattern matching and categorization
|
|
55
|
+
*/
|
|
56
|
+
export class ErrorClassifier {
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new error classifier
|
|
59
|
+
* @param {Object} [options] - Configuration options
|
|
60
|
+
* @param {Array<Object>} [options.patterns] - Custom error patterns
|
|
61
|
+
*/
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
this.patterns = [
|
|
64
|
+
...DEFAULT_PATTERNS,
|
|
65
|
+
...(options.patterns || [])
|
|
66
|
+
].map(p => ErrorPatternSchema.parse(p));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Classifies an error based on patterns
|
|
71
|
+
* @param {Error} error - The error to classify
|
|
72
|
+
* @returns {Object} Classified error object
|
|
73
|
+
* @example
|
|
74
|
+
* const classifier = new ErrorClassifier();
|
|
75
|
+
* const classified = classifier.classify(new Error('ECONNREFUSED'));
|
|
76
|
+
* console.log(classified.category); // 'network'
|
|
77
|
+
*/
|
|
78
|
+
classify(error) {
|
|
79
|
+
if (!(error instanceof Error)) {
|
|
80
|
+
throw new TypeError('Expected Error instance');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const errorMessage = error.message || '';
|
|
84
|
+
const errorName = error.name || '';
|
|
85
|
+
// Only match against name and message, not stack trace
|
|
86
|
+
const fullText = `${errorName} ${errorMessage}`;
|
|
87
|
+
|
|
88
|
+
// Try to match against patterns
|
|
89
|
+
for (const pattern of this.patterns) {
|
|
90
|
+
const regex = pattern.pattern instanceof RegExp
|
|
91
|
+
? pattern.pattern
|
|
92
|
+
: new RegExp(pattern.pattern, 'i');
|
|
93
|
+
|
|
94
|
+
if (regex.test(fullText)) {
|
|
95
|
+
return {
|
|
96
|
+
originalError: error,
|
|
97
|
+
category: pattern.category,
|
|
98
|
+
severity: pattern.severity,
|
|
99
|
+
matchedPattern: pattern.name,
|
|
100
|
+
retryable: this.isRetryable(pattern.category, pattern.severity),
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
metadata: pattern.metadata
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Default classification for unknown errors
|
|
108
|
+
return {
|
|
109
|
+
originalError: error,
|
|
110
|
+
category: 'unknown',
|
|
111
|
+
severity: 'medium',
|
|
112
|
+
retryable: false,
|
|
113
|
+
timestamp: Date.now()
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Determines if an error is retryable based on category and severity
|
|
119
|
+
* @param {string} category - Error category
|
|
120
|
+
* @param {string} severity - Error severity
|
|
121
|
+
* @returns {boolean} True if error is retryable
|
|
122
|
+
*/
|
|
123
|
+
isRetryable(category, severity) {
|
|
124
|
+
// Critical errors are never retryable
|
|
125
|
+
if (severity === 'critical') {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Business logic errors are not retryable
|
|
130
|
+
if (category === 'business-logic') {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validation errors are not retryable
|
|
135
|
+
if (category === 'validation') {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Network, timeout, and resource errors are retryable
|
|
140
|
+
return ['network', 'timeout', 'resource', 'dependency'].includes(category);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Adds a custom error pattern
|
|
145
|
+
* @param {Object} pattern - Error pattern to add
|
|
146
|
+
* @returns {void}
|
|
147
|
+
*/
|
|
148
|
+
addPattern(pattern) {
|
|
149
|
+
const validated = ErrorPatternSchema.parse(pattern);
|
|
150
|
+
this.patterns.push(validated);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Gets all registered patterns
|
|
155
|
+
* @returns {Array<Object>} Error patterns
|
|
156
|
+
*/
|
|
157
|
+
getPatterns() {
|
|
158
|
+
return [...this.patterns];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Classifies multiple errors
|
|
163
|
+
* @param {Array<Error>} errors - Errors to classify
|
|
164
|
+
* @returns {Array<Object>} Classified errors
|
|
165
|
+
*/
|
|
166
|
+
classifyBatch(errors) {
|
|
167
|
+
return errors.map(error => this.classify(error));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Gets error statistics by category
|
|
172
|
+
* @param {Array<Object>} classifiedErrors - Array of classified errors
|
|
173
|
+
* @returns {Object} Statistics by category
|
|
174
|
+
*/
|
|
175
|
+
getStatsByCategory(classifiedErrors) {
|
|
176
|
+
const stats = {};
|
|
177
|
+
|
|
178
|
+
for (const classified of classifiedErrors) {
|
|
179
|
+
const category = classified.category;
|
|
180
|
+
stats[category] = (stats[category] || 0) + 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return stats;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Filters retryable errors from a batch
|
|
188
|
+
* @param {Array<Object>} classifiedErrors - Classified errors
|
|
189
|
+
* @returns {Array<Object>} Retryable errors only
|
|
190
|
+
*/
|
|
191
|
+
filterRetryable(classifiedErrors) {
|
|
192
|
+
return classifiedErrors.filter(e => e.retryable);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Creates a new error classifier instance
|
|
198
|
+
* @param {Object} [options] - Configuration options
|
|
199
|
+
* @returns {ErrorClassifier} Error classifier instance
|
|
200
|
+
*/
|
|
201
|
+
export function createErrorClassifier(options) {
|
|
202
|
+
return new ErrorClassifier(options);
|
|
203
|
+
}
|