refactor: half way to process per session
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 7s
Build Worker Base and Application Images / build-base (push) Has been skipped
Build Worker Base and Application Images / build-docker (push) Successful in 2m52s
Build Worker Base and Application Images / deploy-stack (push) Successful in 9s
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 7s
Build Worker Base and Application Images / build-base (push) Has been skipped
Build Worker Base and Application Images / build-docker (push) Successful in 2m52s
Build Worker Base and Application Images / deploy-stack (push) Successful in 9s
This commit is contained in:
parent
2e5316ca01
commit
34d1982e9e
12 changed files with 2771 additions and 92 deletions
319
core/communication/session_integration.py
Normal file
319
core/communication/session_integration.py
Normal file
|
@ -0,0 +1,319 @@
|
|||
"""
|
||||
Integration layer between WebSocket handler and Session Process Manager.
|
||||
Bridges the existing WebSocket protocol with the new session-based architecture.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
import numpy as np
|
||||
|
||||
from ..processes.session_manager import SessionProcessManager
|
||||
from ..processes.communication import DetectionResultResponse, ErrorResponse
|
||||
from .state import worker_state
|
||||
from .messages import serialize_outgoing_message
|
||||
# Streaming is now handled directly by session workers - no shared stream manager needed
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionWebSocketIntegration:
|
||||
"""
|
||||
Integration layer that connects WebSocket protocol with Session Process Manager.
|
||||
Maintains compatibility with existing WebSocket message handling.
|
||||
"""
|
||||
|
||||
def __init__(self, websocket_handler=None):
|
||||
"""
|
||||
Initialize session WebSocket integration.
|
||||
|
||||
Args:
|
||||
websocket_handler: Reference to WebSocket handler for sending messages
|
||||
"""
|
||||
self.websocket_handler = websocket_handler
|
||||
self.session_manager = SessionProcessManager()
|
||||
|
||||
# Track active subscriptions for compatibility
|
||||
self.active_subscriptions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# Set up callbacks
|
||||
self.session_manager.set_detection_result_callback(self._on_detection_result)
|
||||
self.session_manager.set_error_callback(self._on_session_error)
|
||||
|
||||
async def start(self):
|
||||
"""Start the session integration."""
|
||||
await self.session_manager.start()
|
||||
logger.info("Session WebSocket integration started")
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the session integration."""
|
||||
await self.session_manager.stop()
|
||||
logger.info("Session WebSocket integration stopped")
|
||||
|
||||
async def handle_set_subscription_list(self, message) -> bool:
|
||||
"""
|
||||
Handle setSubscriptionList message by managing session processes.
|
||||
|
||||
Args:
|
||||
message: SetSubscriptionListMessage
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Processing subscription list with {len(message.subscriptions)} subscriptions")
|
||||
|
||||
new_subscription_ids = set()
|
||||
for subscription in message.subscriptions:
|
||||
subscription_id = subscription.subscriptionIdentifier
|
||||
new_subscription_ids.add(subscription_id)
|
||||
|
||||
# Check if this is a new subscription
|
||||
if subscription_id not in self.active_subscriptions:
|
||||
logger.info(f"Creating new session for subscription: {subscription_id}")
|
||||
|
||||
# Convert subscription to configuration dict
|
||||
subscription_config = {
|
||||
'subscriptionIdentifier': subscription.subscriptionIdentifier,
|
||||
'rtspUrl': getattr(subscription, 'rtspUrl', None),
|
||||
'snapshotUrl': getattr(subscription, 'snapshotUrl', None),
|
||||
'snapshotInterval': getattr(subscription, 'snapshotInterval', 5000),
|
||||
'modelUrl': subscription.modelUrl,
|
||||
'modelId': subscription.modelId,
|
||||
'modelName': subscription.modelName,
|
||||
'cropX1': subscription.cropX1,
|
||||
'cropY1': subscription.cropY1,
|
||||
'cropX2': subscription.cropX2,
|
||||
'cropY2': subscription.cropY2
|
||||
}
|
||||
|
||||
# Create session process
|
||||
success = await self.session_manager.create_session(
|
||||
subscription_id, subscription_config
|
||||
)
|
||||
|
||||
if success:
|
||||
self.active_subscriptions[subscription_id] = subscription_config
|
||||
logger.info(f"Session created successfully for {subscription_id}")
|
||||
|
||||
# Stream handling is now integrated into session worker process
|
||||
else:
|
||||
logger.error(f"Failed to create session for {subscription_id}")
|
||||
return False
|
||||
|
||||
else:
|
||||
# Update existing subscription configuration if needed
|
||||
self.active_subscriptions[subscription_id].update({
|
||||
'modelUrl': subscription.modelUrl,
|
||||
'modelId': subscription.modelId,
|
||||
'modelName': subscription.modelName,
|
||||
'cropX1': subscription.cropX1,
|
||||
'cropY1': subscription.cropY1,
|
||||
'cropX2': subscription.cropX2,
|
||||
'cropY2': subscription.cropY2
|
||||
})
|
||||
|
||||
# Remove sessions for subscriptions that are no longer active
|
||||
current_subscription_ids = set(self.active_subscriptions.keys())
|
||||
removed_subscriptions = current_subscription_ids - new_subscription_ids
|
||||
|
||||
for subscription_id in removed_subscriptions:
|
||||
logger.info(f"Removing session for subscription: {subscription_id}")
|
||||
await self.session_manager.remove_session(subscription_id)
|
||||
del self.active_subscriptions[subscription_id]
|
||||
|
||||
# Update worker state for compatibility
|
||||
worker_state.set_subscriptions(message.subscriptions)
|
||||
|
||||
logger.info(f"Subscription list processed: {len(new_subscription_ids)} active sessions")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling subscription list: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def handle_set_session_id(self, message) -> bool:
|
||||
"""
|
||||
Handle setSessionId message by forwarding to appropriate session process.
|
||||
|
||||
Args:
|
||||
message: SetSessionIdMessage
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
display_id = message.payload.displayIdentifier
|
||||
session_id = message.payload.sessionId
|
||||
|
||||
logger.info(f"Setting session ID {session_id} for display {display_id}")
|
||||
|
||||
# Find subscription identifier for this display
|
||||
subscription_id = None
|
||||
for sub_id in self.active_subscriptions.keys():
|
||||
# Extract display identifier from subscription identifier
|
||||
if display_id in sub_id:
|
||||
subscription_id = sub_id
|
||||
break
|
||||
|
||||
if not subscription_id:
|
||||
logger.error(f"No active subscription found for display {display_id}")
|
||||
return False
|
||||
|
||||
# Forward to session process
|
||||
success = await self.session_manager.set_session_id(
|
||||
subscription_id, str(session_id), display_id
|
||||
)
|
||||
|
||||
if success:
|
||||
# Update worker state for compatibility
|
||||
worker_state.set_session_id(display_id, session_id)
|
||||
logger.info(f"Session ID {session_id} set successfully for {display_id}")
|
||||
else:
|
||||
logger.error(f"Failed to set session ID {session_id} for {display_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting session ID: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def process_frame(self, subscription_id: str, frame: np.ndarray, display_id: str, timestamp: float = None) -> bool:
|
||||
"""
|
||||
Process frame through appropriate session process.
|
||||
|
||||
Args:
|
||||
subscription_id: Subscription identifier
|
||||
frame: Frame to process
|
||||
display_id: Display identifier
|
||||
timestamp: Frame timestamp
|
||||
|
||||
Returns:
|
||||
True if frame was processed successfully
|
||||
"""
|
||||
try:
|
||||
if timestamp is None:
|
||||
timestamp = asyncio.get_event_loop().time()
|
||||
|
||||
# Forward frame to session process
|
||||
success = await self.session_manager.process_frame(
|
||||
subscription_id, frame, display_id, timestamp
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.warning(f"Failed to process frame for subscription {subscription_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing frame for {subscription_id}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def _on_detection_result(self, subscription_id: str, response: DetectionResultResponse):
|
||||
"""
|
||||
Handle detection result from session process.
|
||||
|
||||
Args:
|
||||
subscription_id: Subscription identifier
|
||||
response: Detection result response
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"Received detection result from {subscription_id}: phase={response.phase}")
|
||||
|
||||
# Send imageDetection message via WebSocket (if needed)
|
||||
if self.websocket_handler and hasattr(self.websocket_handler, 'send_message'):
|
||||
from .models import ImageDetectionMessage, DetectionData
|
||||
|
||||
# Convert response detections to the expected format
|
||||
# The DetectionData expects modelId and modelName, and detection dict
|
||||
detection_data = DetectionData(
|
||||
detection=response.detections,
|
||||
modelId=getattr(response, 'model_id', 0), # Get from response if available
|
||||
modelName=getattr(response, 'model_name', 'unknown') # Get from response if available
|
||||
)
|
||||
|
||||
# Convert timestamp to string format if it exists
|
||||
timestamp_str = None
|
||||
if hasattr(response, 'timestamp') and response.timestamp:
|
||||
from datetime import datetime
|
||||
if isinstance(response.timestamp, (int, float)):
|
||||
# Convert Unix timestamp to ISO format string
|
||||
timestamp_str = datetime.fromtimestamp(response.timestamp).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
else:
|
||||
timestamp_str = str(response.timestamp)
|
||||
|
||||
detection_message = ImageDetectionMessage(
|
||||
subscriptionIdentifier=subscription_id,
|
||||
data=detection_data,
|
||||
timestamp=timestamp_str
|
||||
)
|
||||
|
||||
serialized = serialize_outgoing_message(detection_message)
|
||||
await self.websocket_handler.send_message(serialized)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling detection result from {subscription_id}: {e}", exc_info=True)
|
||||
|
||||
async def _on_session_error(self, subscription_id: str, error_response: ErrorResponse):
|
||||
"""
|
||||
Handle error from session process.
|
||||
|
||||
Args:
|
||||
subscription_id: Subscription identifier
|
||||
error_response: Error response
|
||||
"""
|
||||
logger.error(f"Session error from {subscription_id}: {error_response.error_type} - {error_response.error_message}")
|
||||
|
||||
# Send error message via WebSocket if needed
|
||||
if self.websocket_handler and hasattr(self.websocket_handler, 'send_message'):
|
||||
error_message = {
|
||||
'type': 'sessionError',
|
||||
'payload': {
|
||||
'subscriptionIdentifier': subscription_id,
|
||||
'errorType': error_response.error_type,
|
||||
'errorMessage': error_response.error_message,
|
||||
'timestamp': error_response.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
serialized = serialize_outgoing_message(error_message)
|
||||
await self.websocket_handler.send_message(serialized)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send error message: {e}")
|
||||
|
||||
def get_session_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get statistics about active sessions.
|
||||
|
||||
Returns:
|
||||
Dictionary with session statistics
|
||||
"""
|
||||
return {
|
||||
'active_sessions': self.session_manager.get_session_count(),
|
||||
'max_sessions': self.session_manager.max_concurrent_sessions,
|
||||
'subscriptions': list(self.active_subscriptions.keys())
|
||||
}
|
||||
|
||||
async def handle_progression_stage(self, message) -> bool:
|
||||
"""
|
||||
Handle setProgressionStage message.
|
||||
|
||||
Args:
|
||||
message: SetProgressionStageMessage
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
# For now, just update worker state for compatibility
|
||||
# In future phases, this could be forwarded to session processes
|
||||
worker_state.set_progression_stage(
|
||||
message.payload.displayIdentifier,
|
||||
message.payload.progressionStage
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling progression stage: {e}", exc_info=True)
|
||||
return False
|
||||
|
|
@ -24,6 +24,7 @@ from .state import worker_state, SystemMetrics
|
|||
from ..models import ModelManager
|
||||
from ..streaming.manager import shared_stream_manager
|
||||
from ..tracking.integration import TrackingPipelineIntegration
|
||||
from .session_integration import SessionWebSocketIntegration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -48,6 +49,9 @@ class WebSocketHandler:
|
|||
self._heartbeat_count = 0
|
||||
self._last_processed_models: set = set() # Cache of last processed model IDs
|
||||
|
||||
# Initialize session integration
|
||||
self.session_integration = SessionWebSocketIntegration(self)
|
||||
|
||||
async def handle_connection(self) -> None:
|
||||
"""
|
||||
Main connection handler that manages the WebSocket lifecycle.
|
||||
|
@ -66,14 +70,16 @@ class WebSocketHandler:
|
|||
# Send immediate heartbeat to show connection is alive
|
||||
await self._send_immediate_heartbeat()
|
||||
|
||||
# Start background tasks (matching original architecture)
|
||||
stream_task = asyncio.create_task(self._process_streams())
|
||||
# Start session integration
|
||||
await self.session_integration.start()
|
||||
|
||||
# Start background tasks - stream processing now handled by session workers
|
||||
heartbeat_task = asyncio.create_task(self._send_heartbeat())
|
||||
message_task = asyncio.create_task(self._handle_messages())
|
||||
|
||||
logger.info(f"WebSocket background tasks started for {client_info} (stream + heartbeat + message handler)")
|
||||
logger.info(f"WebSocket background tasks started for {client_info} (heartbeat + message handler)")
|
||||
|
||||
# Wait for heartbeat and message tasks (stream runs independently)
|
||||
# Wait for heartbeat and message tasks
|
||||
await asyncio.gather(heartbeat_task, message_task)
|
||||
|
||||
except Exception as e:
|
||||
|
@ -87,6 +93,11 @@ class WebSocketHandler:
|
|||
await stream_task
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"Stream task cancelled for {client_info}")
|
||||
|
||||
# Stop session integration
|
||||
if hasattr(self, 'session_integration'):
|
||||
await self.session_integration.stop()
|
||||
|
||||
await self._cleanup()
|
||||
|
||||
async def _send_immediate_heartbeat(self) -> None:
|
||||
|
@ -180,11 +191,11 @@ class WebSocketHandler:
|
|||
|
||||
try:
|
||||
if message_type == MessageTypes.SET_SUBSCRIPTION_LIST:
|
||||
await self._handle_set_subscription_list(message)
|
||||
await self.session_integration.handle_set_subscription_list(message)
|
||||
elif message_type == MessageTypes.SET_SESSION_ID:
|
||||
await self._handle_set_session_id(message)
|
||||
await self.session_integration.handle_set_session_id(message)
|
||||
elif message_type == MessageTypes.SET_PROGRESSION_STAGE:
|
||||
await self._handle_set_progression_stage(message)
|
||||
await self.session_integration.handle_progression_stage(message)
|
||||
elif message_type == MessageTypes.REQUEST_STATE:
|
||||
await self._handle_request_state(message)
|
||||
elif message_type == MessageTypes.PATCH_SESSION_RESULT:
|
||||
|
@ -619,31 +630,108 @@ class WebSocketHandler:
|
|||
logger.error(f"Failed to send WebSocket message: {e}")
|
||||
raise
|
||||
|
||||
async def send_message(self, message) -> None:
|
||||
"""Public method to send messages (used by session integration)."""
|
||||
await self._send_message(message)
|
||||
|
||||
# DEPRECATED: Stream processing is now handled directly by session worker processes
|
||||
async def _process_streams(self) -> None:
|
||||
"""
|
||||
Stream processing task that handles frame processing and detection.
|
||||
This is a placeholder for Phase 2 - currently just logs that it's running.
|
||||
DEPRECATED: Stream processing task that handles frame processing and detection.
|
||||
Stream processing is now integrated directly into session worker processes.
|
||||
"""
|
||||
logger.info("DEPRECATED: Stream processing task - now handled by session workers")
|
||||
return # Exit immediately - no longer needed
|
||||
|
||||
# OLD CODE (disabled):
|
||||
logger.info("Stream processing task started")
|
||||
try:
|
||||
while self.connected:
|
||||
# Get current subscriptions
|
||||
subscriptions = worker_state.get_all_subscriptions()
|
||||
|
||||
# TODO: Phase 2 - Add actual frame processing logic here
|
||||
# This will include:
|
||||
# - Frame reading from RTSP/HTTP streams
|
||||
# - Model inference using loaded pipelines
|
||||
# - Detection result sending via WebSocket
|
||||
if not subscriptions:
|
||||
await asyncio.sleep(0.5)
|
||||
continue
|
||||
|
||||
# Process frames for each subscription
|
||||
for subscription in subscriptions:
|
||||
await self._process_subscription_frames(subscription)
|
||||
|
||||
# Sleep to prevent excessive CPU usage (similar to old poll_interval)
|
||||
await asyncio.sleep(0.1) # 100ms polling interval
|
||||
await asyncio.sleep(0.25) # 250ms polling interval
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Stream processing task cancelled")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in stream processing: {e}", exc_info=True)
|
||||
|
||||
async def _process_subscription_frames(self, subscription) -> None:
|
||||
"""
|
||||
Process frames for a single subscription by getting frames from stream manager
|
||||
and forwarding them to the appropriate session worker.
|
||||
"""
|
||||
try:
|
||||
subscription_id = subscription.subscriptionIdentifier
|
||||
|
||||
# Get the latest frame from the stream manager
|
||||
frame_data = await self._get_frame_from_stream_manager(subscription)
|
||||
|
||||
if frame_data and frame_data['frame'] is not None:
|
||||
# Extract display identifier (format: "test1;Dispenser Camera 1")
|
||||
display_id = subscription_id.split(';')[-1] if ';' in subscription_id else subscription_id
|
||||
|
||||
# Forward frame to session worker via session integration
|
||||
success = await self.session_integration.process_frame(
|
||||
subscription_id=subscription_id,
|
||||
frame=frame_data['frame'],
|
||||
display_id=display_id,
|
||||
timestamp=frame_data.get('timestamp', asyncio.get_event_loop().time())
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.debug(f"[Frame Processing] Sent frame to session worker for {subscription_id}")
|
||||
else:
|
||||
logger.warning(f"[Frame Processing] Failed to send frame to session worker for {subscription_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing frames for {subscription.subscriptionIdentifier}: {e}")
|
||||
|
||||
async def _get_frame_from_stream_manager(self, subscription) -> dict:
|
||||
"""
|
||||
Get the latest frame from the stream manager for a subscription using existing API.
|
||||
"""
|
||||
try:
|
||||
subscription_id = subscription.subscriptionIdentifier
|
||||
|
||||
# Use existing stream manager API to check if frame is available
|
||||
if not shared_stream_manager.has_frame(subscription_id):
|
||||
# Stream should already be started by session integration
|
||||
return {'frame': None, 'timestamp': None}
|
||||
|
||||
# Get frame using existing API with crop coordinates if available
|
||||
crop_coords = None
|
||||
if hasattr(subscription, 'cropX1') and subscription.cropX1 is not None:
|
||||
crop_coords = (
|
||||
subscription.cropX1, subscription.cropY1,
|
||||
subscription.cropX2, subscription.cropY2
|
||||
)
|
||||
|
||||
# Use existing get_frame method
|
||||
frame = shared_stream_manager.get_frame(subscription_id, crop_coords)
|
||||
if frame is not None:
|
||||
return {
|
||||
'frame': frame,
|
||||
'timestamp': asyncio.get_event_loop().time()
|
||||
}
|
||||
|
||||
return {'frame': None, 'timestamp': None}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting frame from stream manager for {subscription.subscriptionIdentifier}: {e}")
|
||||
return {'frame': None, 'timestamp': None}
|
||||
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""Clean up resources when connection closes."""
|
||||
logger.info("Cleaning up WebSocket connection")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue