mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-23 20:21:03 +02:00
Clean shutdown
This commit is contained in:
parent
47eae4a2b3
commit
529da32195
2 changed files with 89 additions and 45 deletions
|
|
@ -93,10 +93,14 @@ class Subscriber:
|
||||||
if self.draining and drain_end_time is None:
|
if self.draining and drain_end_time is None:
|
||||||
drain_end_time = time.time() + self.drain_timeout
|
drain_end_time = time.time() + self.drain_timeout
|
||||||
logger.info(f"Subscriber entering drain mode, timeout={self.drain_timeout}s")
|
logger.info(f"Subscriber entering drain mode, timeout={self.drain_timeout}s")
|
||||||
|
|
||||||
# Stop accepting new messages from Pulsar during drain
|
# Stop accepting new messages from Pulsar during drain
|
||||||
if self.consumer:
|
if self.consumer:
|
||||||
self.consumer.pause_message_listener()
|
try:
|
||||||
|
self.consumer.pause_message_listener()
|
||||||
|
except _pulsar.InvalidConfiguration:
|
||||||
|
# Not all consumers have message listeners (e.g., blocking receive mode)
|
||||||
|
pass
|
||||||
|
|
||||||
# Check drain timeout
|
# Check drain timeout
|
||||||
if self.draining and drain_end_time and time.time() > drain_end_time:
|
if self.draining and drain_end_time and time.time() > drain_end_time:
|
||||||
|
|
@ -151,12 +155,21 @@ class Subscriber:
|
||||||
finally:
|
finally:
|
||||||
# Negative acknowledge any pending messages
|
# Negative acknowledge any pending messages
|
||||||
for msg in self.pending_acks.values():
|
for msg in self.pending_acks.values():
|
||||||
self.consumer.negative_acknowledge(msg)
|
try:
|
||||||
|
self.consumer.negative_acknowledge(msg)
|
||||||
|
except _pulsar.AlreadyClosed:
|
||||||
|
pass # Consumer already closed
|
||||||
self.pending_acks.clear()
|
self.pending_acks.clear()
|
||||||
|
|
||||||
if self.consumer:
|
if self.consumer:
|
||||||
self.consumer.unsubscribe()
|
try:
|
||||||
self.consumer.close()
|
self.consumer.unsubscribe()
|
||||||
|
except _pulsar.AlreadyClosed:
|
||||||
|
pass # Already closed
|
||||||
|
try:
|
||||||
|
self.consumer.close()
|
||||||
|
except _pulsar.AlreadyClosed:
|
||||||
|
pass # Already closed
|
||||||
self.consumer = None
|
self.consumer = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ def format_message(queue_name, msg):
|
||||||
return header + body + "\n"
|
return header + body + "\n"
|
||||||
|
|
||||||
|
|
||||||
async def monitor_queue(subscriber, queue_name, central_queue, monitor_id):
|
async def monitor_queue(subscriber, queue_name, central_queue, monitor_id, shutdown_event):
|
||||||
"""
|
"""
|
||||||
Monitor a single queue via Subscriber and forward messages to central queue.
|
Monitor a single queue via Subscriber and forward messages to central queue.
|
||||||
|
|
||||||
|
|
@ -73,53 +73,71 @@ async def monitor_queue(subscriber, queue_name, central_queue, monitor_id):
|
||||||
queue_name: Name of the queue (for logging)
|
queue_name: Name of the queue (for logging)
|
||||||
central_queue: asyncio.Queue to forward messages to
|
central_queue: asyncio.Queue to forward messages to
|
||||||
monitor_id: Unique ID for this monitor's subscription
|
monitor_id: Unique ID for this monitor's subscription
|
||||||
|
shutdown_event: asyncio.Event to signal shutdown
|
||||||
"""
|
"""
|
||||||
|
msg_queue = None
|
||||||
try:
|
try:
|
||||||
# Subscribe to all messages from this Subscriber
|
# Subscribe to all messages from this Subscriber
|
||||||
msg_queue = await subscriber.subscribe_all(monitor_id)
|
msg_queue = await subscriber.subscribe_all(monitor_id)
|
||||||
|
|
||||||
while True:
|
while not shutdown_event.is_set():
|
||||||
# Read from Subscriber's internal queue
|
try:
|
||||||
msg = await msg_queue.get()
|
# Read from Subscriber's internal queue with timeout
|
||||||
timestamp = datetime.now()
|
msg = await asyncio.wait_for(msg_queue.get(), timeout=0.5)
|
||||||
formatted = format_message(queue_name, msg)
|
timestamp = datetime.now()
|
||||||
|
formatted = format_message(queue_name, msg)
|
||||||
|
|
||||||
# Forward to central queue for writing
|
# Forward to central queue for writing
|
||||||
await central_queue.put((timestamp, queue_name, formatted))
|
await central_queue.put((timestamp, queue_name, formatted))
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# No message, check shutdown flag again
|
||||||
|
continue
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
# Task cancelled during shutdown
|
|
||||||
await subscriber.unsubscribe_all(monitor_id)
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"\n{'='*80}\n[{datetime.now().isoformat()}] ERROR in monitor for {queue_name}\n{'='*80}\n{e}\n"
|
if not shutdown_event.is_set():
|
||||||
await central_queue.put((datetime.now(), queue_name, error_msg))
|
error_msg = f"\n{'='*80}\n[{datetime.now().isoformat()}] ERROR in monitor for {queue_name}\n{'='*80}\n{e}\n"
|
||||||
|
await central_queue.put((datetime.now(), queue_name, error_msg))
|
||||||
|
finally:
|
||||||
|
# Clean unsubscribe
|
||||||
|
if msg_queue is not None:
|
||||||
|
try:
|
||||||
|
await subscriber.unsubscribe_all(monitor_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def log_writer(central_queue, file_handle, console_output=True):
|
async def log_writer(central_queue, file_handle, shutdown_event, console_output=True):
|
||||||
"""
|
"""
|
||||||
Write messages from central queue to file.
|
Write messages from central queue to file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
central_queue: asyncio.Queue containing (timestamp, queue_name, formatted_msg) tuples
|
central_queue: asyncio.Queue containing (timestamp, queue_name, formatted_msg) tuples
|
||||||
file_handle: Open file handle to write to
|
file_handle: Open file handle to write to
|
||||||
|
shutdown_event: asyncio.Event to signal shutdown
|
||||||
console_output: Whether to print abbreviated messages to console
|
console_output: Whether to print abbreviated messages to console
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
while True:
|
while not shutdown_event.is_set():
|
||||||
timestamp, queue_name, formatted_msg = await central_queue.get()
|
try:
|
||||||
|
# Wait for messages with timeout to check shutdown flag
|
||||||
|
timestamp, queue_name, formatted_msg = await asyncio.wait_for(
|
||||||
|
central_queue.get(), timeout=0.5
|
||||||
|
)
|
||||||
|
|
||||||
# Write to file
|
# Write to file
|
||||||
file_handle.write(formatted_msg)
|
file_handle.write(formatted_msg)
|
||||||
file_handle.flush()
|
file_handle.flush()
|
||||||
|
|
||||||
# Print abbreviated message to console
|
# Print abbreviated message to console
|
||||||
if console_output:
|
if console_output:
|
||||||
time_str = timestamp.strftime('%H:%M:%S')
|
time_str = timestamp.strftime('%H:%M:%S')
|
||||||
print(f"[{time_str}] {queue_name}: Message received")
|
print(f"[{time_str}] {queue_name}: Message received")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# No message, check shutdown flag again
|
||||||
|
continue
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
finally:
|
||||||
# Flush remaining messages before shutdown
|
# Flush remaining messages after shutdown
|
||||||
while not central_queue.empty():
|
while not central_queue.empty():
|
||||||
try:
|
try:
|
||||||
timestamp, queue_name, formatted_msg = central_queue.get_nowait()
|
timestamp, queue_name, formatted_msg = central_queue.get_nowait()
|
||||||
|
|
@ -127,7 +145,6 @@ async def log_writer(central_queue, file_handle, console_output=True):
|
||||||
file_handle.flush()
|
file_handle.flush()
|
||||||
except asyncio.QueueEmpty:
|
except asyncio.QueueEmpty:
|
||||||
break
|
break
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
async def async_main(queues, output_file, pulsar_host, listener_name, subscriber_name, append_mode):
|
async def async_main(queues, output_file, pulsar_host, listener_name, subscriber_name, append_mode):
|
||||||
|
|
@ -193,26 +210,40 @@ async def async_main(queues, output_file, pulsar_host, listener_name, subscriber
|
||||||
f.write(f"{'#'*80}\n")
|
f.write(f"{'#'*80}\n")
|
||||||
f.flush()
|
f.flush()
|
||||||
|
|
||||||
|
# Create shutdown event for clean coordination
|
||||||
|
shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
# Start monitoring tasks
|
# Start monitoring tasks
|
||||||
|
tasks = []
|
||||||
try:
|
try:
|
||||||
try:
|
# Create one monitor task per subscriber
|
||||||
async with asyncio.TaskGroup() as tg:
|
for queue_name, sub in subscribers:
|
||||||
# Create one monitor task per subscriber
|
task = asyncio.create_task(
|
||||||
for queue_name, sub in subscribers:
|
monitor_queue(sub, queue_name, central_queue, "logger", shutdown_event)
|
||||||
tg.create_task(monitor_queue(sub, queue_name, central_queue, "logger"))
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
# Create single writer task
|
# Create single writer task
|
||||||
tg.create_task(log_writer(central_queue, f))
|
writer_task = asyncio.create_task(
|
||||||
|
log_writer(central_queue, f, shutdown_event)
|
||||||
|
)
|
||||||
|
tasks.append(writer_task)
|
||||||
|
|
||||||
except* Exception as eg:
|
# Wait for all tasks (they check shutdown_event)
|
||||||
# TaskGroup exception group
|
await asyncio.gather(*tasks)
|
||||||
for exc in eg.exceptions:
|
|
||||||
if not isinstance(exc, asyncio.CancelledError):
|
|
||||||
print(f"Task error: {exc}", file=sys.stderr)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\nStopping...")
|
print("\n\nStopping...")
|
||||||
finally:
|
finally:
|
||||||
|
# Signal shutdown to all tasks
|
||||||
|
shutdown_event.set()
|
||||||
|
|
||||||
|
# Wait for tasks to finish cleanly (with timeout)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), timeout=2.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
print("Warning: Shutdown timeout", file=sys.stderr)
|
||||||
|
|
||||||
# Write session end marker
|
# Write session end marker
|
||||||
f.write(f"\n{'#'*80}\n")
|
f.write(f"\n{'#'*80}\n")
|
||||||
f.write(f"# Session ended: {datetime.now().isoformat()}\n")
|
f.write(f"# Session ended: {datetime.now().isoformat()}\n")
|
||||||
|
|
@ -222,7 +253,7 @@ async def async_main(queues, output_file, pulsar_host, listener_name, subscriber
|
||||||
print(f"Error writing to {output_file}: {e}", file=sys.stderr)
|
print(f"Error writing to {output_file}: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
finally:
|
finally:
|
||||||
# Cleanup subscribers
|
# Clean shutdown of Subscribers
|
||||||
for _, sub in subscribers:
|
for _, sub in subscribers:
|
||||||
await sub.stop()
|
await sub.stop()
|
||||||
client.close()
|
client.close()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue