@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,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Health monitoring and alerting
|
|
3
|
+
* @module @unrdf/self-healing-workflows/health
|
|
4
|
+
* @description Monitors system health and triggers alerts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
HealthCheckConfigSchema
|
|
9
|
+
} from './schemas.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Health monitor for system health checks
|
|
13
|
+
*/
|
|
14
|
+
export class HealthMonitor {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new health monitor
|
|
17
|
+
* @param {Object} [config] - Health check configuration
|
|
18
|
+
* @param {number} [config.interval=30000] - Check interval in ms
|
|
19
|
+
* @param {number} [config.timeout=5000] - Check timeout in ms
|
|
20
|
+
* @param {number} [config.unhealthyThreshold=3] - Failures before unhealthy
|
|
21
|
+
* @param {number} [config.healthyThreshold=2] - Successes before healthy
|
|
22
|
+
*/
|
|
23
|
+
constructor(config = {}) {
|
|
24
|
+
this.config = HealthCheckConfigSchema.parse(config);
|
|
25
|
+
this.checks = new Map();
|
|
26
|
+
this.status = 'healthy';
|
|
27
|
+
this.intervalId = null;
|
|
28
|
+
this.listeners = new Set();
|
|
29
|
+
this.consecutiveFailures = 0;
|
|
30
|
+
this.consecutiveSuccesses = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Registers a health check
|
|
35
|
+
* @param {string} name - Check name
|
|
36
|
+
* @param {Function} checkFn - Async check function
|
|
37
|
+
* @param {Object} [options] - Check options
|
|
38
|
+
* @returns {void}
|
|
39
|
+
*/
|
|
40
|
+
registerCheck(name, checkFn, options = {}) {
|
|
41
|
+
this.checks.set(name, {
|
|
42
|
+
name,
|
|
43
|
+
fn: checkFn,
|
|
44
|
+
status: 'healthy',
|
|
45
|
+
lastCheck: null,
|
|
46
|
+
lastSuccess: null,
|
|
47
|
+
lastFailure: null,
|
|
48
|
+
failures: 0,
|
|
49
|
+
...options
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Removes a health check
|
|
55
|
+
* @param {string} name - Check name
|
|
56
|
+
* @returns {boolean} True if check was removed
|
|
57
|
+
*/
|
|
58
|
+
unregisterCheck(name) {
|
|
59
|
+
return this.checks.delete(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Executes all health checks
|
|
64
|
+
* @returns {Promise<Object>} Health check result
|
|
65
|
+
*/
|
|
66
|
+
async check() {
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
const checkResults = [];
|
|
69
|
+
|
|
70
|
+
for (const [name, check] of this.checks) {
|
|
71
|
+
const checkStart = Date.now();
|
|
72
|
+
let status = 'healthy';
|
|
73
|
+
let message;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Execute check with timeout
|
|
77
|
+
await Promise.race([
|
|
78
|
+
check.fn(),
|
|
79
|
+
new Promise((_, reject) => {
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
reject(new Error(`Health check timeout: ${name}`));
|
|
82
|
+
}, this.config.timeout);
|
|
83
|
+
})
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
check.lastSuccess = Date.now();
|
|
87
|
+
check.failures = 0;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
status = 'unhealthy';
|
|
90
|
+
message = error.message;
|
|
91
|
+
check.lastFailure = Date.now();
|
|
92
|
+
check.failures++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
check.status = status;
|
|
96
|
+
check.lastCheck = Date.now();
|
|
97
|
+
|
|
98
|
+
checkResults.push({
|
|
99
|
+
name,
|
|
100
|
+
status,
|
|
101
|
+
message,
|
|
102
|
+
duration: Date.now() - checkStart
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Calculate overall status
|
|
107
|
+
const overallStatus = this.calculateOverallStatus(checkResults);
|
|
108
|
+
this.updateStatus(overallStatus);
|
|
109
|
+
|
|
110
|
+
const result = {
|
|
111
|
+
status: overallStatus,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
checks: checkResults,
|
|
114
|
+
metadata: {
|
|
115
|
+
duration: Date.now() - startTime,
|
|
116
|
+
totalChecks: checkResults.length
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Notify listeners
|
|
121
|
+
this.notifyListeners(result);
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Calculates overall health status from check results
|
|
128
|
+
* @param {Array<Object>} checkResults - Individual check results
|
|
129
|
+
* @returns {string} Overall status
|
|
130
|
+
*/
|
|
131
|
+
calculateOverallStatus(checkResults) {
|
|
132
|
+
const unhealthyCount = checkResults.filter(c => c.status === 'unhealthy').length;
|
|
133
|
+
const totalCount = checkResults.length;
|
|
134
|
+
|
|
135
|
+
if (totalCount === 0) {
|
|
136
|
+
return 'healthy';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (unhealthyCount === 0) {
|
|
140
|
+
return 'healthy';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (unhealthyCount === totalCount) {
|
|
144
|
+
return 'unhealthy';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return 'degraded';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Updates health status with thresholds
|
|
152
|
+
* @param {string} newStatus - New status
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
updateStatus(newStatus) {
|
|
156
|
+
if (newStatus === 'unhealthy') {
|
|
157
|
+
this.consecutiveFailures++;
|
|
158
|
+
this.consecutiveSuccesses = 0;
|
|
159
|
+
|
|
160
|
+
if (this.consecutiveFailures >= this.config.unhealthyThreshold) {
|
|
161
|
+
this.status = 'unhealthy';
|
|
162
|
+
}
|
|
163
|
+
} else if (newStatus === 'healthy') {
|
|
164
|
+
this.consecutiveSuccesses++;
|
|
165
|
+
this.consecutiveFailures = 0;
|
|
166
|
+
|
|
167
|
+
if (this.consecutiveSuccesses >= this.config.healthyThreshold) {
|
|
168
|
+
this.status = 'healthy';
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
this.status = 'degraded';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Starts periodic health checks
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
start() {
|
|
180
|
+
if (this.intervalId) {
|
|
181
|
+
return; // Already running
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Initial check
|
|
185
|
+
this.check();
|
|
186
|
+
|
|
187
|
+
// Schedule periodic checks
|
|
188
|
+
this.intervalId = setInterval(() => {
|
|
189
|
+
this.check();
|
|
190
|
+
}, this.config.interval);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Stops periodic health checks
|
|
195
|
+
* @returns {void}
|
|
196
|
+
*/
|
|
197
|
+
stop() {
|
|
198
|
+
if (this.intervalId) {
|
|
199
|
+
clearInterval(this.intervalId);
|
|
200
|
+
this.intervalId = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Gets current health status
|
|
206
|
+
* @returns {string} Current status
|
|
207
|
+
*/
|
|
208
|
+
getStatus() {
|
|
209
|
+
return this.status;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Checks if system is healthy
|
|
214
|
+
* @returns {boolean} True if healthy
|
|
215
|
+
*/
|
|
216
|
+
isHealthy() {
|
|
217
|
+
return this.status === 'healthy';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Gets all registered checks
|
|
222
|
+
* @returns {Array<Object>} Health checks
|
|
223
|
+
*/
|
|
224
|
+
getChecks() {
|
|
225
|
+
return Array.from(this.checks.values());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Adds a status change listener
|
|
230
|
+
* @param {Function} listener - Listener function
|
|
231
|
+
* @returns {Function} Unsubscribe function
|
|
232
|
+
*/
|
|
233
|
+
onStatusChange(listener) {
|
|
234
|
+
this.listeners.add(listener);
|
|
235
|
+
return () => this.listeners.delete(listener);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Notifies all listeners of health check result
|
|
240
|
+
* @param {Object} result - Health check result
|
|
241
|
+
* @returns {void}
|
|
242
|
+
*/
|
|
243
|
+
notifyListeners(result) {
|
|
244
|
+
for (const listener of this.listeners) {
|
|
245
|
+
try {
|
|
246
|
+
listener(result);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error('Health monitor listener error:', error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Gets health statistics
|
|
255
|
+
* @returns {Object} Health statistics
|
|
256
|
+
*/
|
|
257
|
+
getStats() {
|
|
258
|
+
const stats = {
|
|
259
|
+
overall: this.status,
|
|
260
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
261
|
+
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
262
|
+
checks: {}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
for (const [name, check] of this.checks) {
|
|
266
|
+
stats.checks[name] = {
|
|
267
|
+
status: check.status,
|
|
268
|
+
failures: check.failures,
|
|
269
|
+
lastCheck: check.lastCheck,
|
|
270
|
+
lastSuccess: check.lastSuccess,
|
|
271
|
+
lastFailure: check.lastFailure
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return stats;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Resets health monitor state
|
|
280
|
+
* @returns {void}
|
|
281
|
+
*/
|
|
282
|
+
reset() {
|
|
283
|
+
this.status = 'healthy';
|
|
284
|
+
this.consecutiveFailures = 0;
|
|
285
|
+
this.consecutiveSuccesses = 0;
|
|
286
|
+
|
|
287
|
+
for (const [, check] of this.checks) {
|
|
288
|
+
check.status = 'healthy';
|
|
289
|
+
check.failures = 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Creates a new health monitor instance
|
|
296
|
+
* @param {Object} [config] - Health check configuration
|
|
297
|
+
* @returns {HealthMonitor} Health monitor instance
|
|
298
|
+
*/
|
|
299
|
+
export function createHealthMonitor(config) {
|
|
300
|
+
return new HealthMonitor(config);
|
|
301
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Self-Healing Workflows - Main export
|
|
3
|
+
* @module @unrdf/self-healing-workflows
|
|
4
|
+
* @description Automatic error recovery system with 85-95% success rate
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Main engine
|
|
8
|
+
export {
|
|
9
|
+
SelfHealingEngine,
|
|
10
|
+
createSelfHealingEngine
|
|
11
|
+
} from './self-healing-engine.mjs';
|
|
12
|
+
|
|
13
|
+
// Error classification
|
|
14
|
+
export {
|
|
15
|
+
ErrorClassifier,
|
|
16
|
+
createErrorClassifier
|
|
17
|
+
} from './error-classifier.mjs';
|
|
18
|
+
|
|
19
|
+
// Retry strategies
|
|
20
|
+
export {
|
|
21
|
+
RetryStrategy,
|
|
22
|
+
createRetryStrategy,
|
|
23
|
+
immediateRetry,
|
|
24
|
+
exponentialRetry
|
|
25
|
+
} from './retry-strategy.mjs';
|
|
26
|
+
|
|
27
|
+
// Circuit breaker
|
|
28
|
+
export {
|
|
29
|
+
CircuitBreaker,
|
|
30
|
+
createCircuitBreaker
|
|
31
|
+
} from './circuit-breaker.mjs';
|
|
32
|
+
|
|
33
|
+
// Recovery actions
|
|
34
|
+
export {
|
|
35
|
+
RecoveryActionExecutor,
|
|
36
|
+
createRecoveryActionExecutor
|
|
37
|
+
} from './recovery-actions.mjs';
|
|
38
|
+
|
|
39
|
+
// Health monitoring
|
|
40
|
+
export {
|
|
41
|
+
HealthMonitor,
|
|
42
|
+
createHealthMonitor
|
|
43
|
+
} from './health-monitor.mjs';
|
|
44
|
+
|
|
45
|
+
// Schemas
|
|
46
|
+
export * from './schemas.mjs';
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Recovery action catalog
|
|
3
|
+
* @module @unrdf/self-healing-workflows/recovery
|
|
4
|
+
* @description Library of recovery actions for error handling
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { RecoveryActionSchema } from './schemas.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recovery action executor
|
|
11
|
+
*/
|
|
12
|
+
export class RecoveryActionExecutor {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new recovery action executor
|
|
15
|
+
*/
|
|
16
|
+
constructor() {
|
|
17
|
+
this.actions = new Map();
|
|
18
|
+
this.stats = new Map();
|
|
19
|
+
this.registerDefaultActions();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Registers default recovery actions
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
registerDefaultActions() {
|
|
27
|
+
// Retry action
|
|
28
|
+
this.register({
|
|
29
|
+
type: 'retry',
|
|
30
|
+
name: 'retry-operation',
|
|
31
|
+
execute: async (context) => {
|
|
32
|
+
const { operation, maxAttempts = 3 } = context;
|
|
33
|
+
let attempt = 0;
|
|
34
|
+
let lastError;
|
|
35
|
+
|
|
36
|
+
while (attempt < maxAttempts) {
|
|
37
|
+
try {
|
|
38
|
+
return await operation();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
lastError = error;
|
|
41
|
+
attempt++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw lastError;
|
|
46
|
+
},
|
|
47
|
+
condition: (error) => error.retryable,
|
|
48
|
+
priority: 80
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Skip action
|
|
52
|
+
this.register({
|
|
53
|
+
type: 'skip',
|
|
54
|
+
name: 'skip-and-continue',
|
|
55
|
+
execute: async (context) => {
|
|
56
|
+
return { skipped: true, reason: context.error?.message };
|
|
57
|
+
},
|
|
58
|
+
priority: 20
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Compensate action
|
|
62
|
+
this.register({
|
|
63
|
+
type: 'compensate',
|
|
64
|
+
name: 'compensating-transaction',
|
|
65
|
+
execute: async (context) => {
|
|
66
|
+
const { compensationFn } = context;
|
|
67
|
+
if (compensationFn) {
|
|
68
|
+
await compensationFn();
|
|
69
|
+
}
|
|
70
|
+
return { compensated: true };
|
|
71
|
+
},
|
|
72
|
+
priority: 60
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Restart action
|
|
76
|
+
this.register({
|
|
77
|
+
type: 'restart',
|
|
78
|
+
name: 'restart-workflow',
|
|
79
|
+
execute: async (context) => {
|
|
80
|
+
const { workflow } = context;
|
|
81
|
+
if (workflow && workflow.restart) {
|
|
82
|
+
await workflow.restart();
|
|
83
|
+
}
|
|
84
|
+
return { restarted: true };
|
|
85
|
+
},
|
|
86
|
+
priority: 40
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Fallback action
|
|
90
|
+
this.register({
|
|
91
|
+
type: 'fallback',
|
|
92
|
+
name: 'use-fallback',
|
|
93
|
+
execute: async (context) => {
|
|
94
|
+
const { fallbackFn } = context;
|
|
95
|
+
if (fallbackFn) {
|
|
96
|
+
return await fallbackFn();
|
|
97
|
+
}
|
|
98
|
+
throw new Error('No fallback function provided');
|
|
99
|
+
},
|
|
100
|
+
priority: 50
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Manual intervention action
|
|
104
|
+
this.register({
|
|
105
|
+
type: 'manual',
|
|
106
|
+
name: 'require-manual-intervention',
|
|
107
|
+
execute: async (context) => {
|
|
108
|
+
const { notificationFn, error } = context;
|
|
109
|
+
if (notificationFn) {
|
|
110
|
+
await notificationFn({
|
|
111
|
+
type: 'manual-intervention-required',
|
|
112
|
+
error: error?.originalError || error,
|
|
113
|
+
timestamp: Date.now()
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return { requiresManualIntervention: true };
|
|
117
|
+
},
|
|
118
|
+
priority: 10
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Registers a recovery action
|
|
124
|
+
* @param {Object} action - Recovery action definition
|
|
125
|
+
* @returns {void}
|
|
126
|
+
*/
|
|
127
|
+
register(action) {
|
|
128
|
+
const validated = RecoveryActionSchema.parse(action);
|
|
129
|
+
const key = `${validated.type}:${validated.name}`;
|
|
130
|
+
this.actions.set(key, validated);
|
|
131
|
+
this.stats.set(key, {
|
|
132
|
+
attempts: 0,
|
|
133
|
+
successes: 0,
|
|
134
|
+
failures: 0
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Executes a recovery action
|
|
140
|
+
* @param {string} type - Action type
|
|
141
|
+
* @param {string} name - Action name
|
|
142
|
+
* @param {Object} context - Execution context
|
|
143
|
+
* @returns {Promise<Object>} Recovery result
|
|
144
|
+
* @throws {Error} If action not found or execution fails
|
|
145
|
+
*/
|
|
146
|
+
async execute(type, name, context) {
|
|
147
|
+
const key = `${type}:${name}`;
|
|
148
|
+
const action = this.actions.get(key);
|
|
149
|
+
|
|
150
|
+
if (!action) {
|
|
151
|
+
throw new Error(`Recovery action not found: ${key}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stats = this.stats.get(key);
|
|
155
|
+
stats.attempts++;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const result = await action.execute(context);
|
|
159
|
+
stats.successes++;
|
|
160
|
+
return result;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
stats.failures++;
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Selects best recovery action for an error
|
|
169
|
+
* @param {Object} classifiedError - Classified error object
|
|
170
|
+
* @param {Object} [_context] - Additional context
|
|
171
|
+
* @returns {Object|null} Selected action or null
|
|
172
|
+
*/
|
|
173
|
+
selectAction(classifiedError, _context = {}) {
|
|
174
|
+
const candidates = [];
|
|
175
|
+
|
|
176
|
+
for (const [key, action] of this.actions) {
|
|
177
|
+
// Check if action condition matches
|
|
178
|
+
if (action.condition && !action.condition(classifiedError)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
candidates.push({ key, action });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (candidates.length === 0) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Sort by priority (highest first)
|
|
190
|
+
candidates.sort((a, b) => b.action.priority - a.action.priority);
|
|
191
|
+
|
|
192
|
+
return candidates[0];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Executes best recovery action for an error
|
|
197
|
+
* @param {Object} classifiedError - Classified error object
|
|
198
|
+
* @param {Object} context - Execution context
|
|
199
|
+
* @returns {Promise<Object>} Recovery result
|
|
200
|
+
* @throws {Error} If no suitable action found or execution fails
|
|
201
|
+
*/
|
|
202
|
+
async recover(classifiedError, context) {
|
|
203
|
+
const selected = this.selectAction(classifiedError, context);
|
|
204
|
+
|
|
205
|
+
if (!selected) {
|
|
206
|
+
throw new Error('No suitable recovery action found');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { action } = selected;
|
|
210
|
+
return this.execute(action.type, action.name, {
|
|
211
|
+
...context,
|
|
212
|
+
error: classifiedError
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Gets all registered actions
|
|
218
|
+
* @returns {Array<Object>} Registered actions
|
|
219
|
+
*/
|
|
220
|
+
getActions() {
|
|
221
|
+
return Array.from(this.actions.values());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Gets action statistics
|
|
226
|
+
* @returns {Object} Statistics by action
|
|
227
|
+
*/
|
|
228
|
+
getStats() {
|
|
229
|
+
const stats = {};
|
|
230
|
+
for (const [key, stat] of this.stats) {
|
|
231
|
+
stats[key] = {
|
|
232
|
+
...stat,
|
|
233
|
+
successRate: stat.attempts > 0 ? stat.successes / stat.attempts : 0
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return stats;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resets action statistics
|
|
241
|
+
* @returns {void}
|
|
242
|
+
*/
|
|
243
|
+
resetStats() {
|
|
244
|
+
for (const [key] of this.stats) {
|
|
245
|
+
this.stats.set(key, {
|
|
246
|
+
attempts: 0,
|
|
247
|
+
successes: 0,
|
|
248
|
+
failures: 0
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Removes a recovery action
|
|
255
|
+
* @param {string} type - Action type
|
|
256
|
+
* @param {string} name - Action name
|
|
257
|
+
* @returns {boolean} True if action was removed
|
|
258
|
+
*/
|
|
259
|
+
unregister(type, name) {
|
|
260
|
+
const key = `${type}:${name}`;
|
|
261
|
+
this.stats.delete(key);
|
|
262
|
+
return this.actions.delete(key);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Creates a new recovery action executor
|
|
268
|
+
* @returns {RecoveryActionExecutor} Recovery action executor
|
|
269
|
+
*/
|
|
270
|
+
export function createRecoveryActionExecutor() {
|
|
271
|
+
return new RecoveryActionExecutor();
|
|
272
|
+
}
|