Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 45 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
### AL ###
#Template for AL projects for Dynamics 365 Business Central
#launch.json folder
<<<<<<< HEAD
__pycache__/
*.py[cod]
*.log
.env
.venv/
venv/
dist/
build/
*.egg-info/
.pytest_cache/
.coverage
htmlcov/
=======
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.log

# Virtual environments
venv/
env/
.env/
.venv/

# Distribution / packaging
dist/
build/
*.egg-info/

# IDEs and editors
.idea/
.vscode/
#Cache folder
.alcache/
#Symbols folder
.alpackages/
#Snapshots folder
.snapshots/
#Testing Output folder
.output/
#Extension App-file
*.app
#Rapid Application Development File
rad.json
#Translation Base-file
*.g.xlf
#License-file
*.flf
#Test results file
TestResults.xml
*.swp
*.swo

# Testing
.pytest_cache/
htmlcov/
.coverage

# Miscellaneous
.DS_Store
>>>>>>> pr-2-Merango-ALP-Looping
3 changes: 3 additions & 0 deletions src/alp/retry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .strategy import TransientErrorRetryHandler, RetryStrategy

__all__ = ['TransientErrorRetryHandler', 'RetryStrategy']
109 changes: 109 additions & 0 deletions src/alp/retry/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import time
import logging
from typing import Callable, TypeVar, Any, Optional
from functools import wraps
from enum import Enum, auto

class RetryStrategy(Enum):
"""Enumeration of retry strategies."""
CONSTANT = auto()
LINEAR = auto()
EXPONENTIAL = auto()

class TransientErrorRetryHandler:
"""
A configurable retry mechanism for handling transient errors in machine learning iterations.

Key Features:
- Multiple retry strategies (constant, linear, exponential backoff)
- Configurable max retries and delay
- Flexible error handling
- Logging support
"""

def __init__(
self,
max_retries: int = 3,
initial_delay: float = 1.0,
strategy: RetryStrategy = RetryStrategy.EXPONENTIAL,
logger: Optional[logging.Logger] = None
):
"""
Initialize the retry handler.

Args:
max_retries (int): Maximum number of retry attempts. Defaults to 3.
initial_delay (float): Initial delay between retries in seconds. Defaults to 1.0.
strategy (RetryStrategy): Retry delay strategy. Defaults to exponential.
logger (Optional[logging.Logger]): Custom logger for retry events.
"""
self.max_retries = max_retries
self.initial_delay = initial_delay
self.strategy = strategy
self.logger = logger or logging.getLogger(__name__)

def _calculate_delay(self, attempt: int) -> float:
"""
Calculate retry delay based on the chosen strategy.

Args:
attempt (int): Current retry attempt number.

Returns:
float: Calculated delay in seconds.
"""
if self.strategy == RetryStrategy.CONSTANT:
return self.initial_delay
elif self.strategy == RetryStrategy.LINEAR:
return self.initial_delay * attempt
elif self.strategy == RetryStrategy.EXPONENTIAL:
return self.initial_delay * (2 ** (attempt - 1))
return self.initial_delay

def retry(self,
exceptions: tuple[type[Exception], ...] = (Exception,),
on_retry: Optional[Callable[[int, Exception], None]] = None
) -> Callable:
"""
Decorator for retrying a function with configurable retry logic.

Args:
exceptions (tuple): Tuple of exception types to catch and retry.
on_retry (Optional[Callable]): Optional callback for retry events.

Returns:
Callable: Decorated function with retry mechanism.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, self.max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
# Log retry attempt
self.logger.warning(
f"Retry attempt {attempt}/{self.max_retries} for {func.__name__}: {str(e)}"
)

# Call custom retry callback if provided
if on_retry:
on_retry(attempt, e)

# Don't delay on the last attempt
if attempt < self.max_retries:
delay = self._calculate_delay(attempt)
time.sleep(delay)

# Raise the last exception if all retries fail
self.logger.error(
f"All retry attempts failed for {func.__name__}"
)
raise last_exception
return wrapper
return decorator

# Type variable for return type preservation
T = TypeVar('T')
142 changes: 142 additions & 0 deletions src/error_reporting/error_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import logging
import sys
import os
import traceback
import json
from typing import Dict, Any, Optional, Callable
from enum import Enum, auto
from datetime import datetime

class ErrorSeverity(Enum):
"""Defines the severity levels for errors."""
LOW = auto()
MEDIUM = auto()
HIGH = auto()
CRITICAL = auto()

class ErrorReportingManager:
"""
Comprehensive error reporting and notification system for ALP loop.

Handles error logging, notification, and potential recovery strategies.
"""

def __init__(
self,
log_file: str = 'logs/alp_error.log',
notification_callback: Optional[Callable[[str, Dict[str, Any]], None]] = None
):
"""
Initialize the Error Reporting Manager.

Args:
log_file (str): Path to the log file for error logging.
notification_callback (Optional[Callable]): Optional callback for custom error notifications.
"""
# Determine base path for relative log files
base_path = os.getcwd()

# Convert to absolute path if it's relative
if not os.path.isabs(log_file):
log_file = os.path.join(base_path, log_file)

# Ensure log directory exists
log_dir = os.path.dirname(log_file)
os.makedirs(log_dir, exist_ok=True)

# Ensure the log file exists (touch the file)
open(log_file, 'a').close()

# Configure logging
logging.basicConfig(
filename=log_file,
level=logging.ERROR,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
self.log_file = log_file
self.notification_callback = notification_callback

def report_error(
self,
error: Exception,
context: Optional[Dict[str, Any]] = None,
severity: ErrorSeverity = ErrorSeverity.MEDIUM
) -> Dict[str, Any]:
"""
Report and log a comprehensive error with context.

Args:
error (Exception): The exception that occurred.
context (Optional[Dict]): Additional context about the error.
severity (ErrorSeverity): Severity level of the error.

Returns:
Dict[str, Any]: Detailed error report.
"""
# Prepare error details
error_report = {
'timestamp': datetime.now().isoformat(),
'error_type': type(error).__name__,
'error_message': str(error),
'severity': severity.name,
'traceback': traceback.format_exc(),
'context': context or {}
}

# Log the error
error_log_message = json.dumps(error_report, indent=2)
self.logger.error(error_log_message)

# Trigger notification if callback is set
if self.notification_callback:
try:
self.notification_callback(error_log_message, error_report)
except Exception as notification_error:
self.logger.error(f"Error in notification callback: {notification_error}")

return error_report

def handle_critical_error(
self,
error: Exception,
context: Optional[Dict[str, Any]] = None
):
"""
Handle critical errors that potentially require system intervention.

Args:
error (Exception): The critical exception.
context (Optional[Dict]): Additional context about the error.
"""
error_report = self.report_error(error, context, severity=ErrorSeverity.CRITICAL)

# Optional: Implement more aggressive error handling
# For example, you might want to:
# 1. Send an urgent notification
# 2. Attempt system recovery
# 3. Potentially halt the ALP loop

# For demonstration, we'll just print a critical error message
print(f"CRITICAL ERROR DETECTED: {error_report}", file=sys.stderr)

def recovery_attempt(self, error: Exception, recovery_strategy: Callable[[], bool]) -> bool:
"""
Attempt to recover from an error using a provided recovery strategy.

Args:
error (Exception): The error to recover from.
recovery_strategy (Callable): A function that attempts to recover from the error.

Returns:
bool: Whether recovery was successful.
"""
try:
return recovery_strategy()
except Exception as recovery_error:
self.report_error(
recovery_error,
context={'original_error': str(error)},
severity=ErrorSeverity.HIGH
)
return False
Loading