File size: 22,755 Bytes
c1dc8ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
"""Graceful failure handling and partial processing recovery system."""

import logging
import time
import json
import traceback
from typing import Any, Dict, List, Optional, Callable, Union
from pathlib import Path
from dataclasses import dataclass, asdict
from enum import Enum
import threading
from contextlib import contextmanager

from .base import PipelineStage, CharacterAttributes, ProcessingResult

logger = logging.getLogger(__name__)

class FailureType(Enum):
    """Types of failures that can occur during processing."""
    NETWORK_ERROR = "network_error"
    MODEL_ERROR = "model_error"
    MEMORY_ERROR = "memory_error"
    IO_ERROR = "io_error"
    VALIDATION_ERROR = "validation_error"
    TIMEOUT_ERROR = "timeout_error"
    UNKNOWN_ERROR = "unknown_error"

@dataclass
class FailureRecord:
    """Record of a processing failure."""
    item_id: str
    failure_type: FailureType
    error_message: str
    timestamp: float
    retry_count: int
    stack_trace: Optional[str] = None
    context: Optional[Dict[str, Any]] = None

class CircuitBreaker:
    """Circuit breaker pattern for handling cascading failures."""
    
    def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = 0
        self.state = 'CLOSED'  # CLOSED, OPEN, HALF_OPEN
        self.lock = threading.Lock()
    
    @contextmanager
    def call(self):
        """Execute operation with circuit breaker protection."""
        with self.lock:
            if self.state == 'OPEN':
                if time.time() - self.last_failure_time > self.recovery_timeout:
                    self.state = 'HALF_OPEN'
                    logger.info("Circuit breaker entering HALF_OPEN state")
                else:
                    raise Exception("Circuit breaker is OPEN - operation blocked")
        
        try:
            yield
            # Success - reset if in HALF_OPEN
            with self.lock:
                if self.state == 'HALF_OPEN':
                    self.state = 'CLOSED'
                    self.failure_count = 0
                    logger.info("Circuit breaker reset to CLOSED state")
        
        except Exception as e:
            with self.lock:
                self.failure_count += 1
                self.last_failure_time = time.time()
                
                if self.failure_count >= self.failure_threshold:
                    self.state = 'OPEN'
                    logger.warning(f"Circuit breaker opened after {self.failure_count} failures")
            
            raise e

class RetryManager:
    """Manages retry logic with exponential backoff."""
    
    def __init__(self, max_retries: int = 3, base_delay: float = 1.0, max_delay: float = 60.0):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay
    
    def execute_with_retry(self, func: Callable, *args, **kwargs) -> Any:
        """Execute function with exponential backoff retry."""
        last_exception = None
        
        for attempt in range(self.max_retries + 1):
            try:
                return func(*args, **kwargs)
            
            except Exception as e:
                last_exception = e
                
                if attempt < self.max_retries:
                    delay = min(self.base_delay * (2 ** attempt), self.max_delay)
                    logger.warning(f"Attempt {attempt + 1} failed, retrying in {delay}s: {e}")
                    time.sleep(delay)
                else:
                    logger.error(f"All {self.max_retries + 1} attempts failed: {e}")
        
        raise last_exception

