@onlineapps/conn-infra-error-handler 1.0.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/API.md +637 -0
- package/package.json +34 -0
- package/src/index.js +822 -0
- package/test/component/error-handling-flow.test.js +336 -0
- package/test/unit/ErrorHandlerConnector.test.js +294 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @onlineapps/conn-infra-error-handler
|
|
3
|
+
* @description Unified error handling connector providing retry strategies, circuit breaker pattern,
|
|
4
|
+
* and compensation mechanisms for OA Drive microservices.
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://github.com/onlineapps/oa-drive/tree/main/shared/connector/conn-infra-error-handler|GitHub Repository}
|
|
7
|
+
* @author OA Drive Team
|
|
8
|
+
* @license MIT
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const CircuitBreaker = require('opossum');
|
|
13
|
+
|
|
14
|
+
// Error types enum
|
|
15
|
+
const ErrorTypes = {
|
|
16
|
+
TRANSIENT: 'TRANSIENT', // Retry with backoff
|
|
17
|
+
BUSINESS: 'BUSINESS', // Don't retry, return error
|
|
18
|
+
FATAL: 'FATAL', // Stop workflow
|
|
19
|
+
VALIDATION: 'VALIDATION', // Input validation error
|
|
20
|
+
TIMEOUT: 'TIMEOUT', // Operation timeout
|
|
21
|
+
RATE_LIMIT: 'RATE_LIMIT', // Rate limiting
|
|
22
|
+
UNKNOWN: 'UNKNOWN' // Unclassified error
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Standard error codes
|
|
26
|
+
const ErrorCodes = {
|
|
27
|
+
// Network errors
|
|
28
|
+
ECONNREFUSED: ErrorTypes.TRANSIENT,
|
|
29
|
+
ENOTFOUND: ErrorTypes.TRANSIENT,
|
|
30
|
+
ETIMEDOUT: ErrorTypes.TRANSIENT,
|
|
31
|
+
ECONNRESET: ErrorTypes.TRANSIENT,
|
|
32
|
+
EPIPE: ErrorTypes.TRANSIENT,
|
|
33
|
+
|
|
34
|
+
// HTTP status codes
|
|
35
|
+
408: ErrorTypes.TIMEOUT,
|
|
36
|
+
429: ErrorTypes.RATE_LIMIT,
|
|
37
|
+
500: ErrorTypes.TRANSIENT,
|
|
38
|
+
502: ErrorTypes.TRANSIENT,
|
|
39
|
+
503: ErrorTypes.TRANSIENT,
|
|
40
|
+
504: ErrorTypes.TIMEOUT,
|
|
41
|
+
|
|
42
|
+
// Business errors
|
|
43
|
+
400: ErrorTypes.VALIDATION,
|
|
44
|
+
401: ErrorTypes.BUSINESS,
|
|
45
|
+
403: ErrorTypes.BUSINESS,
|
|
46
|
+
404: ErrorTypes.BUSINESS,
|
|
47
|
+
409: ErrorTypes.BUSINESS,
|
|
48
|
+
422: ErrorTypes.VALIDATION
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Error handling connector with retry, circuit breaker, and compensation
|
|
53
|
+
*
|
|
54
|
+
* @class ErrorHandlerConnector
|
|
55
|
+
*
|
|
56
|
+
* @example <caption>Basic Usage</caption>
|
|
57
|
+
* const errorHandler = new ErrorHandlerConnector({
|
|
58
|
+
* maxRetries: 3,
|
|
59
|
+
* retryDelay: 1000
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* const result = await errorHandler.executeWithRetry(async () => {
|
|
63
|
+
* return await riskyOperation();
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* @example <caption>With Circuit Breaker</caption>
|
|
67
|
+
* const errorHandler = new ErrorHandlerConnector({
|
|
68
|
+
* circuitBreakerEnabled: true,
|
|
69
|
+
* errorThreshold: 50
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* await errorHandler.executeWithCircuitBreaker('api-call', async () => {
|
|
73
|
+
* return await apiClient.call();
|
|
74
|
+
* });
|
|
75
|
+
*/
|
|
76
|
+
class ErrorHandlerConnector {
|
|
77
|
+
/**
|
|
78
|
+
* Creates a new ErrorHandlerConnector instance
|
|
79
|
+
*
|
|
80
|
+
* @constructor
|
|
81
|
+
* @param {Object} [config={}] - Configuration options
|
|
82
|
+
* @param {number} [config.maxRetries=3] - Maximum retry attempts
|
|
83
|
+
* @param {number} [config.retryDelay=1000] - Initial retry delay in ms
|
|
84
|
+
* @param {number} [config.retryMultiplier=2] - Backoff multiplier
|
|
85
|
+
* @param {number} [config.maxRetryDelay=30000] - Maximum retry delay
|
|
86
|
+
* @param {boolean} [config.circuitBreakerEnabled=true] - Enable circuit breaker
|
|
87
|
+
* @param {number} [config.circuitTimeout=10000] - Circuit breaker timeout
|
|
88
|
+
* @param {number} [config.errorThreshold=50] - Error threshold percentage
|
|
89
|
+
* @param {number} [config.resetTimeout=30000] - Circuit reset timeout
|
|
90
|
+
* @param {boolean} [config.compensationEnabled=true] - Enable compensation
|
|
91
|
+
* @param {Object} [config.circuitBreakerOptions] - Additional circuit breaker options
|
|
92
|
+
*
|
|
93
|
+
* @example <caption>Full Configuration</caption>
|
|
94
|
+
* const errorHandler = new ErrorHandlerConnector({
|
|
95
|
+
* maxRetries: 5,
|
|
96
|
+
* retryDelay: 500,
|
|
97
|
+
* retryMultiplier: 1.5,
|
|
98
|
+
* maxRetryDelay: 20000,
|
|
99
|
+
* circuitBreakerEnabled: true,
|
|
100
|
+
* errorThreshold: 60,
|
|
101
|
+
* compensationEnabled: true
|
|
102
|
+
* });
|
|
103
|
+
*/
|
|
104
|
+
constructor(config = {}) {
|
|
105
|
+
// Retry configuration
|
|
106
|
+
this.maxRetries = config.maxRetries || 3;
|
|
107
|
+
this.retryDelay = config.retryDelay || 1000;
|
|
108
|
+
this.retryMultiplier = config.retryMultiplier || 2;
|
|
109
|
+
this.maxRetryDelay = config.maxRetryDelay || 30000;
|
|
110
|
+
|
|
111
|
+
// Circuit breaker configuration
|
|
112
|
+
this.circuitBreakerEnabled = config.circuitBreakerEnabled !== false;
|
|
113
|
+
this.circuitBreakerOptions = {
|
|
114
|
+
timeout: config.circuitTimeout || 10000,
|
|
115
|
+
errorThresholdPercentage: config.errorThreshold || 50,
|
|
116
|
+
resetTimeout: config.resetTimeout || 30000,
|
|
117
|
+
rollingCountTimeout: config.rollingCountTimeout || 10000,
|
|
118
|
+
rollingCountBuckets: config.rollingCountBuckets || 10,
|
|
119
|
+
...config.circuitBreakerOptions
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Compensation configuration
|
|
123
|
+
this.compensationEnabled = config.compensationEnabled !== false;
|
|
124
|
+
this.compensationHandlers = new Map();
|
|
125
|
+
|
|
126
|
+
// Circuit breakers registry
|
|
127
|
+
this.circuitBreakers = new Map();
|
|
128
|
+
|
|
129
|
+
// Error statistics
|
|
130
|
+
this.stats = {
|
|
131
|
+
errors: 0,
|
|
132
|
+
retries: 0,
|
|
133
|
+
compensations: 0,
|
|
134
|
+
circuitBreaks: 0,
|
|
135
|
+
byType: {}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Initialize error type stats
|
|
139
|
+
Object.values(ErrorTypes).forEach(type => {
|
|
140
|
+
this.stats.byType[type] = 0;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Classify error into type for appropriate handling
|
|
146
|
+
*
|
|
147
|
+
* @method classifyError
|
|
148
|
+
* @param {Error} error - Error to classify
|
|
149
|
+
* @param {string} [error.code] - Error code (e.g., ECONNREFUSED)
|
|
150
|
+
* @param {number} [error.statusCode] - HTTP status code
|
|
151
|
+
* @param {string} [error.type] - Explicit error type
|
|
152
|
+
* @returns {string} Error type from ErrorTypes enum
|
|
153
|
+
*
|
|
154
|
+
* @example <caption>Network Error</caption>
|
|
155
|
+
* const type = errorHandler.classifyError(new Error('ECONNREFUSED'));
|
|
156
|
+
* // Returns: 'TRANSIENT'
|
|
157
|
+
*
|
|
158
|
+
* @example <caption>HTTP Error</caption>
|
|
159
|
+
* const error = new Error('Not Found');
|
|
160
|
+
* error.statusCode = 404;
|
|
161
|
+
* const type = errorHandler.classifyError(error);
|
|
162
|
+
* // Returns: 'BUSINESS'
|
|
163
|
+
*
|
|
164
|
+
* @example <caption>Timeout Error</caption>
|
|
165
|
+
* const type = errorHandler.classifyError(new Error('Operation timeout'));
|
|
166
|
+
* // Returns: 'TIMEOUT'
|
|
167
|
+
*/
|
|
168
|
+
classifyError(error) {
|
|
169
|
+
// Check by error code
|
|
170
|
+
if (error.code && ErrorCodes[error.code]) {
|
|
171
|
+
return ErrorCodes[error.code];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check by HTTP status
|
|
175
|
+
if (error.statusCode && ErrorCodes[error.statusCode]) {
|
|
176
|
+
return ErrorCodes[error.statusCode];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check by error message patterns
|
|
180
|
+
const message = error.message?.toLowerCase() || '';
|
|
181
|
+
|
|
182
|
+
if (message.includes('timeout')) return ErrorTypes.TIMEOUT;
|
|
183
|
+
if (message.includes('rate limit')) return ErrorTypes.RATE_LIMIT;
|
|
184
|
+
if (message.includes('validation')) return ErrorTypes.VALIDATION;
|
|
185
|
+
if (message.includes('unauthorized') || message.includes('forbidden')) {
|
|
186
|
+
return ErrorTypes.BUSINESS;
|
|
187
|
+
}
|
|
188
|
+
if (message.includes('connection') || message.includes('network')) {
|
|
189
|
+
return ErrorTypes.TRANSIENT;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check if error has explicit type
|
|
193
|
+
if (error.type && ErrorTypes[error.type]) {
|
|
194
|
+
return error.type;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return ErrorTypes.UNKNOWN;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Determine if error should be retried
|
|
202
|
+
*
|
|
203
|
+
* @method shouldRetry
|
|
204
|
+
* @param {Error} error - Error to check
|
|
205
|
+
* @param {number} [attempts=0] - Current attempt count
|
|
206
|
+
* @returns {boolean} True if should retry
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* const error = new Error('Connection refused');
|
|
210
|
+
* if (errorHandler.shouldRetry(error, 1)) {
|
|
211
|
+
* // Retry the operation
|
|
212
|
+
* }
|
|
213
|
+
*/
|
|
214
|
+
shouldRetry(error, attempts = 0) {
|
|
215
|
+
if (attempts >= this.maxRetries) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const errorType = this.classifyError(error);
|
|
220
|
+
|
|
221
|
+
// Only retry transient and timeout errors
|
|
222
|
+
return [
|
|
223
|
+
ErrorTypes.TRANSIENT,
|
|
224
|
+
ErrorTypes.TIMEOUT,
|
|
225
|
+
ErrorTypes.RATE_LIMIT
|
|
226
|
+
].includes(errorType);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Calculate exponential backoff delay
|
|
231
|
+
*
|
|
232
|
+
* @method calculateBackoff
|
|
233
|
+
* @param {number} attempts - Current attempt number (1-based)
|
|
234
|
+
* @returns {number} Delay in milliseconds
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* const delay = errorHandler.calculateBackoff(3);
|
|
238
|
+
* // With default config: 1000 * 2^2 = 4000ms
|
|
239
|
+
*/
|
|
240
|
+
calculateBackoff(attempts) {
|
|
241
|
+
const delay = this.retryDelay * Math.pow(this.retryMultiplier, attempts - 1);
|
|
242
|
+
return Math.min(delay, this.maxRetryDelay);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Execute function with automatic retry on failure
|
|
247
|
+
*
|
|
248
|
+
* @async
|
|
249
|
+
* @method executeWithRetry
|
|
250
|
+
* @param {Function} fn - Async function to execute
|
|
251
|
+
* @param {Object} [options={}] - Retry options
|
|
252
|
+
* @param {number} [options.maxRetries] - Override max retries
|
|
253
|
+
* @param {Array<string>} [options.retryOn=[]] - Additional error codes to retry
|
|
254
|
+
* @param {Function} [options.onRetry] - Callback on retry (error, attempt, delay)
|
|
255
|
+
* @returns {Promise<*>} Function result
|
|
256
|
+
*
|
|
257
|
+
* @throws {Error} Last error if all retries fail
|
|
258
|
+
*
|
|
259
|
+
* @example <caption>Simple Retry</caption>
|
|
260
|
+
* const result = await errorHandler.executeWithRetry(async () => {
|
|
261
|
+
* return await apiClient.fetchData();
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* @example <caption>With Options</caption>
|
|
265
|
+
* const result = await errorHandler.executeWithRetry(
|
|
266
|
+
* async () => await riskyOperation(),
|
|
267
|
+
* {
|
|
268
|
+
* maxRetries: 5,
|
|
269
|
+
* retryOn: ['CUSTOM_ERROR'],
|
|
270
|
+
* onRetry: (error, attempt, delay) => {
|
|
271
|
+
* console.log(`Retry ${attempt} after ${delay}ms`);
|
|
272
|
+
* }
|
|
273
|
+
* }
|
|
274
|
+
* );
|
|
275
|
+
*
|
|
276
|
+
* @example <caption>Database Operation</caption>
|
|
277
|
+
* const data = await errorHandler.executeWithRetry(async () => {
|
|
278
|
+
* const conn = await db.connect();
|
|
279
|
+
* try {
|
|
280
|
+
* return await conn.query('SELECT * FROM users');
|
|
281
|
+
* } finally {
|
|
282
|
+
* conn.close();
|
|
283
|
+
* }
|
|
284
|
+
* });
|
|
285
|
+
*/
|
|
286
|
+
async executeWithRetry(fn, options = {}) {
|
|
287
|
+
const maxAttempts = options.maxRetries || this.maxRetries;
|
|
288
|
+
const retryOn = options.retryOn || [];
|
|
289
|
+
|
|
290
|
+
let lastError;
|
|
291
|
+
|
|
292
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
293
|
+
try {
|
|
294
|
+
return await fn();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
lastError = error;
|
|
297
|
+
this.stats.errors++;
|
|
298
|
+
this.stats.byType[this.classifyError(error)]++;
|
|
299
|
+
|
|
300
|
+
// Check if should retry
|
|
301
|
+
const shouldRetry = this.shouldRetry(error, attempt) ||
|
|
302
|
+
retryOn.includes(error.code);
|
|
303
|
+
|
|
304
|
+
if (!shouldRetry || attempt === maxAttempts) {
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Calculate and wait backoff
|
|
309
|
+
const delay = this.calculateBackoff(attempt);
|
|
310
|
+
this.stats.retries++;
|
|
311
|
+
|
|
312
|
+
if (options.onRetry) {
|
|
313
|
+
options.onRetry(error, attempt, delay);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await this.sleep(delay);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
throw lastError;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get or create circuit breaker for operation
|
|
325
|
+
* @param {string} name - Operation name
|
|
326
|
+
* @param {Function} fn - Function to protect
|
|
327
|
+
* @param {object} options - Circuit breaker options
|
|
328
|
+
* @returns {CircuitBreaker} Circuit breaker instance
|
|
329
|
+
*/
|
|
330
|
+
getCircuitBreaker(name, fn, options = {}) {
|
|
331
|
+
if (!this.circuitBreakerEnabled) {
|
|
332
|
+
// Return pass-through if disabled
|
|
333
|
+
return { fire: () => fn() };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!this.circuitBreakers.has(name)) {
|
|
337
|
+
const breaker = new CircuitBreaker(fn, {
|
|
338
|
+
...this.circuitBreakerOptions,
|
|
339
|
+
...options
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Track circuit breaker events
|
|
343
|
+
breaker.on('open', () => {
|
|
344
|
+
this.stats.circuitBreaks++;
|
|
345
|
+
console.warn(`Circuit breaker opened: ${name}`);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
breaker.on('halfOpen', () => {
|
|
349
|
+
console.info(`Circuit breaker half-open: ${name}`);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
this.circuitBreakers.set(name, breaker);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return this.circuitBreakers.get(name);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Execute function with circuit breaker protection
|
|
360
|
+
*
|
|
361
|
+
* @async
|
|
362
|
+
* @method executeWithCircuitBreaker
|
|
363
|
+
* @param {string} name - Operation name for circuit breaker
|
|
364
|
+
* @param {Function} fn - Async function to execute
|
|
365
|
+
* @param {Object} [options={}] - Circuit breaker options
|
|
366
|
+
* @param {number} [options.timeout] - Operation timeout
|
|
367
|
+
* @param {number} [options.errorThresholdPercentage] - Error threshold
|
|
368
|
+
* @returns {Promise<*>} Function result
|
|
369
|
+
*
|
|
370
|
+
* @throws {Error} If circuit is open or operation fails
|
|
371
|
+
*
|
|
372
|
+
* @fires CircuitBreaker#open - When circuit opens
|
|
373
|
+
* @fires CircuitBreaker#halfOpen - When circuit enters half-open state
|
|
374
|
+
*
|
|
375
|
+
* @example <caption>API Call Protection</caption>
|
|
376
|
+
* try {
|
|
377
|
+
* const data = await errorHandler.executeWithCircuitBreaker(
|
|
378
|
+
* 'user-api',
|
|
379
|
+
* async () => await userAPI.getUser(id)
|
|
380
|
+
* );
|
|
381
|
+
* } catch (error) {
|
|
382
|
+
* if (error.message.includes('circuit is open')) {
|
|
383
|
+
* // Service is down, use fallback
|
|
384
|
+
* }
|
|
385
|
+
* }
|
|
386
|
+
*
|
|
387
|
+
* @example <caption>With Custom Options</caption>
|
|
388
|
+
* const result = await errorHandler.executeWithCircuitBreaker(
|
|
389
|
+
* 'payment-gateway',
|
|
390
|
+
* async () => await processPayment(order),
|
|
391
|
+
* {
|
|
392
|
+
* timeout: 5000,
|
|
393
|
+
* errorThresholdPercentage: 30
|
|
394
|
+
* }
|
|
395
|
+
* );
|
|
396
|
+
*/
|
|
397
|
+
async executeWithCircuitBreaker(name, fn, options = {}) {
|
|
398
|
+
const breaker = this.getCircuitBreaker(name, fn, options);
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
return await breaker.fire();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.stats.errors++;
|
|
404
|
+
this.stats.byType[this.classifyError(error)]++;
|
|
405
|
+
|
|
406
|
+
if (error.code === 'EOPENBREAKER') {
|
|
407
|
+
throw new Error(`Service unavailable: ${name} circuit is open`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Register compensation handler for an operation
|
|
416
|
+
*
|
|
417
|
+
* @method registerCompensation
|
|
418
|
+
* @param {string} operation - Operation name
|
|
419
|
+
* @param {Function} handler - Compensation handler function
|
|
420
|
+
* @returns {void}
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* errorHandler.registerCompensation('create-order', async (context) => {
|
|
424
|
+
* // Rollback order creation
|
|
425
|
+
* await orderService.cancel(context.orderId);
|
|
426
|
+
* });
|
|
427
|
+
*
|
|
428
|
+
* errorHandler.registerCompensation('charge-payment', async (context) => {
|
|
429
|
+
* // Refund payment
|
|
430
|
+
* await paymentService.refund(context.chargeId);
|
|
431
|
+
* });
|
|
432
|
+
*/
|
|
433
|
+
registerCompensation(operation, handler) {
|
|
434
|
+
this.compensationHandlers.set(operation, handler);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Execute compensation for failed operation
|
|
439
|
+
*
|
|
440
|
+
* @async
|
|
441
|
+
* @method executeCompensation
|
|
442
|
+
* @param {string} operation - Operation that failed
|
|
443
|
+
* @param {Object} context - Operation context for compensation
|
|
444
|
+
* @returns {Promise<*>} Compensation result or null if no handler
|
|
445
|
+
*
|
|
446
|
+
* @throws {Error} If compensation fails
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* try {
|
|
450
|
+
* await createOrder(data);
|
|
451
|
+
* } catch (error) {
|
|
452
|
+
* await errorHandler.executeCompensation('create-order', {
|
|
453
|
+
* orderId: data.orderId,
|
|
454
|
+
* userId: data.userId
|
|
455
|
+
* });
|
|
456
|
+
* }
|
|
457
|
+
*/
|
|
458
|
+
async executeCompensation(operation, context) {
|
|
459
|
+
if (!this.compensationEnabled) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const handler = this.compensationHandlers.get(operation);
|
|
464
|
+
|
|
465
|
+
if (!handler) {
|
|
466
|
+
console.warn(`No compensation handler for: ${operation}`);
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
this.stats.compensations++;
|
|
472
|
+
const result = await handler(context);
|
|
473
|
+
console.info(`Compensation executed for: ${operation}`);
|
|
474
|
+
return result;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.error(`Compensation failed for ${operation}:`, error);
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Route failed message to dead letter queue
|
|
483
|
+
*
|
|
484
|
+
* @async
|
|
485
|
+
* @method routeToDLQ
|
|
486
|
+
* @param {Object} mqClient - Message queue client
|
|
487
|
+
* @param {Object} message - Original message that failed
|
|
488
|
+
* @param {Error} error - Error that occurred
|
|
489
|
+
* @returns {Promise<void>}
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* try {
|
|
493
|
+
* await processMessage(message);
|
|
494
|
+
* } catch (error) {
|
|
495
|
+
* if (!errorHandler.shouldRetry(error)) {
|
|
496
|
+
* await errorHandler.routeToDLQ(mqClient, message, error);
|
|
497
|
+
* }
|
|
498
|
+
* }
|
|
499
|
+
*/
|
|
500
|
+
async routeToDLQ(mqClient, message, error) {
|
|
501
|
+
const dlqMessage = {
|
|
502
|
+
...message,
|
|
503
|
+
error: {
|
|
504
|
+
message: error.message,
|
|
505
|
+
stack: error.stack,
|
|
506
|
+
code: error.code,
|
|
507
|
+
type: this.classifyError(error),
|
|
508
|
+
timestamp: new Date().toISOString()
|
|
509
|
+
},
|
|
510
|
+
originalQueue: message.queue || 'unknown',
|
|
511
|
+
retryCount: message.retryCount || 0
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const dlqName = `${message.queue || 'unknown'}.dlq`;
|
|
515
|
+
|
|
516
|
+
await mqClient.publish(dlqName, dlqMessage, {
|
|
517
|
+
persistent: true
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
console.info(`Message routed to DLQ: ${dlqName}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Create standardized error response
|
|
525
|
+
*
|
|
526
|
+
* @method createErrorResponse
|
|
527
|
+
* @param {Error} error - Original error
|
|
528
|
+
* @param {Object} [context={}] - Additional context
|
|
529
|
+
* @returns {ErrorResponse} Formatted error response
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* const response = errorHandler.createErrorResponse(
|
|
533
|
+
* new Error('Database connection failed'),
|
|
534
|
+
* { operation: 'user-fetch', userId: 123 }
|
|
535
|
+
* );
|
|
536
|
+
* // Returns standardized error object
|
|
537
|
+
*/
|
|
538
|
+
createErrorResponse(error, context = {}) {
|
|
539
|
+
const errorType = this.classifyError(error);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
success: false,
|
|
543
|
+
error: {
|
|
544
|
+
message: error.message,
|
|
545
|
+
code: error.code || 'UNKNOWN_ERROR',
|
|
546
|
+
type: errorType,
|
|
547
|
+
retryable: this.shouldRetry(error),
|
|
548
|
+
timestamp: new Date().toISOString(),
|
|
549
|
+
...context
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Wrap function with error handling capabilities
|
|
556
|
+
*
|
|
557
|
+
* @method wrap
|
|
558
|
+
* @param {Function} fn - Function to wrap
|
|
559
|
+
* @param {Object} [options={}] - Wrapping options
|
|
560
|
+
* @param {string} [options.name] - Operation name
|
|
561
|
+
* @param {boolean} [options.circuitBreaker=true] - Use circuit breaker
|
|
562
|
+
* @param {boolean} [options.retry=true] - Use retry logic
|
|
563
|
+
* @param {number} [options.maxRetries] - Max retry attempts
|
|
564
|
+
* @returns {Function} Wrapped function
|
|
565
|
+
*
|
|
566
|
+
* @example <caption>Wrap API Function</caption>
|
|
567
|
+
* const safeApiCall = errorHandler.wrap(
|
|
568
|
+
* async (id) => await api.getUser(id),
|
|
569
|
+
* { name: 'get-user', maxRetries: 3 }
|
|
570
|
+
* );
|
|
571
|
+
*
|
|
572
|
+
* const user = await safeApiCall(123);
|
|
573
|
+
*
|
|
574
|
+
* @example <caption>Wrap Database Function</caption>
|
|
575
|
+
* const safeQuery = errorHandler.wrap(
|
|
576
|
+
* async (sql, params) => await db.query(sql, params),
|
|
577
|
+
* { circuitBreaker: false, retry: true }
|
|
578
|
+
* );
|
|
579
|
+
*/
|
|
580
|
+
wrap(fn, options = {}) {
|
|
581
|
+
const name = options.name || fn.name || 'wrapped';
|
|
582
|
+
const useCircuitBreaker = options.circuitBreaker !== false;
|
|
583
|
+
const useRetry = options.retry !== false;
|
|
584
|
+
|
|
585
|
+
return async (...args) => {
|
|
586
|
+
const execute = async () => {
|
|
587
|
+
if (useRetry) {
|
|
588
|
+
return await this.executeWithRetry(() => fn(...args), options);
|
|
589
|
+
}
|
|
590
|
+
return await fn(...args);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
if (useCircuitBreaker) {
|
|
594
|
+
return await this.executeWithCircuitBreaker(name, execute, options);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return await execute();
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Handle workflow error with compensation support
|
|
603
|
+
*
|
|
604
|
+
* @async
|
|
605
|
+
* @method handleWorkflowError
|
|
606
|
+
* @param {Object} workflow - Workflow context
|
|
607
|
+
* @param {Error} error - Error that occurred
|
|
608
|
+
* @param {string} failedStep - Step that failed
|
|
609
|
+
* @returns {Promise<WorkflowErrorResult>} Error handling result
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* const result = await errorHandler.handleWorkflowError(
|
|
613
|
+
* workflowContext,
|
|
614
|
+
* error,
|
|
615
|
+
* 'payment-processing'
|
|
616
|
+
* );
|
|
617
|
+
*
|
|
618
|
+
* if (result.shouldRetry) {
|
|
619
|
+
* // Retry the step
|
|
620
|
+
* } else if (result.compensated) {
|
|
621
|
+
* // Compensation was executed
|
|
622
|
+
* }
|
|
623
|
+
*/
|
|
624
|
+
async handleWorkflowError(workflow, error, failedStep) {
|
|
625
|
+
const errorType = this.classifyError(error);
|
|
626
|
+
|
|
627
|
+
const result = {
|
|
628
|
+
handled: false,
|
|
629
|
+
compensated: false,
|
|
630
|
+
shouldContinue: false,
|
|
631
|
+
shouldRetry: false,
|
|
632
|
+
error: this.createErrorResponse(error, { step: failedStep })
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// Determine action based on error type
|
|
636
|
+
switch (errorType) {
|
|
637
|
+
case ErrorTypes.TRANSIENT:
|
|
638
|
+
case ErrorTypes.TIMEOUT:
|
|
639
|
+
result.shouldRetry = true;
|
|
640
|
+
result.handled = true;
|
|
641
|
+
break;
|
|
642
|
+
|
|
643
|
+
case ErrorTypes.BUSINESS:
|
|
644
|
+
case ErrorTypes.VALIDATION:
|
|
645
|
+
// Try compensation
|
|
646
|
+
if (this.compensationEnabled && workflow.compensationSteps) {
|
|
647
|
+
try {
|
|
648
|
+
await this.executeCompensation(failedStep, workflow);
|
|
649
|
+
result.compensated = true;
|
|
650
|
+
result.handled = true;
|
|
651
|
+
} catch (compError) {
|
|
652
|
+
console.error('Compensation failed:', compError);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
|
|
657
|
+
case ErrorTypes.FATAL:
|
|
658
|
+
// Stop workflow immediately
|
|
659
|
+
result.shouldContinue = false;
|
|
660
|
+
result.handled = true;
|
|
661
|
+
break;
|
|
662
|
+
|
|
663
|
+
default:
|
|
664
|
+
// Unknown error, let it propagate
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Get error handler statistics
|
|
673
|
+
*
|
|
674
|
+
* @method getStats
|
|
675
|
+
* @returns {ErrorStats} Current statistics
|
|
676
|
+
*
|
|
677
|
+
* @example
|
|
678
|
+
* const stats = errorHandler.getStats();
|
|
679
|
+
* console.log(`Total errors: ${stats.errors}`);
|
|
680
|
+
* console.log(`Retry success rate: ${stats.retries / stats.errors * 100}%`);
|
|
681
|
+
* console.log(`Circuit breakers:`, stats.circuitBreakers);
|
|
682
|
+
*/
|
|
683
|
+
getStats() {
|
|
684
|
+
const circuitBreakerStats = {};
|
|
685
|
+
|
|
686
|
+
this.circuitBreakers.forEach((breaker, name) => {
|
|
687
|
+
circuitBreakerStats[name] = {
|
|
688
|
+
state: breaker.opened ? 'open' : breaker.halfOpen ? 'half-open' : 'closed',
|
|
689
|
+
stats: breaker.stats
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
...this.stats,
|
|
695
|
+
circuitBreakers: circuitBreakerStats
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Reset all statistics
|
|
701
|
+
*
|
|
702
|
+
* @method resetStats
|
|
703
|
+
* @returns {void}
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* errorHandler.resetStats();
|
|
707
|
+
* // All counters reset to 0
|
|
708
|
+
*/
|
|
709
|
+
resetStats() {
|
|
710
|
+
this.stats = {
|
|
711
|
+
errors: 0,
|
|
712
|
+
retries: 0,
|
|
713
|
+
compensations: 0,
|
|
714
|
+
circuitBreaks: 0,
|
|
715
|
+
byType: {}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
Object.values(ErrorTypes).forEach(type => {
|
|
719
|
+
this.stats.byType[type] = 0;
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Sleep utility
|
|
725
|
+
* @private
|
|
726
|
+
*/
|
|
727
|
+
async sleep(ms) {
|
|
728
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* @typedef {Object} ErrorResponse
|
|
734
|
+
* @property {boolean} success - Always false for errors
|
|
735
|
+
* @property {Object} error - Error details
|
|
736
|
+
* @property {string} error.message - Error message
|
|
737
|
+
* @property {string} error.code - Error code
|
|
738
|
+
* @property {string} error.type - Error type from ErrorTypes
|
|
739
|
+
* @property {boolean} error.retryable - Whether error is retryable
|
|
740
|
+
* @property {string} error.timestamp - ISO timestamp
|
|
741
|
+
*/
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* @typedef {Object} WorkflowErrorResult
|
|
745
|
+
* @property {boolean} handled - Whether error was handled
|
|
746
|
+
* @property {boolean} compensated - Whether compensation was executed
|
|
747
|
+
* @property {boolean} shouldContinue - Whether workflow should continue
|
|
748
|
+
* @property {boolean} shouldRetry - Whether step should be retried
|
|
749
|
+
* @property {ErrorResponse} error - Error response
|
|
750
|
+
*/
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* @typedef {Object} ErrorStats
|
|
754
|
+
* @property {number} errors - Total errors
|
|
755
|
+
* @property {number} retries - Total retries
|
|
756
|
+
* @property {number} compensations - Total compensations
|
|
757
|
+
* @property {number} circuitBreaks - Circuit breaker trips
|
|
758
|
+
* @property {Object} byType - Errors by type
|
|
759
|
+
* @property {Object} circuitBreakers - Circuit breaker states
|
|
760
|
+
*/
|
|
761
|
+
|
|
762
|
+
// Export main class as default
|
|
763
|
+
module.exports = ErrorHandlerConnector;
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Error type enumeration
|
|
767
|
+
* @enum {string}
|
|
768
|
+
*/
|
|
769
|
+
module.exports.ErrorTypes = ErrorTypes;
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Standard error code mappings
|
|
773
|
+
* @enum {string}
|
|
774
|
+
*/
|
|
775
|
+
module.exports.ErrorCodes = ErrorCodes;
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Factory function to create error handler instance
|
|
779
|
+
*
|
|
780
|
+
* @function create
|
|
781
|
+
* @param {Object} config - Configuration object
|
|
782
|
+
* @returns {ErrorHandlerConnector} New error handler instance
|
|
783
|
+
*
|
|
784
|
+
* @example
|
|
785
|
+
* const errorHandler = ErrorHandlerConnector.create({
|
|
786
|
+
* maxRetries: 5,
|
|
787
|
+
* circuitBreakerEnabled: true
|
|
788
|
+
* });
|
|
789
|
+
*/
|
|
790
|
+
module.exports.create = (config) => new ErrorHandlerConnector(config);
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Current version
|
|
794
|
+
* @constant {string}
|
|
795
|
+
*/
|
|
796
|
+
module.exports.VERSION = '1.0.0';
|
|
797
|
+
|
|
798
|
+
// Export mock for testing
|
|
799
|
+
module.exports.MockErrorHandler = class MockErrorHandler {
|
|
800
|
+
constructor() {
|
|
801
|
+
this.stats = { errors: 0, retries: 0, compensations: 0 };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
classifyError(error) {
|
|
805
|
+
return error.type || ErrorTypes.UNKNOWN;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
shouldRetry(error) {
|
|
809
|
+
return error.retryable !== false;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async executeWithRetry(fn) {
|
|
813
|
+
try {
|
|
814
|
+
return await fn();
|
|
815
|
+
} catch (error) {
|
|
816
|
+
this.stats.errors++;
|
|
817
|
+
throw error;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
getStats() { return this.stats; }
|
|
822
|
+
};
|