UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

489 lines (417 loc) 16.3 kB
/** * Manages pipeline configuration, registration, and execution * @class PipelineManager */ class PipelineManager { /** * Creates an instance of PipelineManager * @param {Object} options - Manager options * @param {Object} [options.logger] - Logger instance * @param {Object} [options.config] - Configuration object * @param {Object} [options.serviceRegistry] - Service registry * @param {Object} [options.pipelineConfig] - Pipeline configuration * @param {Object} [options.processorFactory] - Processor factory * @param {Object} [options.performanceMonitor] - Performance monitoring * @param {Object} [options.errorHandler] - Error handling service */ constructor(options = {}) { if (!options.logger) { throw new Error('Logger is required for PipelineManager'); } if (!options.pipelineConfig) { throw new Error('PipelineConfig is required for PipelineManager'); } if (!options.processorFactory) { throw new Error('ProcessorFactory is required for PipelineManager'); } if (!options.errorHandler) { throw new Error('ErrorHandler is required for PipelineManager'); } if (!options.performanceMonitor) { throw new Error('PerformanceMonitor is required for PipelineManager'); } this.logger = options.logger; this.config = options.config || {}; this.serviceRegistry = options.serviceRegistry; this.errorHandler = options.errorHandler; this.performanceMonitor = options.performanceMonitor; // Pipeline definitions this.pipelines = new Map(); // Pipeline execution history this.executionHistory = []; this.maxHistorySize = 100; // Pipeline stages this.stages = new Map(); // Active pipeline executions this.activeExecutions = new Map(); // Pipeline configuration and processor factory this.pipelineConfig = options.pipelineConfig; this.processorFactory = options.processorFactory; } /** * Initialize pipeline manager from configuration * @param {Object} [options={}] - Initialization options * @param {String} [options.configPath] - Path to configuration file * @returns {Promise<PipelineManager>} - For method chaining */ async initialize(options = {}) { const initSpan = this.performanceMonitor.startSpan('pipeline-manager-init'); try { // Load pipeline configuration await this.loadPipelineConfiguration(options.configPath); this.logger.info('Pipeline manager initialized successfully'); this.performanceMonitor.endSpan(initSpan); return this; } catch (error) { this.logger.error('Failed to initialize pipeline manager:', error); this.performanceMonitor.failSpan(initSpan, error); await this.errorHandler.handleError('PipelineManager.initialize', error, { options }); throw error; } } /** * Load pipeline configuration from config file * @param {String} [configPath] - Path to pipeline config * @returns {Promise<PipelineManager>} - For method chaining */ async loadPipelineConfiguration(configPath) { const configSpan = this.performanceMonitor.startSpan('load-pipeline-config'); try { if (!this.pipelineConfig) { this.performanceMonitor.failSpan(configSpan, new Error('PipelineConfig not initialized')); throw new Error('PipelineConfig required to load configuration'); } // Load configuration from file if specified if (configPath) { await this.pipelineConfig.loadFromFile(configPath); } // Load processor configurations into the processor factory const factoryConfig = this.pipelineConfig.buildProcessorFactoryConfig(); this.processorFactory.loadFromConfig(factoryConfig); const processorCount = factoryConfig.processors ? Object.keys(factoryConfig.processors).length : 0; this.logger.info(`Loaded ${processorCount} processor definitions into factory`); // Register pipelines from config const pipelines = this.pipelineConfig.getAllPipelineConfigs(); for (const pipeline of pipelines) { this.registerPipeline(pipeline.id, pipeline); } this.logger.info(`Registered ${pipelines.length} pipelines from configuration`); this.performanceMonitor.endSpan(configSpan, { processorCount, pipelineCount: pipelines.length }); return this; } catch (error) { this.logger.error('Failed to load pipeline configuration:', error); this.performanceMonitor.failSpan(configSpan, error); await this.errorHandler.handleError('PipelineManager.loadPipelineConfiguration', error, { configPath }); throw error; } } /** * Register a pipeline definition * @param {String} id - Pipeline identifier * @param {Object} definition - Pipeline definition * @param {String} [definition.name] - Human-readable name * @param {String} [definition.description] - Pipeline description * @param {Array<Object>} [definition.stages] - Pipeline stages * @param {String} [definition.template] - Pipeline template to use * @param {Object} [definition.config] - Pipeline-specific configuration * @param {Object} [definition.validation] - Validation rules * @returns {PipelineManager} - For method chaining */ registerPipeline(id, definition) { if (!id || typeof id !== 'string') { throw new Error('Pipeline ID must be a non-empty string'); } if (!definition || typeof definition !== 'object') { throw new Error('Pipeline definition must be an object'); } if (!definition.name) { throw new Error('Pipeline must have a name'); } if (!Array.isArray(definition.stages) || definition.stages.length === 0) { throw new Error('Pipeline must have at least one stage'); } // Validate stages - they must exist in the processor factory for (const stage of definition.stages) { if (typeof stage === 'string') { if (!this.processorFactory.hasType(stage)) { throw new Error(`Unknown processor type: ${stage}`); } } else if (typeof stage === 'object') { if (!stage.id || !stage.type || !this.processorFactory.hasType(stage.type)) { throw new Error(`Invalid stage definition: ${JSON.stringify(stage)}`); } } else { throw new Error('Pipeline stage must be a string or object'); } } // Normalize the pipeline definition const normalizedDefinition = { id, name: definition.name || id, description: definition.description || '', stages: definition.stages || [], config: { ...(definition.config || {}) }, validation: { ...(definition.validation || {}) }, options: { ...(definition.options || {}) } }; // Register the pipeline this.pipelines.set(id, normalizedDefinition); this.logger.debug(`Registered pipeline: ${id}`); return this; } /** * Execute a pipeline * @param {String} pipelineId - Pipeline ID * @param {Object} input - Input data * @param {Object} [options={}] - Execution options * @returns {Promise<Object>} - Execution result */ async executePipeline(pipelineId, input, options = {}) { if (!this.pipelines.has(pipelineId)) { throw new Error(`Pipeline not found: ${pipelineId}`); } const pipelineDefinition = this.pipelines.get(pipelineId); const executionId = options.executionId || this._generateExecutionId(); const pipelineSpan = this.performanceMonitor.startSpan(`pipeline:${pipelineId}`, { executionId }); // Create execution context const context = { executionId, pipeline: pipelineDefinition, options: { ...pipelineDefinition.options, ...options }, startTime: Date.now(), stages: [], errors: [], stageResults: new Map(), logger: options.logger || this.logger, serviceRegistry: options.serviceRegistry || this.serviceRegistry, performanceMonitor: this.performanceMonitor, data: { input } }; // Record start of execution this.activeExecutions.set(executionId, context); try { this.logger.info(`Starting pipeline execution: ${pipelineId} (${executionId})`); // Create and initialize processors for each stage await this._initializeStages(context); // Execute each stage in sequence for (const stage of context.stages) { const stageSpan = this.performanceMonitor.startSpan(`stage:${stage.id}`, { executionId }); try { this.logger.debug(`Executing stage: ${stage.id}`); // Execute the stage const input = context.data.current || context.data.input; const result = await stage.processor.process(input, { pipelineContext: context, stageId: stage.id, executionId, options: stage.options }); // Store the result context.stageResults.set(stage.id, result); context.data.current = result; this.performanceMonitor.endSpan(stageSpan, { stageId: stage.id, processorType: stage.type, successful: true }); } catch (error) { this.logger.error(`Error executing stage ${stage.id}:`, error); // Record the error context.errors.push({ stage: stage.id, error: error.message, stack: error.stack, timestamp: new Date().toISOString() }); this.performanceMonitor.failSpan(stageSpan, error); // Handle the error based on pipeline configuration if (context.options.continueOnError) { this.logger.warn(`Continuing pipeline after error in stage ${stage.id}`); } else { throw new Error(`Pipeline execution failed at stage ${stage.id}: ${error.message}`); } } } // Prepare final result const result = { ...context.data.current, _metadata: { pipelineId, executionId, startTime: context.startTime, endTime: Date.now(), duration: Date.now() - context.startTime, stageCount: context.stages.length, errorCount: context.errors.length, errors: context.errors } }; // Add to execution history this._addToExecutionHistory(context, result); // Complete the pipeline span this.performanceMonitor.endSpan(pipelineSpan, { pipelineId, successful: true, stageCount: context.stages.length, errorCount: context.errors.length, duration: Date.now() - context.startTime }); this.logger.info(`Pipeline execution completed: ${pipelineId} (${executionId})`); return result; } catch (error) { this.logger.error(`Pipeline execution failed: ${pipelineId} (${executionId})`, error); // Add to execution history this._addToExecutionHistory(context, null, error); // Fail the pipeline span this.performanceMonitor.failSpan(pipelineSpan, error); // Handle the error await this.errorHandler.handleError('PipelineManager.executePipeline', error, { pipelineId, executionId, context }); throw error; } finally { // Remove from active executions this.activeExecutions.delete(executionId); } } /** * Initialize processors for all stages in the pipeline * @param {Object} context - Execution context * @returns {Promise<void>} * @private */ async _initializeStages(context) { const pipeline = context.pipeline; const stages = []; for (const stageConfig of pipeline.stages) { let stageId, stageType, stageOptions; if (typeof stageConfig === 'string') { stageId = stageConfig; stageType = stageConfig; stageOptions = {}; } else { stageId = stageConfig.id; stageType = stageConfig.type; stageOptions = stageConfig.options || {}; } try { // Create and initialize the processor const processor = await this.processorFactory.createAndInitialize(stageType, { name: stageId, ...stageOptions }); stages.push({ id: stageId, type: stageType, processor, options: stageOptions }); } catch (error) { this.logger.error(`Failed to initialize stage ${stageId}:`, error); throw new Error(`Failed to initialize stage ${stageId}: ${error.message}`); } } context.stages = stages; } /** * Generate a unique execution ID * @returns {string} Execution ID * @private */ _generateExecutionId() { return `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Add an execution to the history * @param {Object} context - Execution context * @param {Object} result - Execution result * @param {Error} [error] - Execution error * @private */ _addToExecutionHistory(context, result, error) { const historyEntry = { executionId: context.executionId, pipelineId: context.pipeline.id, startTime: context.startTime, endTime: Date.now(), duration: Date.now() - context.startTime, stageCount: context.stages.length, stages: context.stages.map(s => s.id), errorCount: context.errors.length, errors: context.errors, status: error ? 'failed' : 'completed', result: result }; this.executionHistory.unshift(historyEntry); // Limit history size if (this.executionHistory.length > this.maxHistorySize) { this.executionHistory = this.executionHistory.slice(0, this.maxHistorySize); } } /** * Get pipeline execution history * @param {Object} [options] - Options for filtering * @param {Number} [options.limit] - Maximum number of entries to return * @param {String} [options.pipelineId] - Filter by pipeline ID * @param {String} [options.status] - Filter by status * @returns {Array<Object>} Execution history */ getExecutionHistory(options = {}) { let history = [...this.executionHistory]; // Apply filters if (options.pipelineId) { history = history.filter(entry => entry.pipelineId === options.pipelineId); } if (options.status) { history = history.filter(entry => entry.status === options.status); } // Apply limit if (options.limit && options.limit > 0) { history = history.slice(0, options.limit); } return history; } /** * Get all registered pipelines * @returns {Array<Object>} Array of pipeline definitions */ getAllPipelines() { return Array.from(this.pipelines.values()); } /** * Get a pipeline by ID * @param {String} id - Pipeline ID * @returns {Object|null} Pipeline definition or null if not found */ getPipeline(id) { return this.pipelines.get(id) || null; } /** * Get performance metrics for pipelines * @param {Object} [options] - Options for filtering * @param {String} [options.pipelineId] - Filter by pipeline ID * @param {Number} [options.timeframe] - Timeframe in ms to look back * @returns {Object} Performance metrics */ getPerformanceMetrics(options = {}) { return this.performanceMonitor.getMetrics('pipeline', options); } /** * Get active pipeline executions * @returns {Array<Object>} Active executions */ getActiveExecutions() { return Array.from(this.activeExecutions.values()).map(context => ({ executionId: context.executionId, pipelineId: context.pipeline.id, startTime: context.startTime, duration: Date.now() - context.startTime, stageCount: context.stages.length, currentStage: context.currentStage, stages: context.stages.map(s => s.id) })); } } module.exports = PipelineManager;