class FailureHandler(PipelineStage):
    """Comprehensive failure handling and recovery system."""
    
    def __init__(self, config: Optional[Dict[str, Any]] = None):
        super().__init__("FailureHandler", config)
        
        # Configuration
        self.max_retries = config.get('max_retries', 3) if config else 3
        self.circuit_breaker_threshold = config.get('circuit_breaker_threshold', 10) if config else 10
        self.recovery_timeout = config.get('recovery_timeout', 300) if config else 300  # 5 minutes
        self.failure_log_path = config.get('failure_log_path', './failures.jsonl') if config else './failures.jsonl'
        
        # Components
        self.retry_manager = RetryManager(self.max_retries)
        self.circuit_breaker = CircuitBreaker(self.circuit_breaker_threshold, self.recovery_timeout)
        
        # Failure tracking
        self.failure_records: List[FailureRecord] = []
        self.failure_stats = {
            FailureType.NETWORK_ERROR: 0,
            FailureType.MODEL_ERROR: 0,
            FailureType.MEMORY_ERROR: 0,
            FailureType.IO_ERROR: 0,
            FailureType.VALIDATION_ERROR: 0,
            FailureType.TIMEOUT_ERROR: 0,
            FailureType.UNKNOWN_ERROR: 0
        }
        
        # Recovery strategies
        self.recovery_strategies = {
            FailureType.MEMORY_ERROR: self._handle_memory_error,
            FailureType.MODEL_ERROR: self._handle_model_error,
            FailureType.NETWORK_ERROR: self._handle_network_error,
            FailureType.IO_ERROR: self._handle_io_error,
            FailureType.TIMEOUT_ERROR: self._handle_timeout_error
        }
    
    def _classify_error(self, error: Exception) -> FailureType:
        """Classify error type for appropriate handling."""
        error_str = str(error).lower()
        error_type = type(error).__name__.lower()
        
        if 'memory' in error_str or 'oom' in error_str or isinstance(error, MemoryError):
            return FailureType.MEMORY_ERROR
        elif 'network' in error_str or 'connection' in error_str or 'timeout' in error_str:
            return FailureType.NETWORK_ERROR
        elif 'model' in error_str or 'cuda' in error_str or 'tensor' in error_str:
            return FailureType.MODEL_ERROR
        elif 'file' in error_str or 'io' in error_str or isinstance(error, (IOError, FileNotFoundError)):
            return FailureType.IO_ERROR
        elif 'validation' in error_str or 'schema' in error_str or isinstance(error, ValueError):
            return FailureType.VALIDATION_ERROR
        elif 'timeout' in error_str or isinstance(error, TimeoutError):
            return FailureType.TIMEOUT_ERROR
        else:
            return FailureType.UNKNOWN_ERROR
    
    def _handle_memory_error(self, error: Exception, context: Dict[str, Any]) -> Dict[str, Any]:
        """Handle memory-related errors."""
        import gc
        gc.collect()
        
        return {
            'strategy': 'memory_cleanup',
            'action': 'garbage_collection_performed',
            'recommendation': 'reduce_batch_size',
            'fallback': 'process_individually'
        }
    
    def _handle_model_error(self, error: Exception, context: Dict[str, Any]) -> Dict[str, Any]:
        """Handle model-related errors."""
        return {
            'strategy': 'model_fallback',
            'action': 'switch_to_backup_model',
            'recommendation': 'check_model_compatibility',
            'fallback': 'use_simplified_extraction'
        }
    
    def _handle_network_error(self, error: Exception, context: Dict[str, Any]) -> Dict[str, Any]:
        """Handle network-related errors."""
        return {
            'strategy': 'network_retry',
            'action': 'exponential_backoff_retry',
            'recommendation': 'check_network_connectivity',
            'fallback': 'use_local_models_only'
        }
    
    def _handle_io_error(self, error: Exception, context: Dict[str, Any]) -> Dict[str, Any]:
        """Handle I/O related errors."""
        return {
            'strategy': 'io_recovery',
            'action': 'verify_file_permissions',
            'recommendation': 'check_disk_space',
            'fallback': 'skip_corrupted_files'
        }
    
    def _handle_timeout_error(self, error: Exception, context: Dict[str, Any]) -> Dict[str, Any]:
        """Handle timeout errors."""
        return {
            'strategy': 'timeout_recovery',
            'action': 'increase_timeout_duration',
            'recommendation': 'optimize_processing_pipeline',
            'fallback': 'process_with_reduced_quality'
        }
    
    def record_failure(self, item_id: str, error: Exception, context: Optional[Dict[str, Any]] = None):
        """Record failure for analysis and recovery."""
        failure_type = self._classify_error(error)
        
        failure_record = FailureRecord(
            item_id=item_id,
            failure_type=failure_type,
            error_message=str(error),
            timestamp=time.time(),
            retry_count=context.get('retry_count', 0) if context else 0,
            stack_trace=traceback.format_exc(),
            context=context
        )
        
        self.failure_records.append(failure_record)
        self.failure_stats[failure_type] += 1
        
        # Log to file for persistence
        self._log_failure_to_file(failure_record)
        
        logger.error(f"Recorded failure for {item_id}: {failure_type.value} - {error}")
    
    def _log_failure_to_file(self, failure_record: FailureRecord):
        """Log failure record to persistent file."""
        try:
            failure_data = asdict(failure_record)
            failure_data['failure_type'] = failure_record.failure_type.value
            
            with open(self.failure_log_path, 'a') as f:
                f.write(json.dumps(failure_data) + '\n')
        except Exception as e:
            logger.error(f"Failed to log failure record: {e}")
    
    def handle_failure(self, item_id: str, error: Exception, 
                      context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Handle failure with appropriate recovery strategy."""
        failure_type = self._classify_error(error)
        
        # Record the failure
        self.record_failure(item_id, error, context)
        
        # Apply recovery strategy
        recovery_info = {'strategy': 'none', 'action': 'skip'}
        if failure_type in self.recovery_strategies:
            try:
                recovery_info = self.recovery_strategies[failure_type](error, context or {})
            except Exception as recovery_error:
                logger.error(f"Recovery strategy failed: {recovery_error}")
        
        return {
            'item_id': item_id,
            'failure_type': failure_type.value,
            'error_message': str(error),
            'recovery': recovery_info,
            'should_retry': recovery_info.get('action') != 'skip',
            'fallback_available': 'fallback' in recovery_info
        }
    
    def execute_with_protection(self, func: Callable, item_id: str, 
                               *args, **kwargs) -> ProcessingResult:
        """Execute function with comprehensive failure protection."""
        context = {
            'item_id': item_id,
            'function': func.__name__,
            'retry_count': 0
        }
        
        try:
            with self.circuit_breaker.call():
                # Execute with retry logic
                result = self.retry_manager.execute_with_retry(
                    self._execute_with_monitoring, func, item_id, context, *args, **kwargs
                )
                return result
                
        except Exception as e:
            # Handle failure
            failure_info = self.handle_failure(item_id, e, context)
            
            # Try fallback if available
            if failure_info['fallback_available']:
                try:
                    fallback_result = self._execute_fallback(item_id, failure_info, *args, **kwargs)
                    logger.info(f"Fallback successful for {item_id}")
                    return fallback_result
                except Exception as fallback_error:
                    logger.error(f"Fallback also failed for {item_id}: {fallback_error}")
            
            # Return failed result
            return ProcessingResult(
                item_id=item_id,
                attributes=CharacterAttributes(),
                success=False,
                error_message=str(e),
                processing_time=0.0
            )
    
    def _execute_with_monitoring(self, func: Callable, item_id: str, 
                                context: Dict[str, Any], *args, **kwargs) -> ProcessingResult:
        """Execute function with monitoring and context tracking."""
        start_time = time.time()
        
        try:
            # Update retry count
            context['retry_count'] = context.get('retry_count', 0) + 1
            
            # Execute function
            result = func(*args, **kwargs)
            processing_time = time.time() - start_time
            
            # Validate result
            if not self._validate_result(result):
                raise ValueError(f"Invalid result returned for {item_id}")
            
            return ProcessingResult(
                item_id=item_id,
                attributes=result,
                success=True,
                processing_time=processing_time
            )
            
        except Exception as e:
            processing_time = time.time() - start_time
            context['processing_time'] = processing_time
            raise e
    
    def _validate_result(self, result: Any) -> bool:
        """Validate processing result."""
        if not isinstance(result, CharacterAttributes):
            return False
        
        # Check if result has meaningful content
        attrs_dict = asdict(result)
        non_none_attrs = {k: v for k, v in attrs_dict.items() if v is not None and v != ""}
        
        # Must have at least some attributes
        return len(non_none_attrs) >= 3
    
    def _execute_fallback(self, item_id: str, failure_info: Dict[str, Any], 
                         *args, **kwargs) -> ProcessingResult:
        """Execute fallback processing strategy."""
        fallback_strategy = failure_info['recovery']['fallback']
        
        if fallback_strategy == 'use_simplified_extraction':
            # Return basic attributes with low confidence
            return ProcessingResult(
                item_id=item_id,
                attributes=CharacterAttributes(
                    age="unknown",
                    gender="unknown",
                    ethnicity="unknown",
                    hair_style="unknown",
                    hair_color="unknown",
                    hair_length="unknown",
                    eye_color="unknown",
                    body_type="unknown",
                    dress="unknown",
                    confidence_score=0.1
                ),
                success=True,
                processing_time=0.001,
                metadata={'fallback': True, 'strategy': fallback_strategy}
            )
        
        elif fallback_strategy == 'skip_corrupted_files':
            # Return empty result but mark as successful skip
            return ProcessingResult(
                item_id=item_id,
                attributes=CharacterAttributes(),
                success=True,
                processing_time=0.001,
                metadata={'skipped': True, 'reason': 'corrupted_file'}
            )
        
        else:
            # Default fallback
            raise Exception(f"Unknown fallback strategy: {fallback_strategy}")
    
    def get_failure_analysis(self) -> Dict[str, Any]:
        """Analyze failure patterns and provide recommendations."""
        total_failures = len(self.failure_records)
        
        if total_failures == 0:
            return {
                'total_failures': 0,
                'failure_rate': 0.0,
                'recommendations': ['No failures detected - system running smoothly']
            }
        
        # Analyze failure patterns
        failure_by_type = {ft.value: count for ft, count in self.failure_stats.items()}
        most_common_failure = max(failure_by_type.items(), key=lambda x: x[1])
        
        # Recent failures (last hour)
        recent_threshold = time.time() - 3600
        recent_failures = [f for f in self.failure_records if f.timestamp > recent_threshold]
        
        # Generate recommendations
        recommendations = self._generate_failure_recommendations(failure_by_type, recent_failures)
        
        return {
            'total_failures': total_failures,
            'failure_by_type': failure_by_type,
            'most_common_failure': most_common_failure,
            'recent_failures_count': len(recent_failures),
            'circuit_breaker_state': self.circuit_breaker.state,
            'recommendations': recommendations,
            'failure_trends': self._analyze_failure_trends()
        }
    
    def _generate_failure_recommendations(self, failure_by_type: Dict[str, int], 
                                        recent_failures: List[FailureRecord]) -> List[str]:
        """Generate recommendations based on failure patterns."""
        recommendations = []
        
        # Memory error recommendations
        if failure_by_type.get('memory_error', 0) > 5:
            recommendations.extend([
                "High memory errors detected - reduce batch size",
                "Consider implementing memory pooling",
                "Enable more aggressive garbage collection"
            ])
        
        # Model error recommendations
        if failure_by_type.get('model_error', 0) > 3:
            recommendations.extend([
                "Model errors detected - verify model compatibility",
                "Consider model warm-up strategies",
                "Implement model health checks"
            ])
        
        # Network error recommendations
        if failure_by_type.get('network_error', 0) > 5:
            recommendations.extend([
                "Network instability detected - implement offline mode",
                "Use local model caching",
                "Increase network timeout values"
            ])
        
        # Recent failure spike
        if len(recent_failures) > 10:
            recommendations.append("Recent failure spike detected - investigate system health")
        
        # General recommendations
        recommendations.extend([
            "Monitor system resources during processing",
            "Implement progressive quality degradation",
            "Use health checks before processing batches"
        ])
        
        return recommendations
    
    def _analyze_failure_trends(self) -> Dict[str, Any]:
        """Analyze failure trends over time."""
        if not self.failure_records:
            return {'trend': 'stable', 'analysis': 'No failures to analyze'}
        
        # Group failures by hour
        hourly_failures = {}
        for failure in self.failure_records:
            hour = int(failure.timestamp // 3600)
            hourly_failures[hour] = hourly_failures.get(hour, 0) + 1
        
        # Analyze trend
        if len(hourly_failures) < 2:
            trend = 'insufficient_data'
        else:
            hours = sorted(hourly_failures.keys())
            recent_avg = sum(hourly_failures[h] for h in hours[-3:]) / min(3, len(hours))
            older_avg = sum(hourly_failures[h] for h in hours[:-3]) / max(1, len(hours) - 3)
            
            if recent_avg > older_avg * 1.5:
                trend = 'increasing'
            elif recent_avg < older_avg * 0.5:
                trend = 'decreasing'
            else:
                trend = 'stable'
        
        return {
            'trend': trend,
            'hourly_failures': hourly_failures,
            'analysis': f"Failure trend is {trend} based on recent patterns"
        }
    
    def create_recovery_checkpoint(self, processed_items: List[str], 
                                  failed_items: List[str], output_path: str) -> str:
        """Create recovery checkpoint for resuming processing."""
        checkpoint_data = {
            'timestamp': time.time(),
            'processed_items': processed_items,
            'failed_items': failed_items,
            'failure_stats': {ft.value: count for ft, count in self.failure_stats.items()},
            'circuit_breaker_state': self.circuit_breaker.state,
            'total_processed': len(processed_items),
            'total_failed': len(failed_items)
        }
        
        checkpoint_path = f"{output_path}.recovery_checkpoint.json"
        with open(checkpoint_path, 'w') as f:
            json.dump(checkpoint_data, f, indent=2)
        
        logger.info(f"Recovery checkpoint created: {checkpoint_path}")
        return checkpoint_path
    
    def load_recovery_checkpoint(self, checkpoint_path: str) -> Optional[Dict[str, Any]]:
        """Load recovery checkpoint to resume processing."""
        try:
            with open(checkpoint_path, 'r') as f:
                checkpoint_data = json.load(f)
            
            logger.info(f"Loaded recovery checkpoint: {len(checkpoint_data['processed_items'])} processed, {len(checkpoint_data['failed_items'])} failed")
            return checkpoint_data
            
        except Exception as e:
            logger.error(f"Failed to load recovery checkpoint: {e}")
            return None
    
    def process(self, input_data: Any) -> Any:
        """Process failure handling operations."""
        if isinstance(input_data, dict):
            operation = input_data.get('operation')
            
            if operation == 'execute_protected':
                return self.execute_with_protection(
                    input_data['function'],
                    input_data['item_id'],
                    *input_data.get('args', []),
                    **input_data.get('kwargs', {})
                )
            elif operation == 'analyze_failures':
                return self.get_failure_analysis()
            elif operation == 'create_checkpoint':
                return self.create_recovery_checkpoint(
                    input_data['processed_items'],
                    input_data['failed_items'],
                    input_data['output_path']
                )
            elif operation == 'load_checkpoint':
                return self.load_recovery_checkpoint(input_data['checkpoint_path'])
        
        raise ValueError("FailureHandler expects operation dict as input")
    
    def validate_input(self, input_data: Any) -> bool:
        """Validate input data."""
        return isinstance(input_data, dict) and 'operation' in input_data