225 lines
7.0 KiB
Python
225 lines
7.0 KiB
Python
"""
|
|
Trigger queue - priority queue with sequence number assignment.
|
|
|
|
All operations flow through this queue:
|
|
- WebSocket messages from users
|
|
- Cron scheduled tasks
|
|
- DataSource bar updates
|
|
- Manual triggers
|
|
|
|
Queue assigns seq numbers on dequeue, executes triggers, and submits to coordinator.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from .context import ExecutionContext, clear_execution_context, set_execution_context
|
|
from .coordinator import CommitCoordinator
|
|
from .types import Priority, PriorityTuple, Trigger
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TriggerQueue:
|
|
"""
|
|
Priority queue for trigger execution.
|
|
|
|
Key responsibilities:
|
|
- Maintain priority queue (high priority dequeued first)
|
|
- Assign sequence numbers on dequeue (determines commit order)
|
|
- Execute triggers with context set
|
|
- Submit results to CommitCoordinator
|
|
- Handle execution errors gracefully
|
|
"""
|
|
|
|
def __init__(self, coordinator: CommitCoordinator):
|
|
"""
|
|
Initialize trigger queue.
|
|
|
|
Args:
|
|
coordinator: CommitCoordinator for handling commits
|
|
"""
|
|
self._coordinator = coordinator
|
|
self._queue: asyncio.PriorityQueue = asyncio.PriorityQueue()
|
|
self._seq_counter = 0
|
|
self._seq_lock = asyncio.Lock()
|
|
self._processor_task: Optional[asyncio.Task] = None
|
|
self._running = False
|
|
|
|
async def start(self) -> None:
|
|
"""Start the queue processor"""
|
|
if self._running:
|
|
logger.warning("TriggerQueue already running")
|
|
return
|
|
|
|
self._running = True
|
|
self._processor_task = asyncio.create_task(self._process_loop())
|
|
logger.info("TriggerQueue started")
|
|
|
|
async def stop(self) -> None:
|
|
"""Stop the queue processor gracefully"""
|
|
if not self._running:
|
|
return
|
|
|
|
self._running = False
|
|
|
|
if self._processor_task:
|
|
self._processor_task.cancel()
|
|
try:
|
|
await self._processor_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
logger.info("TriggerQueue stopped")
|
|
|
|
async def enqueue(
|
|
self,
|
|
trigger: Trigger,
|
|
priority_override: Optional[Priority | PriorityTuple] = None
|
|
) -> int:
|
|
"""
|
|
Add a trigger to the queue.
|
|
|
|
Args:
|
|
trigger: Trigger to execute
|
|
priority_override: Override priority (simple Priority or tuple)
|
|
If None, uses trigger's priority/priority_tuple
|
|
If Priority enum, creates single-element tuple
|
|
If tuple, uses as-is
|
|
|
|
Returns:
|
|
Queue sequence number (appended to priority tuple)
|
|
|
|
Examples:
|
|
# Simple priority
|
|
await queue.enqueue(trigger, Priority.USER_AGENT)
|
|
# Results in: (Priority.USER_AGENT, queue_seq)
|
|
|
|
# Tuple priority with event time
|
|
await queue.enqueue(
|
|
trigger,
|
|
(Priority.DATA_SOURCE, bar_data['time'])
|
|
)
|
|
# Results in: (Priority.DATA_SOURCE, bar_time, queue_seq)
|
|
|
|
# Let trigger decide
|
|
await queue.enqueue(trigger)
|
|
"""
|
|
# Get monotonic seq for queue ordering (appended to tuple)
|
|
async with self._seq_lock:
|
|
queue_seq = self._seq_counter
|
|
self._seq_counter += 1
|
|
|
|
# Determine priority tuple
|
|
if priority_override is not None:
|
|
if isinstance(priority_override, Priority):
|
|
# Convert simple priority to tuple
|
|
priority_tuple = (priority_override.value, queue_seq)
|
|
else:
|
|
# Use provided tuple, append queue_seq
|
|
priority_tuple = priority_override + (queue_seq,)
|
|
else:
|
|
# Let trigger determine its own priority tuple
|
|
priority_tuple = trigger.get_priority_tuple(queue_seq)
|
|
|
|
# Priority queue: (priority_tuple, trigger)
|
|
# Python's PriorityQueue compares tuples element-by-element
|
|
await self._queue.put((priority_tuple, trigger))
|
|
|
|
logger.debug(
|
|
f"Enqueued: {trigger.name} with priority_tuple={priority_tuple}"
|
|
)
|
|
|
|
return queue_seq
|
|
|
|
async def _process_loop(self) -> None:
|
|
"""
|
|
Main processing loop.
|
|
|
|
Dequeues triggers, assigns execution seq, executes, and submits to coordinator.
|
|
"""
|
|
execution_seq = 0 # Separate counter for execution sequence
|
|
|
|
while self._running:
|
|
try:
|
|
# Wait for next trigger (with timeout to check _running flag)
|
|
try:
|
|
priority_tuple, trigger = await asyncio.wait_for(
|
|
self._queue.get(), timeout=1.0
|
|
)
|
|
except asyncio.TimeoutError:
|
|
continue
|
|
|
|
# Assign execution sequence number
|
|
execution_seq += 1
|
|
|
|
logger.info(
|
|
f"Dequeued: seq={execution_seq}, trigger={trigger.name}, "
|
|
f"priority_tuple={priority_tuple}"
|
|
)
|
|
|
|
# Execute in background (don't block queue)
|
|
asyncio.create_task(
|
|
self._execute_trigger(execution_seq, trigger)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in process loop: {e}", exc_info=True)
|
|
|
|
async def _execute_trigger(self, seq: int, trigger: Trigger) -> None:
|
|
"""
|
|
Execute a trigger with proper context and error handling.
|
|
|
|
Args:
|
|
seq: Execution sequence number
|
|
trigger: Trigger to execute
|
|
"""
|
|
# Set up execution context
|
|
ctx = ExecutionContext(
|
|
seq=seq,
|
|
trigger_name=trigger.name,
|
|
)
|
|
set_execution_context(ctx)
|
|
|
|
# Record execution start with coordinator
|
|
await self._coordinator.start_execution(seq, trigger)
|
|
|
|
try:
|
|
logger.info(f"Executing: seq={seq}, trigger={trigger.name}")
|
|
|
|
# Execute trigger (can be long-running)
|
|
commit_intents = await trigger.execute()
|
|
|
|
logger.info(
|
|
f"Execution complete: seq={seq}, {len(commit_intents)} commit intents"
|
|
)
|
|
|
|
# Submit for sequential commit
|
|
await self._coordinator.submit_for_commit(seq, commit_intents)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Execution failed: seq={seq}, trigger={trigger.name}, error={e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
# Notify coordinator of failure
|
|
await self._coordinator.execution_failed(seq, e)
|
|
|
|
finally:
|
|
clear_execution_context()
|
|
|
|
def get_queue_size(self) -> int:
|
|
"""Get current queue size (approximate)"""
|
|
return self._queue.qsize()
|
|
|
|
def is_running(self) -> bool:
|
|
"""Check if queue processor is running"""
|
|
return self._running
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"TriggerQueue(running={self._running}, queue_size={self.get_queue_size()})"
|
|
)
|