mirror of
https://github.com/ipnet-mesh/meshcore-mqtt.git
synced 2026-07-06 18:01:11 +02:00
306 lines
9.5 KiB
Python
306 lines
9.5 KiB
Python
"""Message queue system for inbox/outbox communication between threads."""
|
|
|
|
import asyncio
|
|
import logging
|
|
import time
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Any, Dict, Optional
|
|
|
|
|
|
class MessageType(Enum):
|
|
"""Types of messages that can be sent between components."""
|
|
|
|
# MeshCore to MQTT
|
|
MESHCORE_MESSAGE = "meshcore_message"
|
|
MESHCORE_STATUS = "meshcore_status"
|
|
MESHCORE_EVENT = "meshcore_event"
|
|
|
|
# MQTT to MeshCore
|
|
MQTT_COMMAND = "mqtt_command"
|
|
MQTT_STATUS = "mqtt_status"
|
|
|
|
# Control messages
|
|
SHUTDOWN = "shutdown"
|
|
HEALTH_CHECK = "health_check"
|
|
|
|
|
|
class ComponentStatus(Enum):
|
|
"""Status of a component."""
|
|
|
|
STARTING = "starting"
|
|
RUNNING = "running"
|
|
CONNECTED = "connected"
|
|
DISCONNECTED = "disconnected"
|
|
ERROR = "error"
|
|
STOPPING = "stopping"
|
|
STOPPED = "stopped"
|
|
|
|
|
|
@dataclass
|
|
class Message:
|
|
"""A message in the inbox/outbox system."""
|
|
|
|
id: str
|
|
message_type: MessageType
|
|
source: str
|
|
target: str
|
|
payload: Any
|
|
timestamp: float
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
message_type: MessageType,
|
|
source: str,
|
|
target: str,
|
|
payload: Any,
|
|
metadata: Optional[Dict[str, Any]] = None,
|
|
) -> "Message":
|
|
"""Create a new message with auto-generated ID and timestamp."""
|
|
import uuid
|
|
|
|
return cls(
|
|
id=uuid.uuid4().hex[:8],
|
|
message_type=message_type,
|
|
source=source,
|
|
target=target,
|
|
payload=payload,
|
|
timestamp=time.time(),
|
|
metadata=metadata or {},
|
|
)
|
|
|
|
|
|
class MessageQueue:
|
|
"""Thread-safe message queue for component communication."""
|
|
|
|
def __init__(self, name: str, max_size: int = 1000) -> None:
|
|
"""Initialize the message queue."""
|
|
self.name = name
|
|
self.max_size = max_size
|
|
self.logger = logging.getLogger(f"{__name__}.{name}")
|
|
|
|
# Use asyncio.Queue for thread-safe async operations
|
|
self._queue: asyncio.Queue[Message] = asyncio.Queue(maxsize=max_size)
|
|
self._dropped_messages = 0
|
|
|
|
async def put(self, message: Message, timeout: Optional[float] = None) -> bool:
|
|
"""Put a message in the queue. Returns True if successful."""
|
|
try:
|
|
if timeout is None:
|
|
await self._queue.put(message)
|
|
else:
|
|
await asyncio.wait_for(self._queue.put(message), timeout=timeout)
|
|
|
|
self.logger.debug(
|
|
f"Queued {message.message_type.value} from {message.source} "
|
|
f"to {message.target} (id: {message.id})"
|
|
)
|
|
return True
|
|
|
|
except asyncio.TimeoutError:
|
|
self.logger.warning(
|
|
f"Timeout queueing message {message.id} "
|
|
f"({message.message_type.value})"
|
|
)
|
|
return False
|
|
except asyncio.QueueFull:
|
|
self._dropped_messages += 1
|
|
self.logger.warning(
|
|
f"Queue {self.name} full, dropping message {message.id} "
|
|
f"(total dropped: {self._dropped_messages})"
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Error queueing message {message.id}: {e}")
|
|
return False
|
|
|
|
async def get(self, timeout: Optional[float] = None) -> Optional[Message]:
|
|
"""Get a message from the queue. Returns None on timeout or error."""
|
|
try:
|
|
if timeout is None:
|
|
message = await self._queue.get()
|
|
else:
|
|
message = await asyncio.wait_for(self._queue.get(), timeout=timeout)
|
|
|
|
self.logger.debug(
|
|
f"Dequeued {message.message_type.value} from {message.source} "
|
|
f"to {message.target} (id: {message.id})"
|
|
)
|
|
return message
|
|
|
|
except asyncio.TimeoutError:
|
|
return None
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting message from queue: {e}")
|
|
return None
|
|
|
|
async def get_nowait(self) -> Optional[Message]:
|
|
"""Get a message without waiting. Returns None if queue is empty."""
|
|
try:
|
|
message = self._queue.get_nowait()
|
|
self.logger.debug(
|
|
f"Dequeued (nowait) {message.message_type.value} from "
|
|
f"{message.source} to {message.target} (id: {message.id})"
|
|
)
|
|
return message
|
|
except asyncio.QueueEmpty:
|
|
return None
|
|
except Exception as e:
|
|
self.logger.error(f"Error getting message nowait: {e}")
|
|
return None
|
|
|
|
def qsize(self) -> int:
|
|
"""Get the approximate size of the queue."""
|
|
return self._queue.qsize()
|
|
|
|
def empty(self) -> bool:
|
|
"""Check if the queue is empty."""
|
|
return self._queue.empty()
|
|
|
|
def full(self) -> bool:
|
|
"""Check if the queue is full."""
|
|
return self._queue.full()
|
|
|
|
def stats(self) -> Dict[str, Any]:
|
|
"""Get queue statistics."""
|
|
return {
|
|
"name": self.name,
|
|
"size": self.qsize(),
|
|
"max_size": self.max_size,
|
|
"dropped_messages": self._dropped_messages,
|
|
"empty": self.empty(),
|
|
"full": self.full(),
|
|
}
|
|
|
|
|
|
class MessageBus:
|
|
"""Central message bus coordinating inbox/outbox between components."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the message bus."""
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# Component queues
|
|
self._queues: Dict[str, MessageQueue] = {}
|
|
|
|
# Component status tracking
|
|
self._component_status: Dict[str, ComponentStatus] = {}
|
|
|
|
# Running state
|
|
self._running = False
|
|
self._shutdown_event = asyncio.Event()
|
|
|
|
def register_component(self, name: str, queue_size: int = 1000) -> MessageQueue:
|
|
"""Register a component and get its message queue."""
|
|
if name in self._queues:
|
|
self.logger.warning(f"Component {name} already registered")
|
|
return self._queues[name]
|
|
|
|
queue = MessageQueue(name, max_size=queue_size)
|
|
self._queues[name] = queue
|
|
self._component_status[name] = ComponentStatus.STARTING
|
|
|
|
self.logger.info(f"Registered component: {name}")
|
|
return queue
|
|
|
|
def update_component_status(self, name: str, status: ComponentStatus) -> None:
|
|
"""Update the status of a component."""
|
|
old_status = self._component_status.get(name, ComponentStatus.STARTING)
|
|
self._component_status[name] = status
|
|
|
|
if old_status != status:
|
|
self.logger.info(f"Component {name}: {old_status.value} -> {status.value}")
|
|
|
|
def get_component_status(self, name: str) -> Optional[ComponentStatus]:
|
|
"""Get the status of a component."""
|
|
return self._component_status.get(name)
|
|
|
|
async def send_message(
|
|
self, message: Message, timeout: Optional[float] = 1.0
|
|
) -> bool:
|
|
"""Send a message to the target component."""
|
|
target_queue = self._queues.get(message.target)
|
|
if not target_queue:
|
|
self.logger.error(f"Unknown target component: {message.target}")
|
|
return False
|
|
|
|
return await target_queue.put(message, timeout=timeout)
|
|
|
|
async def broadcast_message(
|
|
self,
|
|
message_type: MessageType,
|
|
source: str,
|
|
payload: Any,
|
|
exclude: Optional[list[str]] = None,
|
|
metadata: Optional[Dict[str, Any]] = None,
|
|
) -> int:
|
|
"""Broadcast a message to all components except excluded ones."""
|
|
exclude = exclude or []
|
|
exclude.append(source) # Don't send to self
|
|
|
|
sent_count = 0
|
|
for target in self._queues.keys():
|
|
if target not in exclude:
|
|
message = Message.create(
|
|
message_type=message_type,
|
|
source=source,
|
|
target=target,
|
|
payload=payload,
|
|
metadata=metadata,
|
|
)
|
|
if await self.send_message(message):
|
|
sent_count += 1
|
|
|
|
return sent_count
|
|
|
|
async def shutdown(self) -> None:
|
|
"""Shutdown the message bus and notify all components."""
|
|
self.logger.info("Shutting down message bus")
|
|
self._running = False
|
|
|
|
# Send shutdown messages to all components
|
|
await self.broadcast_message(
|
|
MessageType.SHUTDOWN,
|
|
source="message_bus",
|
|
payload={"reason": "shutdown_requested"},
|
|
)
|
|
|
|
self._shutdown_event.set()
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
"""Get statistics for all queues and components."""
|
|
return {
|
|
"components": {
|
|
name: {"status": status.value, "queue": queue.stats()}
|
|
for name, (status, queue) in zip(
|
|
self._component_status.keys(),
|
|
[
|
|
(self._component_status[name], self._queues[name])
|
|
for name in self._component_status.keys()
|
|
],
|
|
)
|
|
},
|
|
"total_components": len(self._queues),
|
|
"running": self._running,
|
|
}
|
|
|
|
|
|
# Global message bus instance
|
|
_message_bus: Optional[MessageBus] = None
|
|
|
|
|
|
def get_message_bus() -> MessageBus:
|
|
"""Get the global message bus instance."""
|
|
global _message_bus
|
|
if _message_bus is None:
|
|
_message_bus = MessageBus()
|
|
return _message_bus
|
|
|
|
|
|
def reset_message_bus() -> None:
|
|
"""Reset the global message bus (mainly for testing)."""
|
|
global _message_bus
|
|
_message_bus = None
|