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

1"""Thread-safe priority queue for scheduling tasks by priority level.""" 

2 

3import heapq 

4import itertools 

5import threading 

6import time 

7from dataclasses import dataclass, field 

8from enum import IntEnum 

9from typing import Any, Optional 

10 

11 

12class Priority(IntEnum): 

13 """Task priority levels. Lower value = higher priority.""" 

14 

15 HIGH = 0 

16 NORMAL = 5 

17 LOW = 9 

18 

19 

20@dataclass(order=True) 

21class _PriorityEntry: 

22 """Internal entry for the priority heap. 

23 

24 Ordering: (effective_priority, sequence) ensures stable FIFO within 

25 the same priority level. 

26 

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 """ 

36 

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) 

42 

43 

44class PriorityTaskQueue: 

45 """Thread-safe priority queue with starvation protection. 

46 

47 Tasks are dequeued in priority order (lower value = higher priority). 

48 Within the same priority, tasks follow FIFO order. 

49 

50 Starvation protection: tasks waiting longer than ``starvation_threshold`` 

51 seconds are automatically promoted to HIGH priority on the next ``get``. 

52 

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 """ 

60 

61 def __init__(self, starvation_threshold: float = 60.0) -> None: 

62 """Initialize the priority queue. 

63 

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") 

70 

71 self._heap: list[_PriorityEntry] = [] 

72 self._counter = itertools.count() 

73 self._lock = threading.Lock() 

74 self._starvation_threshold = starvation_threshold 

75 

76 def put(self, item: Any, priority: int = Priority.NORMAL) -> None: 

77 """Add an item to the queue with the given priority. 

78 

79 Args: 

80 item: The item to enqueue. 

81 priority: Priority value (0-9, lower = higher priority). 

82 

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}") 

88 

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) 

97 

98 def get(self) -> Optional[Any]: 

99 """Remove and return the highest-priority item. 

100 

101 Applies starvation protection before selecting: any entry waiting 

102 longer than the threshold is promoted to HIGH priority. 

103 

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 

114 

115 def peek(self) -> Optional[Any]: 

116 """Return the highest-priority item without removing it. 

117 

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 

127 

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) 

133 

134 @property 

135 def is_empty(self) -> bool: 

136 """Check if the queue has no pending items.""" 

137 return self.queue_size == 0 

138 

139 def clear(self) -> None: 

140 """Remove all items from the queue.""" 

141 with self._lock: 

142 self._heap.clear() 

143 

144 def snapshot(self) -> list[dict]: 

145 """Return a snapshot of all pending entries for debugging. 

146 

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 ] 

161 

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 

166 

167 now = time.monotonic() 

168 promoted = False 

169 

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 

180 

181 if promoted: 

182 heapq.heapify(self._heap)