feat: improve error handling and resource cleanup in training workflow
- Updated the `Training` class to raise `ModelTrainingError` on training failures for better error management. - Enhanced the `run` method in `TrainModel` to return training results and ensure proper resource cleanup, including validation files. - Refactored exception handling to prevent silent failures during resource cleanup and experiment run updates. - Adjusted type hints for improved clarity and consistency in method signatures.
This commit is contained in:
@@ -3,7 +3,7 @@ Training activities for ML model training operations.
|
|||||||
|
|
||||||
This module provides activities for training machine learning models.
|
This module provides activities for training machine learning models.
|
||||||
The activity extends BaseActivity and receives pre-downloaded files
|
The activity extends BaseActivity and receives pre-downloaded files
|
||||||
to return success/failure status without raising exceptions.
|
and raises `ModelTrainingError` when training fails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from temporalio import activity, workflow
|
from temporalio import activity, workflow
|
||||||
@@ -35,8 +35,8 @@ class Training(SientiaMonitoring):
|
|||||||
|
|
||||||
This activity extends SientiaMonitoring and handles machine learning model
|
This activity extends SientiaMonitoring and handles machine learning model
|
||||||
training with comprehensive error handling. It receives pre-downloaded
|
training with comprehensive error handling. It receives pre-downloaded
|
||||||
files from the workflow and returns success/failure status without
|
files from the workflow and raises `ModelTrainingError` on failure so the
|
||||||
raising exceptions.
|
workflow can map the correct experiment status.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -300,7 +300,7 @@ class Training(SientiaMonitoring):
|
|||||||
metadata = input_data.get('metadata', {})
|
metadata = input_data.get('metadata', {})
|
||||||
bucket_name = input_data.get('bucket_name', '')
|
bucket_name = input_data.get('bucket_name', '')
|
||||||
file_name = input_data.get('file_name', '')
|
file_name = input_data.get('file_name', '')
|
||||||
metrics_status = 'success'
|
val_file_name = input_data.get('val_file_name')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.minio_repository.delete_file(
|
await self.minio_repository.delete_file(
|
||||||
@@ -308,6 +308,12 @@ class Training(SientiaMonitoring):
|
|||||||
bucket=bucket_name,
|
bucket=bucket_name,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
if val_file_name:
|
||||||
|
await self.minio_repository.delete_file(
|
||||||
|
object_name=val_file_name,
|
||||||
|
bucket=bucket_name,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
error_msg = (
|
error_msg = (
|
||||||
'Error cleaning up resources - '
|
'Error cleaning up resources - '
|
||||||
@@ -316,7 +322,7 @@ class Training(SientiaMonitoring):
|
|||||||
|
|
||||||
trace = traceback.format_exc()
|
trace = traceback.format_exc()
|
||||||
|
|
||||||
self.send_notification(
|
await self.send_notification_async(
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
notification_id='CLEANUP_RESOURCES_ERROR',
|
notification_id='CLEANUP_RESOURCES_ERROR',
|
||||||
message=error_msg,
|
message=error_msg,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class TrainModel:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@workflow.run
|
@workflow.run
|
||||||
async def run(self, input_data: dict[str, Any]) -> None:
|
async def run(self, input_data: dict[str, Any]) -> dict[str, str | None]:
|
||||||
"""
|
"""
|
||||||
Execute the complete model training workflow.
|
Execute the complete model training workflow.
|
||||||
|
|
||||||
@@ -112,18 +112,29 @@ class TrainModel:
|
|||||||
input_data, experiment_run_id, metadata
|
input_data, experiment_run_id, metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
|
training_succeeded = False
|
||||||
|
train_result: dict[str, str | None]
|
||||||
|
|
||||||
|
try:
|
||||||
train_result = await self._train_model(
|
train_result = await self._train_model(
|
||||||
train_params=train_params,
|
train_params=train_params,
|
||||||
experiment_run_id=experiment_run_id,
|
experiment_run_id=experiment_run_id,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
training_succeeded = True
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
await self._cleanup_resources(
|
await self._cleanup_resources(
|
||||||
experiment_run_id=experiment_run_id,
|
experiment_run_id=experiment_run_id,
|
||||||
bucket_name=train_params.bucket_name,
|
bucket_name=train_params.bucket_name,
|
||||||
file_name=train_params.file_name,
|
file_name=train_params.file_name,
|
||||||
|
val_file_name=train_params.val_file_name,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
if training_succeeded:
|
||||||
|
raise
|
||||||
|
return train_result
|
||||||
|
|
||||||
def _validate_experiment_run_id(self, input_data: dict[str, Any]) -> int:
|
def _validate_experiment_run_id(self, input_data: dict[str, Any]) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -147,13 +158,16 @@ class TrainModel:
|
|||||||
if experiment_run_id is None:
|
if experiment_run_id is None:
|
||||||
raise ValueError('experiment_run_id is required but was not provided')
|
raise ValueError('experiment_run_id is required but was not provided')
|
||||||
|
|
||||||
if not isinstance(experiment_run_id, int):
|
if isinstance(experiment_run_id, int):
|
||||||
raise ValueError(
|
|
||||||
f'experiment_run_id must be an integer, got {type(experiment_run_id).__name__}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return experiment_run_id
|
return experiment_run_id
|
||||||
|
|
||||||
|
if isinstance(experiment_run_id, str) and experiment_run_id.strip().isdigit():
|
||||||
|
return int(experiment_run_id.strip())
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f'experiment_run_id must be an integer or numeric string, got {type(experiment_run_id).__name__}'
|
||||||
|
)
|
||||||
|
|
||||||
async def _validate_training_parameters(
|
async def _validate_training_parameters(
|
||||||
self,
|
self,
|
||||||
input_data: dict[str, Any],
|
input_data: dict[str, Any],
|
||||||
@@ -208,6 +222,7 @@ class TrainModel:
|
|||||||
|
|
||||||
return train_params
|
return train_params
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
try:
|
||||||
await self._update_experiment_run(
|
await self._update_experiment_run(
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
experiment_run_id=experiment_run_id,
|
experiment_run_id=experiment_run_id,
|
||||||
@@ -215,7 +230,8 @@ class TrainModel:
|
|||||||
status=ExperimentStatus.ORCHESTRATOR_VALIDATION_ERROR,
|
status=ExperimentStatus.ORCHESTRATOR_VALIDATION_ERROR,
|
||||||
error_message=self._extract_error_message(e),
|
error_message=self._extract_error_message(e),
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _train_model(
|
async def _train_model(
|
||||||
@@ -273,6 +289,7 @@ class TrainModel:
|
|||||||
if isinstance(e, ModelTrainingError) and (e.model_trained and not e.model_saved):
|
if isinstance(e, ModelTrainingError) and (e.model_trained and not e.model_saved):
|
||||||
status = ExperimentStatus.TRACKING_SEND_ERROR
|
status = ExperimentStatus.TRACKING_SEND_ERROR
|
||||||
|
|
||||||
|
try:
|
||||||
await self._update_experiment_run(
|
await self._update_experiment_run(
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
experiment_run_id=experiment_run_id,
|
experiment_run_id=experiment_run_id,
|
||||||
@@ -280,7 +297,8 @@ class TrainModel:
|
|||||||
status=status,
|
status=status,
|
||||||
error_message=self._extract_error_message(e),
|
error_message=self._extract_error_message(e),
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _cleanup_resources(
|
async def _cleanup_resources(
|
||||||
@@ -288,6 +306,7 @@ class TrainModel:
|
|||||||
experiment_run_id: int,
|
experiment_run_id: int,
|
||||||
bucket_name: str,
|
bucket_name: str,
|
||||||
file_name: str,
|
file_name: str,
|
||||||
|
val_file_name: str | None,
|
||||||
metadata: dict[str, Any],
|
metadata: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -311,6 +330,7 @@ class TrainModel:
|
|||||||
**metadata,
|
**metadata,
|
||||||
'bucket_name': bucket_name,
|
'bucket_name': bucket_name,
|
||||||
'file_name': file_name,
|
'file_name': file_name,
|
||||||
|
'val_file_name': val_file_name,
|
||||||
},
|
},
|
||||||
retry_policy=network_retry_policy,
|
retry_policy=network_retry_policy,
|
||||||
start_to_close_timeout=timedelta(seconds=TIMEOUT_DELETE_FILE),
|
start_to_close_timeout=timedelta(seconds=TIMEOUT_DELETE_FILE),
|
||||||
@@ -323,6 +343,7 @@ class TrainModel:
|
|||||||
status=ExperimentStatus.FILE_DELETED,
|
status=ExperimentStatus.FILE_DELETED,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
try:
|
||||||
await self._update_experiment_run(
|
await self._update_experiment_run(
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
experiment_run_id=experiment_run_id,
|
experiment_run_id=experiment_run_id,
|
||||||
@@ -330,7 +351,8 @@ class TrainModel:
|
|||||||
status=ExperimentStatus.FILE_DELETE_ERROR,
|
status=ExperimentStatus.FILE_DELETE_ERROR,
|
||||||
error_message=self._extract_error_message(e),
|
error_message=self._extract_error_message(e),
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _update_experiment_run(
|
async def _update_experiment_run(
|
||||||
@@ -388,7 +410,9 @@ class TrainModel:
|
|||||||
if text and text not in message_parts:
|
if text and text not in message_parts:
|
||||||
message_parts.append(text)
|
message_parts.append(text)
|
||||||
|
|
||||||
current = getattr(current, 'cause', None)
|
cause = getattr(current, '__cause__', None)
|
||||||
|
context = getattr(current, '__context__', None)
|
||||||
|
current = cause if isinstance(cause, Exception) else context
|
||||||
|
|
||||||
if not message_parts:
|
if not message_parts:
|
||||||
return repr(exc)
|
return repr(exc)
|
||||||
|
|||||||
Reference in New Issue
Block a user