@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.
@@ -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
+ }