1+ import asyncio
2+ import os
13import threading
24import time
35from typing import Any , Optional
@@ -16,17 +18,14 @@ def __init__(self, db_path: Optional[str] = None):
1618
1719 # Use the same database as the evaluation row store
1820 if db_path is None :
19- import os
20-
2121 from eval_protocol .directory_utils import find_eval_protocol_dir
2222
2323 eval_protocol_dir = find_eval_protocol_dir ()
2424 db_path = os .path .join (eval_protocol_dir , "logs.db" )
2525
26- self ._db = SqliteEventBusDatabase (db_path )
26+ self ._db : SqliteEventBusDatabase = SqliteEventBusDatabase (db_path )
2727 self ._running = False
28- self ._listener_thread : Optional [threading .Thread ] = None
29- self ._process_id = str (uuid4 ())
28+ self ._process_id = str (os .getpid ())
3029
3130 def emit (self , event_type : str , data : Any ) -> None :
3231 """Emit an event to all subscribers.
@@ -64,67 +63,54 @@ def start_listening(self) -> None:
6463
6564 logger .debug ("[CROSS_PROCESS_LISTEN] Starting cross-process event listening" )
6665 self ._running = True
67- self ._start_database_listener ()
68- logger .debug ("[CROSS_PROCESS_LISTEN] Started database listener thread" )
66+ loop = asyncio .get_running_loop ()
67+ loop .create_task (self ._database_listener_task ())
68+ logger .debug ("[CROSS_PROCESS_LISTEN] Started async database listener task" )
6969
7070 def stop_listening (self ) -> None :
7171 """Stop listening for cross-process events."""
7272 logger .debug ("[CROSS_PROCESS_LISTEN] Stopping cross-process event listening" )
7373 self ._running = False
74- if self ._listener_thread and self ._listener_thread .is_alive ():
75- logger .debug ("[CROSS_PROCESS_LISTEN] Waiting for listener thread to stop" )
76- self ._listener_thread .join (timeout = 1 )
77- logger .debug ("[CROSS_PROCESS_LISTEN] Listener thread stopped" )
78-
79- def _start_database_listener (self ) -> None :
80- """Start database-based event listener."""
81-
82- def database_listener ():
83- logger .debug ("[CROSS_PROCESS_LISTENER] Starting database listener loop" )
84- last_cleanup = time .time ()
85-
86- while self ._running :
87- try :
88- # Get unprocessed events from other processes
89- events = self ._db .get_unprocessed_events (self ._process_id )
90- if events :
91- logger .debug (f"[CROSS_PROCESS_LISTENER] Found { len (events )} unprocessed events" )
92-
93- for event in events :
94- if not self ._running :
95- break
96-
97- try :
98- logger .debug (
99- f"[CROSS_PROCESS_LISTENER] Processing event { event ['event_id' ]} of type { event ['event_type' ]} "
100- )
101- # Handle the event
102- self ._handle_cross_process_event (event ["event_type" ], event ["data" ])
103- logger .debug (f"[CROSS_PROCESS_LISTENER] Successfully processed event { event ['event_id' ]} " )
104-
105- # Mark as processed
106- self ._db .mark_event_processed (event ["event_id" ])
107- logger .debug (f"[CROSS_PROCESS_LISTENER] Marked event { event ['event_id' ]} as processed" )
108-
109- except Exception as e :
110- logger .debug (f"[CROSS_PROCESS_LISTENER] Failed to process event { event ['event_id' ]} : { e } " )
111-
112- # Clean up old events every hour
113- current_time = time .time ()
114- if current_time - last_cleanup >= 3600 :
115- logger .debug ("[CROSS_PROCESS_LISTENER] Cleaning up old events" )
116- self ._db .cleanup_old_events ()
117- last_cleanup = current_time
118-
119- # Small sleep to prevent busy waiting
120- time .sleep (0.1 )
121-
122- except Exception as e :
123- logger .debug (f"[CROSS_PROCESS_LISTENER] Database listener error: { e } " )
124- time .sleep (1 )
125-
126- self ._listener_thread = threading .Thread (target = database_listener , daemon = True )
127- self ._listener_thread .start ()
74+
75+ async def _database_listener_task (self ) -> None :
76+ """Single database listener task that processes events and recreates itself."""
77+ if not self ._running :
78+ # this should end the task loop
79+ logger .debug ("[CROSS_PROCESS_LISTENER] Stopping database listener task" )
80+ return
81+
82+ # Get unprocessed events from other processes
83+ events = self ._db .get_unprocessed_events (str (self ._process_id ))
84+ if events :
85+ logger .debug (f"[CROSS_PROCESS_LISTENER] Found { len (events )} unprocessed events" )
86+ else :
87+ logger .debug (f"[CROSS_PROCESS_LISTENER] No unprocessed events found for process { self ._process_id } " )
88+
89+ for event in events :
90+ logger .debug (
91+ f"[CROSS_PROCESS_LISTENER] Processing event { event ['event_id' ]} of type { event ['event_type' ]} "
92+ )
93+ # Handle the event
94+ self ._handle_cross_process_event (event ["event_type" ], event ["data" ])
95+ logger .debug (f"[CROSS_PROCESS_LISTENER] Successfully processed event { event ['event_id' ]} " )
96+
97+ # Mark as processed
98+ self ._db .mark_event_processed (event ["event_id" ])
99+ logger .debug (f"[CROSS_PROCESS_LISTENER] Marked event { event ['event_id' ]} as processed" )
100+
101+ # Clean up old events every hour
102+ current_time = time .time ()
103+ if not hasattr (self , "_last_cleanup" ):
104+ self ._last_cleanup = current_time
105+ elif current_time - self ._last_cleanup >= 3600 :
106+ logger .debug ("[CROSS_PROCESS_LISTENER] Cleaning up old events" )
107+ self ._db .cleanup_old_events ()
108+ self ._last_cleanup = current_time
109+
110+ # Schedule the next task if still running
111+ await asyncio .sleep (1.0 )
112+ loop = asyncio .get_running_loop ()
113+ loop .create_task (self ._database_listener_task ())
128114
129115 def _handle_cross_process_event (self , event_type : str , data : Any ) -> None :
130116 """Handle events received from other processes."""
0 commit comments