UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

406 lines (361 loc) 11 kB
/** * Health Check Engine * * Core execution engine for health checks. Handles parallel execution, * caching, timeout management, and result aggregation. * * @module aios-core/health-check/engine * @version 1.0.0 * @story HCS-2 - Health Check System Implementation */ const { CheckStatus, CheckSeverity } = require('./base-check'); /** * Simple in-memory cache for check results * @private */ class ResultCache { constructor(ttl = 300000) { this.cache = new Map(); this.ttl = ttl; } /** * Get cached result * @param {string} key - Cache key * @returns {Object|null} Cached result or null */ get(key) { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() - entry.timestamp > this.ttl) { this.cache.delete(key); return null; } return entry.value; } /** * Set cached result * @param {string} key - Cache key * @param {Object} value - Value to cache */ set(key, value) { this.cache.set(key, { value, timestamp: Date.now(), }); } /** * Clear all cached results */ clear() { this.cache.clear(); } /** * Get cache statistics * @returns {Object} Cache stats */ getStats() { return { size: this.cache.size, ttl: this.ttl, }; } } /** * Health Check Engine * * Executes health checks with support for: * - Parallel execution within domains * - Result caching * - Timeout management * - Fail-fast for critical issues * * @class HealthCheckEngine */ class HealthCheckEngine { /** * Create a new HealthCheckEngine * @param {Object} config - Engine configuration * @param {boolean} [config.parallel=true] - Enable parallel execution * @param {Object} [config.cache] - Cache configuration * @param {Object} [config.performance] - Performance settings */ constructor(config = {}) { this.config = config; this.parallel = config.parallel !== false; this.cache = new ResultCache(config.cache?.ttl || 300000); this.cacheEnabled = config.cache?.enabled !== false; this.timeouts = { quick: config.performance?.quickModeTimeout || 10000, full: config.performance?.fullModeTimeout || 60000, }; this.startTime = null; this.results = []; this.errors = []; } /** * Run all provided checks * * @param {BaseCheck[]} checks - Array of checks to run * @param {Object} runConfig - Runtime configuration * @returns {Promise<Object[]>} Array of check results */ async runChecks(checks, runConfig = {}) { this.startTime = Date.now(); this.results = []; this.errors = []; const mode = runConfig.mode || 'quick'; const timeout = this.timeouts[mode] || this.timeouts.quick; // Group checks by domain for parallel execution const _checksByDomain = this.groupByDomain(checks); // Execute checks by priority const _priorityOrder = [ CheckSeverity.CRITICAL, CheckSeverity.HIGH, CheckSeverity.MEDIUM, CheckSeverity.LOW, CheckSeverity.INFO, ]; try { // Run CRITICAL checks first const criticalChecks = checks.filter((c) => c.severity === CheckSeverity.CRITICAL); if (criticalChecks.length > 0) { const criticalResults = await this.runCheckGroup(criticalChecks, timeout, runConfig); this.results.push(...criticalResults); // Fail-fast in quick mode if critical failures if (mode === 'quick' && this.hasCriticalFailure(criticalResults)) { return this.results; } } // Run remaining checks by domain (parallel) const remainingChecks = checks.filter((c) => c.severity !== CheckSeverity.CRITICAL); if (this.parallel) { // Group by domain and run domains in parallel const domainGroups = this.groupByDomain(remainingChecks); const domainPromises = Object.entries(domainGroups).map(([_domain, domainChecks]) => this.runCheckGroup(domainChecks, timeout, runConfig), ); const domainResults = await Promise.all(domainPromises); for (const results of domainResults) { this.results.push(...results); } } else { // Run sequentially const results = await this.runCheckGroup(remainingChecks, timeout, runConfig); this.results.push(...results); } return this.results; } catch (error) { this.errors.push({ message: error.message, timestamp: Date.now(), }); throw error; } } /** * Run a group of checks (with optional parallelization within group) * @private * @param {BaseCheck[]} checks - Checks to run * @param {number} timeout - Timeout in milliseconds * @param {Object} runConfig - Runtime configuration * @returns {Promise<Object[]>} Check results */ async runCheckGroup(checks, timeout, runConfig) { if (checks.length === 0) return []; const results = []; const remainingTime = timeout - (Date.now() - this.startTime); if (remainingTime <= 0) { // Timeout exceeded, mark remaining as skipped return checks.map((check) => this.createSkippedResult(check, 'Timeout exceeded')); } if (this.parallel) { // Run in parallel with timeout const promises = checks.map((check) => this.runSingleCheck(check, remainingTime, runConfig)); const settledResults = await Promise.allSettled(promises); for (let i = 0; i < settledResults.length; i++) { const settled = settledResults[i]; if (settled.status === 'fulfilled') { results.push(settled.value); } else { results.push(this.createErrorResult(checks[i], settled.reason)); } } } else { // Run sequentially for (const check of checks) { const elapsedTime = Date.now() - this.startTime; if (elapsedTime >= timeout) { results.push(this.createSkippedResult(check, 'Timeout exceeded')); continue; } try { const result = await this.runSingleCheck(check, timeout - elapsedTime, runConfig); results.push(result); } catch (error) { results.push(this.createErrorResult(check, error)); } } } return results; } /** * Run a single check with timeout * @private * @param {BaseCheck} check - Check to run * @param {number} timeout - Timeout in milliseconds * @param {Object} runConfig - Runtime configuration * @returns {Promise<Object>} Check result */ async runSingleCheck(check, timeout, runConfig) { // Check cache first if (this.cacheEnabled && check.cacheable !== false) { const cached = this.cache.get(check.id); if (cached) { return { ...cached, fromCache: true }; } } const startTime = Date.now(); let timeoutId = null; try { // Create timeout promise with clearable timeout // Story TD-6: Fix Jest worker leak by properly clearing timeouts const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout( () => reject(new Error('Check timeout')), Math.min(timeout, check.timeout || 5000), ); }); // Execute check with timeout const result = await Promise.race([check.execute(runConfig), timeoutPromise]); // Clear timeout to prevent Jest worker leak (TD-6) if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } const checkResult = { checkId: check.id, name: check.name, domain: check.domain, severity: check.severity, status: result.status, message: result.message, details: result.details || null, recommendation: result.recommendation || null, healable: result.healable || false, healingTier: result.healingTier || 0, duration: Date.now() - startTime, timestamp: new Date().toISOString(), fromCache: false, }; // Cache successful results if (this.cacheEnabled && check.cacheable !== false) { this.cache.set(check.id, checkResult); } return checkResult; } catch (error) { // Clear timeout on error as well (TD-6) if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } return this.createErrorResult(check, error, Date.now() - startTime); } } /** * Create an error result * @private * @param {BaseCheck} check - The check that failed * @param {Error} error - The error * @param {number} duration - Execution duration * @returns {Object} Error result */ createErrorResult(check, error, duration = 0) { return { checkId: check.id, name: check.name, domain: check.domain, severity: check.severity, status: CheckStatus.ERROR, message: `Check failed: ${error.message}`, details: { error: error.message }, recommendation: 'Investigate check execution error', healable: false, healingTier: 0, duration, timestamp: new Date().toISOString(), fromCache: false, }; } /** * Create a skipped result * @private * @param {BaseCheck} check - The skipped check * @param {string} reason - Skip reason * @returns {Object} Skipped result */ createSkippedResult(check, reason) { return { checkId: check.id, name: check.name, domain: check.domain, severity: check.severity, status: CheckStatus.SKIPPED, message: reason, details: null, recommendation: 'Run in full mode or increase timeout', healable: false, healingTier: 0, duration: 0, timestamp: new Date().toISOString(), fromCache: false, }; } /** * Check if results contain critical failures * @private * @param {Object[]} results - Check results * @returns {boolean} True if critical failure exists */ hasCriticalFailure(results) { return results.some( (r) => r.severity === CheckSeverity.CRITICAL && (r.status === CheckStatus.FAIL || r.status === CheckStatus.ERROR), ); } /** * Group checks by domain * @private * @param {BaseCheck[]} checks - Checks to group * @returns {Object} Checks grouped by domain */ groupByDomain(checks) { return checks.reduce((acc, check) => { const domain = check.domain || 'unknown'; if (!acc[domain]) { acc[domain] = []; } acc[domain].push(check); return acc; }, {}); } /** * Clear the result cache */ clearCache() { this.cache.clear(); } /** * Get execution statistics * @returns {Object} Execution stats */ getStats() { return { totalDuration: this.startTime ? Date.now() - this.startTime : 0, checksRun: this.results.length, errors: this.errors.length, cacheStats: this.cache.getStats(), }; } } module.exports = HealthCheckEngine;