fix: Add thread-safe event emission and fix background thread errors
This commit is contained in:
parent
933434fb03
commit
b6d33127e4
|
|
@ -104,6 +104,9 @@ class EUUtilityApp:
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
# Start event bus first (captures main event loop)
|
||||||
|
self.event_bus.start()
|
||||||
|
|
||||||
# Discover and load plugins
|
# Discover and load plugins
|
||||||
self.plugin_manager.discover_all()
|
self.plugin_manager.discover_all()
|
||||||
self.plugin_manager.load_all(auto_activate=True)
|
self.plugin_manager.load_all(auto_activate=True)
|
||||||
|
|
@ -119,10 +122,12 @@ class EUUtilityApp:
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Shutdown the application gracefully."""
|
"""Shutdown the application gracefully."""
|
||||||
if self.plugin_manager:
|
|
||||||
self.plugin_manager.shutdown()
|
|
||||||
if self.game_client:
|
if self.game_client:
|
||||||
self.game_client.stop()
|
self.game_client.stop()
|
||||||
|
if self.plugin_manager:
|
||||||
|
self.plugin_manager.shutdown()
|
||||||
|
if self.event_bus:
|
||||||
|
self.event_bus.stop()
|
||||||
|
|
||||||
def _root_reducer(self, state, action):
|
def _root_reducer(self, state, action):
|
||||||
"""Root state reducer."""
|
"""Root state reducer."""
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ from abc import ABC, abstractmethod
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
|
from queue import Queue
|
||||||
|
from threading import Lock
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, Coroutine, Dict, List, Optional, Set, Type, TypeVar,
|
Any, Callable, Coroutine, Dict, List, Optional, Set, Type, TypeVar,
|
||||||
Union, Generic, Protocol, runtime_checkable
|
Union, Generic, Protocol, runtime_checkable
|
||||||
|
|
@ -238,6 +240,11 @@ class EventBus:
|
||||||
self._event_count = 0
|
self._event_count = 0
|
||||||
self._dropped_count = 0
|
self._dropped_count = 0
|
||||||
|
|
||||||
|
# Thread-safe queue for cross-thread event emission
|
||||||
|
self._thread_queue: Queue = Queue()
|
||||||
|
self._thread_lock = Lock()
|
||||||
|
self._main_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
|
||||||
# ========== Subscription Methods ==========
|
# ========== Subscription Methods ==========
|
||||||
|
|
||||||
def on(
|
def on(
|
||||||
|
|
@ -321,6 +328,24 @@ class EventBus:
|
||||||
|
|
||||||
return sub_id
|
return sub_id
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Start the event bus and capture the main event loop.
|
||||||
|
|
||||||
|
This should be called from the main thread's async context.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._main_loop = asyncio.get_running_loop()
|
||||||
|
self._running = True
|
||||||
|
self._logger.info("Event bus started")
|
||||||
|
except RuntimeError:
|
||||||
|
self._logger.warning("Event bus started without event loop")
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stop the event bus."""
|
||||||
|
self._running = False
|
||||||
|
self._main_loop = None
|
||||||
|
self._logger.info("Event bus stopped")
|
||||||
|
|
||||||
def unsubscribe(self, subscription_id: str) -> bool:
|
def unsubscribe(self, subscription_id: str) -> bool:
|
||||||
"""Unsubscribe from events.
|
"""Unsubscribe from events.
|
||||||
|
|
||||||
|
|
@ -413,17 +438,37 @@ class EventBus:
|
||||||
self._queue.append(event)
|
self._queue.append(event)
|
||||||
self._event_count += 1
|
self._event_count += 1
|
||||||
|
|
||||||
# Process immediately in async context (thread-safe)
|
# Try to process in async context
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
if loop.is_running():
|
# We're in an async context - safe to create task
|
||||||
asyncio.create_task(self._process_event(event))
|
asyncio.create_task(self._process_event(event))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
# No event loop running in this thread - queue for later processing
|
# No event loop in this thread - queue for main thread
|
||||||
pass
|
with self._thread_lock:
|
||||||
|
self._thread_queue.put(event)
|
||||||
|
# Try to notify main loop if available
|
||||||
|
if self._main_loop and self._main_loop.is_running():
|
||||||
|
try:
|
||||||
|
self._main_loop.call_soon_threadsafe(
|
||||||
|
lambda: asyncio.create_task(self._process_thread_queue())
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
async def _process_thread_queue(self) -> None:
|
||||||
|
"""Process events queued from other threads."""
|
||||||
|
while not self._thread_queue.empty():
|
||||||
|
try:
|
||||||
|
with self._thread_lock:
|
||||||
|
if not self._thread_queue.empty():
|
||||||
|
event = self._thread_queue.get_nowait()
|
||||||
|
await self._process_event(event)
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
async def emit_async(
|
async def emit_async(
|
||||||
self,
|
self,
|
||||||
event_type: str,
|
event_type: str,
|
||||||
|
|
|
||||||
|
|
@ -188,10 +188,14 @@ class GameClient:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._logger.error(f"Error in callback: {e}")
|
self._logger.error(f"Error in callback: {e}")
|
||||||
|
|
||||||
# Emit to event bus
|
# Emit to event bus (thread-safe)
|
||||||
if self.event_bus:
|
if self.event_bus:
|
||||||
try:
|
try:
|
||||||
self.event_bus.emit(f"game.{event_type}", data, source="game_client")
|
self.event_bus.emit(f"game.{event_type}", data, source="game_client")
|
||||||
|
except RuntimeError as e:
|
||||||
|
# No event loop in this thread - event is queued for main thread
|
||||||
|
# This is expected when emitting from background threads
|
||||||
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._logger.error(f"Error emitting to event bus: {e}")
|
self._logger.error(f"Error emitting to event bus: {e}")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue