bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
489 lines (417 loc) • 16.3 kB
JavaScript
/**
* 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;