Refactor import statements in worker.py for improved organization - Moved the import of the os module to the appropriate section, enhancing clarity and consistency in the import structure.
341 lines
12 KiB
Python
341 lines
12 KiB
Python
"""
|
|
Laborious Worker Module
|
|
|
|
This module provides the main worker implementation for the Sientia DataOps Laborious system.
|
|
It orchestrates Temporal workers, manages task queues, and handles the lifecycle of
|
|
prediction and retraining workflows.
|
|
|
|
The worker supports multiple task queues:
|
|
- predictions_batch-queue: Handles batch prediction workflows (heavy workload)
|
|
- minimal_retrain-queue: Handles model retraining workflows
|
|
- drift-queue: Handles drift detection workflows
|
|
- simple_metrics-queue: Handles simple metrics calculation workflows
|
|
|
|
Key Features:
|
|
- Resource-based scaling with WorkerTuner (CPU and memory aware)
|
|
- Automatic polling scaling with PollerBehaviorAutoscaling
|
|
- Prometheus metrics integration
|
|
- Comprehensive error handling and logging
|
|
- Graceful shutdown with cleanup
|
|
- Multiple worker instances for different workflow types
|
|
|
|
Environment Variables:
|
|
- TEMPORAL_HOST: Temporal server address (default: localhost:7233)
|
|
- TEMPORAL_NAMESPACE: Temporal namespace (default: laborious)
|
|
- POD_ID: Kubernetes pod identifier for metrics
|
|
- HTTP_METRICS_PORT: Prometheus metrics server port (default: 9090)
|
|
- HTTP_SDK_METRICS_PORT: Temporal SDK metrics port (default: 9091)
|
|
- PROJECT_NAME: Project name for notifications (default: laborious)
|
|
|
|
Tuner Configuration (Resource-based scaling):
|
|
- TUNER_TARGET_MEMORY_USAGE: Target memory usage (0.0-1.0, default: 0.75)
|
|
- TUNER_TARGET_CPU_USAGE: Target CPU usage (0.0-1.0, default: 0.80)
|
|
- TUNER_WORKFLOW_MIN_SLOTS: Minimum workflow slots (default: 5)
|
|
- TUNER_WORKFLOW_MAX_SLOTS: Maximum workflow slots (default: 50)
|
|
- TUNER_ACTIVITY_MIN_SLOTS: Minimum activity slots (default: 5)
|
|
- TUNER_ACTIVITY_MAX_SLOTS: Maximum activity slots (default: 50)
|
|
- TUNER_WORKFLOW_RAMP_THROTTLE_MS: Workflow ramp throttle in ms (default: 100)
|
|
- TUNER_ACTIVITY_RAMP_THROTTLE_MS: Activity ramp throttle in ms (default: 50)
|
|
|
|
Poller Configuration:
|
|
- POLLER_MINIMUM: Minimum number of pollers (default: 1)
|
|
- POLLER_MAXIMUM: Maximum number of pollers (default: 10)
|
|
- POLLER_INITIAL: Initial number of pollers (default: 2)
|
|
"""
|
|
|
|
|
|
from temporalio import client, workflow
|
|
from temporalio.runtime import PrometheusConfig, Runtime, TelemetryConfig
|
|
from temporalio.worker import (
|
|
PollerBehaviorAutoscaling,
|
|
ResourceBasedSlotConfig,
|
|
Worker,
|
|
WorkerTuner,
|
|
)
|
|
|
|
with workflow.unsafe.imports_passed_through():
|
|
import asyncio
|
|
import sys
|
|
from datetime import timedelta
|
|
|
|
from prometheus_client import start_http_server
|
|
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
|
|
from sientia_do.observability.logger import get_logger
|
|
|
|
from laborious import metrics
|
|
from laborious.activities.activities import Activities
|
|
from laborious.utils.connectors_config import (
|
|
build_minio_config,
|
|
build_mlflow_config,
|
|
build_mongodb_config,
|
|
build_opc_config,
|
|
build_postgres_config,
|
|
)
|
|
from laborious.workflows.drift import Drift
|
|
from laborious.workflows.minimal_retrain import MinimalRetrain
|
|
from laborious.workflows.predictions_batch import PredictionsBatch
|
|
from laborious.workflows.simple_metrics import SimpleMetrics
|
|
from laborious.workflows.sub_workflows.format_and_export_prediction import (
|
|
FormatAndExportPrediction,
|
|
)
|
|
from laborious.workflows.sub_workflows.prediction_process import PredictionProcess
|
|
import os
|
|
|
|
POD_ID = os.getenv('POD_ID')
|
|
SDK_METRICS_PORT = int(os.getenv('HTTP_SDK_METRICS_PORT', '9091'))
|
|
|
|
|
|
def create_resource_tuner() -> WorkerTuner:
|
|
"""Create a resource-based tuner from environment variables."""
|
|
target_memory = float(os.getenv('TUNER_TARGET_MEMORY_USAGE', '0.75'))
|
|
target_cpu = float(os.getenv('TUNER_TARGET_CPU_USAGE', '0.50'))
|
|
workflow_min = int(os.getenv('TUNER_WORKFLOW_MIN_SLOTS', '5'))
|
|
workflow_max = int(os.getenv('TUNER_WORKFLOW_MAX_SLOTS', '50'))
|
|
activity_min = int(os.getenv('TUNER_ACTIVITY_MIN_SLOTS', '5'))
|
|
activity_max = int(os.getenv('TUNER_ACTIVITY_MAX_SLOTS', '50'))
|
|
local_activity_min = int(os.getenv('TUNER_LOCAL_ACTIVITY_MIN_SLOTS', '1'))
|
|
local_activity_max = int(os.getenv('TUNER_LOCAL_ACTIVITY_MAX_SLOTS', '30'))
|
|
workflow_ramp = int(os.getenv('TUNER_WORKFLOW_RAMP_THROTTLE_MS', '100'))
|
|
activity_ramp = int(os.getenv('TUNER_ACTIVITY_RAMP_THROTTLE_MS', '50'))
|
|
local_activity_ramp = int(os.getenv('TUNER_LOCAL_ACTIVITY_RAMP_THROTTLE_MS', '50'))
|
|
|
|
return WorkerTuner.create_resource_based(
|
|
target_memory_usage=target_memory,
|
|
target_cpu_usage=target_cpu,
|
|
workflow_config=ResourceBasedSlotConfig(
|
|
minimum_slots=workflow_min,
|
|
maximum_slots=workflow_max,
|
|
ramp_throttle=timedelta(milliseconds=workflow_ramp),
|
|
),
|
|
activity_config=ResourceBasedSlotConfig(
|
|
minimum_slots=activity_min,
|
|
maximum_slots=activity_max,
|
|
ramp_throttle=timedelta(milliseconds=activity_ramp),
|
|
),
|
|
local_activity_config=ResourceBasedSlotConfig(
|
|
minimum_slots=local_activity_min,
|
|
maximum_slots=local_activity_max,
|
|
ramp_throttle=timedelta(milliseconds=local_activity_ramp),
|
|
),
|
|
)
|
|
|
|
|
|
def create_poller_behavior() -> PollerBehaviorAutoscaling:
|
|
"""Create poller behavior from environment variables."""
|
|
minimum = int(os.getenv('POLLER_MINIMUM', '1'))
|
|
maximum = int(os.getenv('POLLER_MAXIMUM', '10'))
|
|
initial = int(os.getenv('POLLER_INITIAL', '2'))
|
|
return PollerBehaviorAutoscaling(minimum=minimum, maximum=maximum, initial=initial)
|
|
|
|
|
|
async def main():
|
|
"""
|
|
Main entry point for the Laborious worker application.
|
|
|
|
This function initializes and starts all components of the worker:
|
|
1. Sets up logging and metadata
|
|
2. Starts Prometheus metrics server
|
|
3. Initializes notification handler
|
|
4. Creates and configures activities
|
|
5. Initializes OPC connections
|
|
6. Starts Temporal client and workers
|
|
7. Manages worker lifecycle and graceful shutdown
|
|
|
|
The function runs indefinitely until interrupted or an error occurs.
|
|
On error, it performs cleanup and exits with a non-zero status code.
|
|
|
|
Raises:
|
|
Exception: Any unhandled exception during worker execution
|
|
SystemExit: On graceful shutdown or error conditions
|
|
"""
|
|
host = os.getenv('TEMPORAL_HOST', 'localhost:7233')
|
|
logger = get_logger(__name__)
|
|
|
|
metadata = {
|
|
'pod_id': POD_ID,
|
|
'model_name': '-',
|
|
'model_id': '-',
|
|
'workflow_name': '-',
|
|
'schedule_name': '-',
|
|
}
|
|
|
|
logger.custom_info(f'Starting Worker with POD_ID: {POD_ID}', metadata)
|
|
|
|
logger.custom_info('Starting prometheus client...', metadata)
|
|
start_prometheus_server()
|
|
|
|
logger.custom_info('Starting Notification Handler...', metadata)
|
|
|
|
mongo_config = build_mongodb_config()
|
|
notification_handler = NotificationHandler(
|
|
connection_string=mongo_config['connection_string'],
|
|
database=mongo_config['database_name'],
|
|
logger=logger,
|
|
project_name=os.getenv('PROJECT_NAME', 'laborious'),
|
|
)
|
|
|
|
logger.custom_info('Starting Activities...', metadata)
|
|
|
|
activities = Activities(
|
|
postgres_config=build_postgres_config(),
|
|
mlflow_config=build_mlflow_config(),
|
|
minio_config=build_minio_config(),
|
|
opc_config=build_opc_config(),
|
|
logger=logger,
|
|
notification_handler=notification_handler,
|
|
)
|
|
|
|
logger.custom_info('Initializing OPC...', metadata)
|
|
await activities.init_opc()
|
|
|
|
logger.custom_info(f'Starting SDK Metrics Server on port {SDK_METRICS_PORT}...', metadata)
|
|
|
|
new_runtime = Runtime(
|
|
telemetry=TelemetryConfig(
|
|
metrics=PrometheusConfig(bind_address=f'0.0.0.0:{SDK_METRICS_PORT}')
|
|
)
|
|
)
|
|
|
|
logger.custom_info(f'Starting Temporal Client at {host}...', metadata)
|
|
|
|
temporal_client = await client.Client.connect(
|
|
target_host=host,
|
|
namespace=os.getenv('TEMPORAL_NAMESPACE', 'laborious'),
|
|
runtime=new_runtime,
|
|
)
|
|
|
|
logger.custom_info('Starting Workers...', metadata)
|
|
|
|
tuner = create_resource_tuner()
|
|
poller = create_poller_behavior()
|
|
|
|
workers = [
|
|
Worker(
|
|
temporal_client,
|
|
task_queue='minimal_retrain-queue',
|
|
workflows=[MinimalRetrain],
|
|
activities=[
|
|
activities.load_custom_query,
|
|
activities.query_to_minio,
|
|
activities.retrain_model,
|
|
activities.update_production_model,
|
|
activities.format_retrain_report,
|
|
activities.export_data_to_postgres,
|
|
],
|
|
tuner=tuner,
|
|
max_cached_workflows=2,
|
|
workflow_task_poller_behavior=poller,
|
|
activity_task_poller_behavior=poller,
|
|
),
|
|
Worker(
|
|
temporal_client,
|
|
task_queue='drift-queue',
|
|
workflows=[Drift],
|
|
activities=[
|
|
activities.load_custom_query,
|
|
activities.get_reference_data,
|
|
activities.calculate_drift,
|
|
activities.export_data_to_postgres,
|
|
],
|
|
tuner=tuner,
|
|
max_cached_workflows=2,
|
|
workflow_task_poller_behavior=poller,
|
|
activity_task_poller_behavior=poller,
|
|
),
|
|
Worker(
|
|
temporal_client,
|
|
task_queue='simple_metrics-queue',
|
|
workflows=[SimpleMetrics],
|
|
activities=[
|
|
activities.load_custom_query,
|
|
activities.calculate_simple_metrics,
|
|
activities.export_data_to_postgres,
|
|
],
|
|
tuner=tuner,
|
|
max_cached_workflows=2,
|
|
workflow_task_poller_behavior=poller,
|
|
activity_task_poller_behavior=poller,
|
|
),
|
|
Worker(
|
|
temporal_client,
|
|
task_queue='predictions_batch-queue',
|
|
workflows=[PredictionsBatch, PredictionProcess, FormatAndExportPrediction],
|
|
activities=[
|
|
# MLFlow
|
|
activities.request_predict,
|
|
activities.request_transform,
|
|
# Gates
|
|
activities.input_gate,
|
|
activities.mlflow_response_gate,
|
|
activities.mlflow_content_gate,
|
|
activities.format_transformed_data,
|
|
activities.format_prediction,
|
|
activities.format_default_prediction,
|
|
activities.get_last_timestamp,
|
|
# OPC
|
|
activities.write_opc_data,
|
|
# Postgres
|
|
activities.load_custom_query,
|
|
activities.repeat_last_prediction,
|
|
activities.export_data_to_postgres,
|
|
activities.write_metrics,
|
|
],
|
|
tuner=tuner,
|
|
max_cached_workflows=200,
|
|
workflow_task_poller_behavior=poller,
|
|
activity_task_poller_behavior=poller,
|
|
),
|
|
]
|
|
|
|
handlers = []
|
|
for w in workers:
|
|
handlers.append(w.run())
|
|
|
|
logger.custom_info('Workers started successfully', metadata)
|
|
|
|
try:
|
|
# This will run the workers and wait for them to complete.
|
|
# If an exception occurs in any of the worker handlers, it will be propagated here.
|
|
await asyncio.gather(*handlers)
|
|
except BaseException as e: # NOSONAR
|
|
logger.custom_error(f'An unhandled exception occurred: {e}', metadata)
|
|
finally:
|
|
if notification_handler:
|
|
notification_handler.shutdown()
|
|
if activities:
|
|
await activities.shutdown()
|
|
# Exit with a non-zero status code to indicate failure to Kubernetes
|
|
metrics.APP_UP.labels(pod_id=POD_ID).set(0) # Mark app as DOWN
|
|
sys.exit(1)
|
|
|
|
|
|
def start_prometheus_server():
|
|
"""
|
|
Starts the Prometheus metrics server for monitoring and observability.
|
|
|
|
This function initializes the Prometheus HTTP server on the configured port
|
|
and sets the application health metric to indicate the service is running.
|
|
|
|
The server exposes metrics that can be scraped by Prometheus for monitoring
|
|
the health and performance of the Laborious worker.
|
|
|
|
Environment Variables:
|
|
HTTP_METRICS_PORT: Port for the metrics server (default: 9090)
|
|
POD_ID: Pod identifier for metrics labeling
|
|
|
|
Raises:
|
|
SystemExit: If the metrics server fails to start
|
|
"""
|
|
try:
|
|
port = int(os.getenv('HTTP_METRICS_PORT', 9090))
|
|
start_http_server(port)
|
|
print(f'Prometheus server started on port {port}.')
|
|
metrics.APP_UP.labels(pod_id=POD_ID).set(1) # Mark app as UP
|
|
except Exception as e:
|
|
print(f'Failed to start Prometheus server: {e}')
|
|
os._exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
asyncio.run(main())
|