Coverage for qreward / utils / scheduler / priority_queue.py: 100.00%
75 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 19:32 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 19:32 +0000
1"""Thread-safe priority queue for scheduling tasks by priority level."""
3import heapq
4import itertools
5import threading
6import time
7from dataclasses import dataclass, field
8from enum import IntEnum
9from typing import Any, Optional
12class Priority(IntEnum):
13 """Task priority levels. Lower value = higher priority."""
15 HIGH = 0
16 NORMAL = 5
17 LOW = 9
20@dataclass(order=True)
21class _PriorityEntry:
22 """Internal entry for the priority heap.
24 Ordering: (effective_priority, sequence) ensures stable FIFO within
25 the same priority level.
27 Attributes:
28 effective_priority: Current priority (may be boosted by starvation
29 protection).
30 sequence: Monotonically increasing counter for FIFO ordering.
31 enqueue_time: Timestamp when the entry was added (for starvation
32 detection).
33 item: The actual payload (excluded from ordering).
34 cancelled: Whether this entry has been cancelled.
35 """
37 effective_priority: int
38 sequence: int
39 enqueue_time: float = field(compare=False)
40 item: Any = field(compare=False)
41 cancelled: bool = field(default=False, compare=False)
44class PriorityTaskQueue:
45 """Thread-safe priority queue with starvation protection.
47 Tasks are dequeued in priority order (lower value = higher priority).
48 Within the same priority, tasks follow FIFO order.
50 Starvation protection: tasks waiting longer than ``starvation_threshold``
51 seconds are automatically promoted to HIGH priority on the next ``get``.
53 Example:
54 >>> queue = PriorityTaskQueue(starvation_threshold=30.0)
55 >>> queue.put("critical_task", priority=Priority.HIGH)
56 >>> queue.put("normal_task", priority=Priority.NORMAL)
57 >>> queue.get() # returns "critical_task"
58 >>> queue.get() # returns "normal_task"
59 """
61 def __init__(self, starvation_threshold: float = 60.0) -> None:
62 """Initialize the priority queue.
64 Args:
65 starvation_threshold: Seconds after which a waiting task is
66 promoted to HIGH priority. Set to 0 to disable.
67 """
68 if starvation_threshold < 0:
69 raise ValueError("starvation_threshold must be >= 0")
71 self._heap: list[_PriorityEntry] = []
72 self._counter = itertools.count()
73 self._lock = threading.Lock()
74 self._starvation_threshold = starvation_threshold
76 def put(self, item: Any, priority: int = Priority.NORMAL) -> None:
77 """Add an item to the queue with the given priority.
79 Args:
80 item: The item to enqueue.
81 priority: Priority value (0-9, lower = higher priority).
83 Raises:
84 ValueError: If priority is not in range [0, 9].
85 """
86 if not 0 <= priority <= 9:
87 raise ValueError(f"priority must be in [0, 9], got {priority}")
89 entry = _PriorityEntry(
90 effective_priority=priority,
91 sequence=next(self._counter),
92 enqueue_time=time.monotonic(),
93 item=item,
94 )
95 with self._lock:
96 heapq.heappush(self._heap, entry)
98 def get(self) -> Optional[Any]:
99 """Remove and return the highest-priority item.
101 Applies starvation protection before selecting: any entry waiting
102 longer than the threshold is promoted to HIGH priority.
104 Returns:
105 The item, or None if the queue is empty.
106 """
107 with self._lock:
108 self._apply_starvation_protection()
109 while self._heap:
110 entry = heapq.heappop(self._heap)
111 if not entry.cancelled:
112 return entry.item
113 return None
115 def peek(self) -> Optional[Any]:
116 """Return the highest-priority item without removing it.
118 Returns:
119 The item, or None if the queue is empty.
120 """
121 with self._lock:
122 self._apply_starvation_protection()
123 for entry in self._heap:
124 if not entry.cancelled:
125 return entry.item
126 return None
128 @property
129 def queue_size(self) -> int:
130 """Return the number of non-cancelled items in the queue."""
131 with self._lock:
132 return sum(1 for entry in self._heap if not entry.cancelled)
134 @property
135 def is_empty(self) -> bool:
136 """Check if the queue has no pending items."""
137 return self.queue_size == 0
139 def clear(self) -> None:
140 """Remove all items from the queue."""
141 with self._lock:
142 self._heap.clear()
144 def snapshot(self) -> list[dict]:
145 """Return a snapshot of all pending entries for debugging.
147 Returns:
148 List of dicts with priority, sequence, wait_seconds, item info.
149 """
150 now = time.monotonic()
151 with self._lock:
152 return [
153 {
154 "priority": entry.effective_priority,
155 "sequence": entry.sequence,
156 "wait_seconds": round(now - entry.enqueue_time, 3),
157 "cancelled": entry.cancelled,
158 }
159 for entry in sorted(self._heap)
160 ]
162 def _apply_starvation_protection(self) -> None:
163 """Promote starved entries to HIGH priority. Must hold lock."""
164 if self._starvation_threshold <= 0:
165 return
167 now = time.monotonic()
168 promoted = False
170 for entry in self._heap:
171 if entry.cancelled:
172 continue
173 wait_time = now - entry.enqueue_time
174 if (
175 wait_time >= self._starvation_threshold
176 and entry.effective_priority > Priority.HIGH
177 ):
178 entry.effective_priority = Priority.HIGH
179 promoted = True
181 if promoted:
182 heapq.heapify(self._heap)