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