requestflow 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ from enum import IntEnum
2
+ from typing import Union
3
+
4
+
5
+ class Priority(IntEnum):
6
+ CRITICAL = 0
7
+ HIGH = 1
8
+ NORMAL = 2
9
+ LOW = 3
10
+
11
+ @classmethod
12
+ def from_value(cls, value: Union['Priority', str, int]) -> 'Priority':
13
+ if isinstance(value, Priority):
14
+ return value
15
+ if isinstance(value, int):
16
+ try:
17
+ return cls(value)
18
+ except ValueError:
19
+ raise ValueError(
20
+ f'invalid priority integer: {value}. must be 0, 1, 2, or 3.')
21
+ if isinstance(value, str):
22
+ normalized = value.strip().upper()
23
+ try:
24
+ return cls[normalized]
25
+ except KeyError:
26
+ raise ValueError(
27
+ f"unknown priority name: '{value}'. valid options: {[p.name.lower() for p in cls]}")
28
+ raise TypeError(
29
+ f'priority must be priority, str, or int, not {type(value).__name__}')
30
+
31
+ def __lt__(self, other: object) -> bool:
32
+ if isinstance(other, Priority):
33
+ return self.value < other.value
34
+ return NotImplemented
35
+
36
+ def __le__(self, other: object) -> bool:
37
+ if isinstance(other, Priority):
38
+ return self.value <= other.value
39
+ return NotImplemented
40
+
41
+ def __gt__(self, other: object) -> bool:
42
+ if isinstance(other, Priority):
43
+ return self.value > other.value
44
+ return NotImplemented
45
+
46
+ def __ge__(self, other: object) -> bool:
47
+ if isinstance(other, Priority):
48
+ return self.value >= other.value
49
+ return NotImplemented
requestflow/py.typed ADDED
@@ -0,0 +1 @@
1
+
requestflow/queue.py ADDED
@@ -0,0 +1,236 @@
1
+ import asyncio
2
+ import collections
3
+ import time
4
+ from typing import Callable, Deque, Dict, List, Optional, Set
5
+ from requestflow.exceptions import QueueFullError
6
+ from requestflow.models import RequestItem
7
+ from requestflow.policies import OverloadPolicy
8
+ from requestflow.priorities import Priority
9
+
10
+
11
+ class RequestQueue:
12
+ def __init__(self, max_pending: int = 5000, overload_policy: OverloadPolicy = OverloadPolicy.REJECT, aging_threshold_seconds: float = 2.0, guild_quantum: int = 3) -> None:
13
+ if max_pending <= 0:
14
+ raise ValueError(f'max_pending must be > 0, got {max_pending}')
15
+ self._max_pending: int = max_pending
16
+ self._overload_policy: OverloadPolicy = overload_policy
17
+ self._aging_threshold: float = aging_threshold_seconds
18
+ self._guild_quantum: int = guild_quantum
19
+ self._size: int = 0
20
+ self._sequence_counter: int = 0
21
+ self._high_water_mark: int = 0
22
+ self._queues: Dict[Priority, Dict[Optional[int], Deque[RequestItem]]] = {
23
+ p: collections.defaultdict(collections.deque) for p in Priority}
24
+ self._active_guild_keys: Dict[Priority, List[Optional[int]]] = {
25
+ p: [] for p in Priority}
26
+ self._guild_indices: Dict[Priority, int] = {p: 0 for p in Priority}
27
+ self._guild_deficits: Dict[Optional[int],
28
+ int] = collections.defaultdict(int)
29
+ self._not_empty: asyncio.Event = asyncio.Event()
30
+ self._not_full: asyncio.Event = asyncio.Event()
31
+ self._not_full.set()
32
+ self._lock: asyncio.Lock = asyncio.Lock()
33
+
34
+ @property
35
+ def size(self) -> int:
36
+ return self._size
37
+
38
+ @property
39
+ def max_pending(self) -> int:
40
+ return self._max_pending
41
+
42
+ @property
43
+ def high_water_mark(self) -> int:
44
+ return self._high_water_mark
45
+
46
+ @property
47
+ def overload_policy(self) -> OverloadPolicy:
48
+ return self._overload_policy
49
+
50
+ def is_empty(self) -> bool:
51
+ return self._size == 0
52
+
53
+ def is_full(self) -> bool:
54
+ return self._size >= self._max_pending
55
+
56
+ def get_priority_counts(self) -> Dict[str, int]:
57
+ counts = {}
58
+ for p in Priority:
59
+ total = sum((len(q) for q in self._queues[p].values()))
60
+ counts[p.name.lower()] = total
61
+ return counts
62
+
63
+ def get_active_guild_count(self) -> int:
64
+ seen: Set[Optional[int]] = set()
65
+ for p in Priority:
66
+ for guild_id, q in self._queues[p].items():
67
+ if len(q) > 0:
68
+ seen.add(guild_id)
69
+ return len(seen)
70
+
71
+ async def put(self, item: RequestItem, timeout: Optional[float] = None) -> None:
72
+ async with self._lock:
73
+ if self._size >= self._max_pending:
74
+ if self._overload_policy == OverloadPolicy.REJECT:
75
+ raise QueueFullError(
76
+ f'queue is full ({self._size}/{self._max_pending}). workload rejected.')
77
+ elif self._overload_policy == OverloadPolicy.DROP_OLDEST:
78
+ self._drop_oldest_locked()
79
+ elif self._overload_policy == OverloadPolicy.WAIT:
80
+ self._not_full.clear()
81
+ if self._overload_policy == OverloadPolicy.WAIT and self._size >= self._max_pending:
82
+ start_wait = time.monotonic()
83
+ while True:
84
+ remaining_timeout = None
85
+ if timeout is not None:
86
+ elapsed = time.monotonic() - start_wait
87
+ remaining_timeout = timeout - elapsed
88
+ if remaining_timeout <= 0:
89
+ raise QueueFullError(
90
+ 'timed out waiting for queue capacity.')
91
+ try:
92
+ await asyncio.wait_for(self._not_full.wait(), timeout=remaining_timeout)
93
+ except asyncio.TimeoutError:
94
+ raise QueueFullError(
95
+ 'timed out waiting for queue capacity.')
96
+ async with self._lock:
97
+ if self._size < self._max_pending:
98
+ break
99
+ async with self._lock:
100
+ self._enqueue_item_locked(item)
101
+
102
+ def _enqueue_item_locked(self, item: RequestItem) -> None:
103
+ self._sequence_counter += 1
104
+ item.sequence = self._sequence_counter
105
+ p = item.priority
106
+ g = item.guild_id
107
+ guild_dict = self._queues[p]
108
+ if g not in guild_dict or len(guild_dict[g]) == 0:
109
+ if g not in self._active_guild_keys[p]:
110
+ self._active_guild_keys[p].append(g)
111
+ guild_dict[g].append(item)
112
+ self._size += 1
113
+ if self._size > self._high_water_mark:
114
+ self._high_water_mark = self._size
115
+ self._not_empty.set()
116
+ if self._size >= self._max_pending:
117
+ self._not_full.clear()
118
+
119
+ def _drop_oldest_locked(self) -> None:
120
+ for p in reversed(Priority):
121
+ guild_dict = self._queues[p]
122
+ oldest_item: Optional[RequestItem] = None
123
+ target_guild: Optional[int] = None
124
+ for g, q in guild_dict.items():
125
+ if q:
126
+ head = q[0]
127
+ if oldest_item is None or head.sequence < oldest_item.sequence:
128
+ oldest_item = head
129
+ target_guild = g
130
+ if oldest_item is not None and target_guild is not None:
131
+ dropped = guild_dict[target_guild].popleft()
132
+ if len(guild_dict[target_guild]) == 0:
133
+ if target_guild in self._active_guild_keys[p]:
134
+ self._active_guild_keys[p].remove(target_guild)
135
+ self._size -= 1
136
+ dropped.cancelled = True
137
+ if dropped.future and (not dropped.future.done()):
138
+ dropped.future.set_exception(QueueFullError(
139
+ 'dropped due to queue overload under drop_oldest policy'))
140
+ return
141
+
142
+ def remove_item(self, item: RequestItem) -> bool:
143
+ p = item.effective_priority
144
+ g = item.guild_id
145
+ guild_dict = self._queues[p]
146
+ q = guild_dict.get(g)
147
+ if q:
148
+ try:
149
+ q.remove(item)
150
+ self._size -= 1
151
+ if len(q) == 0 and g in self._active_guild_keys[p]:
152
+ self._active_guild_keys[p].remove(g)
153
+ if self._size < self._max_pending:
154
+ self._not_full.set()
155
+ if self._size == 0:
156
+ self._not_empty.clear()
157
+ return True
158
+ except ValueError:
159
+ pass
160
+ return False
161
+
162
+ async def get(self, is_route_limited: Optional[Callable[[Optional[str]], bool]] = None) -> RequestItem:
163
+ while True:
164
+ await self._not_empty.wait()
165
+ async with self._lock:
166
+ item = self._extract_next_item_locked(is_route_limited)
167
+ if item is not None:
168
+ self._size -= 1
169
+ if self._size < self._max_pending:
170
+ self._not_full.set()
171
+ if self._size == 0:
172
+ self._not_empty.clear()
173
+ return item
174
+ self._not_empty.clear()
175
+ await asyncio.sleep(0.01)
176
+
177
+ def _extract_next_item_locked(self, is_route_limited: Optional[Callable[[Optional[str]], bool]] = None) -> Optional[RequestItem]:
178
+ now = time.monotonic()
179
+ self._apply_aging_locked(now)
180
+ for p in Priority:
181
+ active_guilds = self._active_guild_keys[p]
182
+ if not active_guilds:
183
+ continue
184
+ num_guilds = len(active_guilds)
185
+ start_index = self._guild_indices[p] % num_guilds
186
+ for i in range(num_guilds):
187
+ idx = (start_index + i) % num_guilds
188
+ g = active_guilds[idx]
189
+ q = self._queues[p][g]
190
+ if not q:
191
+ continue
192
+ head = q[0]
193
+ if head.cancelled or (head.future and head.future.cancelled()):
194
+ q.popleft()
195
+ self._size -= 1
196
+ continue
197
+ if is_route_limited and is_route_limited(head.route_key):
198
+ continue
199
+ if self._guild_deficits[g] <= 0:
200
+ self._guild_deficits[g] += self._guild_quantum
201
+ self._guild_deficits[g] -= 1
202
+ item = q.popleft()
203
+ if len(q) == 0:
204
+ self._guild_deficits[g] = 0
205
+ active_guilds.remove(g)
206
+ if active_guilds:
207
+ self._guild_indices[p] = idx % len(active_guilds)
208
+ else:
209
+ self._guild_indices[p] = 0
210
+ elif self._guild_deficits[g] <= 0:
211
+ self._guild_deficits[g] = 0
212
+ self._guild_indices[p] = (idx + 1) % num_guilds
213
+ else:
214
+ self._guild_indices[p] = idx
215
+ return item
216
+ return None
217
+
218
+ def _apply_aging_locked(self, now: float) -> None:
219
+ if self._aging_threshold <= 0:
220
+ return
221
+ for p in [Priority.LOW, Priority.NORMAL, Priority.HIGH]:
222
+ guild_dict = self._queues[p]
223
+ higher_priority = Priority(p.value - 1)
224
+ for g, q in list(guild_dict.items()):
225
+ stale_items = []
226
+ for item in list(q):
227
+ if now - item.enqueue_time >= self._aging_threshold:
228
+ stale_items.append(item)
229
+ for item in stale_items:
230
+ q.remove(item)
231
+ item.effective_priority = higher_priority
232
+ self._queues[higher_priority][g].append(item)
233
+ if g not in self._active_guild_keys[higher_priority]:
234
+ self._active_guild_keys[higher_priority].append(g)
235
+ if len(q) == 0 and g in self._active_guild_keys[p]:
236
+ self._active_guild_keys[p].remove(g)
@@ -0,0 +1,188 @@
1
+ import asyncio
2
+ import time
3
+ from typing import Dict, Optional, Tuple
4
+
5
+
6
+ class GlobalRateLimiter:
7
+ def __init__(self, rate: float = 40.0, ceiling: float = 50.0, burst_allowance: float = 5.0, allow_custom_ceiling: bool = False) -> None:
8
+ if rate <= 0:
9
+ raise ValueError(f'global rate limit must be positive, got {rate}')
10
+ if ceiling <= 0:
11
+ raise ValueError(
12
+ f'rate limit ceiling must be positive, got {ceiling}')
13
+ if not allow_custom_ceiling and rate > ceiling:
14
+ raise ValueError(
15
+ f'configured rate ({rate}) exceeds standard discord limit ceiling ({ceiling}). discord standard limit is 50 req/s. if you have an officially approved higher limit, set allow_custom_ceiling=true.')
16
+ self._rate: float = float(rate)
17
+ self._ceiling: float = float(ceiling)
18
+ self._burst_tokens: float = float(burst_allowance)
19
+ self._allow_custom_ceiling: bool = allow_custom_ceiling
20
+ self._emission_interval: float = 1.0 / self._rate
21
+ self._tau: float = self._burst_tokens * self._emission_interval
22
+ self._tat: float = 0.0
23
+ self._lock: asyncio.Lock = asyncio.Lock()
24
+ self._global_cooldown_until: float = 0.0
25
+ self._global_cooldown_event: asyncio.Event = asyncio.Event()
26
+ self._global_cooldown_event.set()
27
+ self._total_acquisitions: int = 0
28
+ self._total_wait_time: float = 0.0
29
+ self._cooldown_429_count: int = 0
30
+
31
+ @property
32
+ def rate(self) -> float:
33
+ return self._rate
34
+
35
+ @property
36
+ def ceiling(self) -> float:
37
+ return self._ceiling
38
+
39
+ @property
40
+ def is_cooling_down(self) -> bool:
41
+ return time.monotonic() < self._global_cooldown_until
42
+
43
+ @property
44
+ def cooldown_remaining(self) -> float:
45
+ now = time.monotonic()
46
+ return max(0.0, self._global_cooldown_until - now)
47
+
48
+ async def update_rate(self, new_rate: float) -> None:
49
+ if new_rate <= 0:
50
+ raise ValueError(
51
+ f'global rate limit must be positive, got {new_rate}')
52
+ if not self._allow_custom_ceiling and new_rate > self._ceiling:
53
+ raise ValueError(
54
+ f'new rate ({new_rate}) exceeds ceiling ({self._ceiling}).')
55
+ async with self._lock:
56
+ self._rate = float(new_rate)
57
+ self._emission_interval = 1.0 / self._rate
58
+ self._tau = self._burst_tokens * self._emission_interval
59
+ self._tat = max(self._tat, time.monotonic())
60
+
61
+ async def acquire(self, *, is_interaction: bool = False) -> float:
62
+ if is_interaction:
63
+ return 0.0
64
+ wait_seconds: float = 0.0
65
+ while True:
66
+ if not self._global_cooldown_event.is_set():
67
+ await self._global_cooldown_event.wait()
68
+ now = time.monotonic()
69
+ if now < self._global_cooldown_until:
70
+ sleep_duration = self._global_cooldown_until - now
71
+ if sleep_duration > 0:
72
+ await asyncio.sleep(sleep_duration)
73
+ continue
74
+ async with self._lock:
75
+ now = time.monotonic()
76
+ if now > self._tat:
77
+ self._tat = now
78
+ dispatch_time = self._tat
79
+ self._tat = dispatch_time + self._emission_interval
80
+ delay = max(0.0, dispatch_time - now)
81
+ if delay > 0:
82
+ try:
83
+ await asyncio.sleep(delay)
84
+ wait_seconds += delay
85
+ except asyncio.CancelledError:
86
+ async with self._lock:
87
+ now_cancel = time.monotonic()
88
+ if self._tat > now_cancel:
89
+ self._tat = max(
90
+ now_cancel, self._tat - self._emission_interval)
91
+ raise
92
+ self._total_acquisitions += 1
93
+ self._total_wait_time += wait_seconds
94
+ return wait_seconds
95
+
96
+ def handle_global_429(self, retry_after: float) -> None:
97
+ now = time.monotonic()
98
+ cooldown_end = now + max(0.05, retry_after)
99
+ if cooldown_end > self._global_cooldown_until:
100
+ self._global_cooldown_until = cooldown_end
101
+ self._cooldown_429_count += 1
102
+ self._global_cooldown_event.clear()
103
+ loop = asyncio.get_event_loop()
104
+ loop.call_later(max(0.05, retry_after),
105
+ self._clear_cooldown, cooldown_end)
106
+
107
+ def _clear_cooldown(self, target_end: float) -> None:
108
+ if time.monotonic() >= self._global_cooldown_until or target_end == self._global_cooldown_until:
109
+ self._global_cooldown_event.set()
110
+
111
+
112
+ class RouteBucketCoordinator:
113
+ def __init__(self) -> None:
114
+ self._cooldowns: Dict[str, float] = {}
115
+ self._remaining: Dict[str, int] = {}
116
+ self._lock: asyncio.Lock = asyncio.Lock()
117
+ self._route_429_count: int = 0
118
+
119
+ def is_route_cooling_down(self, route_key: Optional[str]) -> bool:
120
+ if not route_key:
121
+ return False
122
+ expiry = self._cooldowns.get(route_key)
123
+ if expiry is None:
124
+ return False
125
+ if time.monotonic() >= expiry:
126
+ self._cooldowns.pop(route_key, None)
127
+ return False
128
+ return True
129
+
130
+ def get_route_delay(self, route_key: Optional[str]) -> float:
131
+ if not route_key:
132
+ return 0.0
133
+ expiry = self._cooldowns.get(route_key)
134
+ if expiry is None:
135
+ return 0.0
136
+ remaining = expiry - time.monotonic()
137
+ if remaining <= 0:
138
+ self._cooldowns.pop(route_key, None)
139
+ return 0.0
140
+ return remaining
141
+
142
+ def update_route_cooldown(self, route_key: str, retry_after: float) -> None:
143
+ if not route_key or retry_after <= 0:
144
+ return
145
+ cooldown_until = time.monotonic() + retry_after
146
+ existing = self._cooldowns.get(route_key, 0.0)
147
+ if cooldown_until > existing:
148
+ self._cooldowns[route_key] = cooldown_until
149
+ self._route_429_count += 1
150
+
151
+ def update_from_headers(self, route_key: str, headers: Dict[str, str]) -> None:
152
+ if not route_key:
153
+ return
154
+ remaining = headers.get('X-RateLimit-Remaining')
155
+ reset_after = headers.get('X-RateLimit-Reset-After')
156
+ if remaining is not None:
157
+ try:
158
+ rem_val = int(remaining)
159
+ self._remaining[route_key] = rem_val
160
+ if rem_val == 0 and reset_after is not None:
161
+ try:
162
+ reset_val = float(reset_after)
163
+ self.update_route_cooldown(route_key, reset_val)
164
+ except ValueError:
165
+ pass
166
+ except ValueError:
167
+ pass
168
+
169
+
170
+ class RateLimitCoordinator:
171
+ def __init__(self, global_rate_limit: float = 40.0, rate_limit_ceiling: float = 50.0, burst_allowance: float = 5.0, allow_custom_ceiling: bool = False) -> None:
172
+ self.global_limiter = GlobalRateLimiter(
173
+ rate=global_rate_limit, ceiling=rate_limit_ceiling, burst_allowance=burst_allowance, allow_custom_ceiling=allow_custom_ceiling)
174
+ self.route_coordinator = RouteBucketCoordinator()
175
+
176
+ async def acquire(self, route_key: Optional[str] = None, *, is_interaction: bool = False) -> float:
177
+ if route_key and self.route_coordinator.is_route_cooling_down(route_key):
178
+ delay = self.route_coordinator.get_route_delay(route_key)
179
+ if delay > 0:
180
+ await asyncio.sleep(delay)
181
+ return await self.global_limiter.acquire(is_interaction=is_interaction)
182
+
183
+ def handle_429(self, retry_after: float, *, is_global: bool = False, route_key: Optional[str] = None) -> None:
184
+ if is_global:
185
+ self.global_limiter.handle_global_429(retry_after)
186
+ elif route_key:
187
+ self.route_coordinator.update_route_cooldown(
188
+ route_key, retry_after)
@@ -0,0 +1,131 @@
1
+ import asyncio
2
+ import time
3
+ from typing import Any, Callable, Coroutine, List, Optional, Set, TypeVar
4
+ from requestflow.exceptions import OperationCancelledError, QueueFullError, SchedulerClosedError, SchedulerNotStartedError
5
+ from requestflow.executor import RequestExecutor
6
+ from requestflow.metrics import RequestMetrics
7
+ from requestflow.models import RequestItem
8
+ from requestflow.policies import OverloadPolicy, RetryPolicy
9
+ from requestflow.priorities import Priority
10
+ from requestflow.queue import RequestQueue
11
+ from requestflow.ratelimit import RateLimitCoordinator
12
+ T = TypeVar('t')
13
+
14
+
15
+ class RequestScheduler:
16
+ def __init__(self, max_pending: int = 5000, max_concurrency: int = 16, global_rate_limit: float = 40.0, rate_limit_ceiling: float = 50.0, burst_allowance: float = 5.0, allow_custom_ceiling: bool = False, overload_policy: OverloadPolicy = OverloadPolicy.REJECT, aging_threshold_seconds: float = 2.0, guild_quantum: int = 3, default_timeout: Optional[float] = None, default_retry_policy: Optional[RetryPolicy] = None) -> None:
17
+ self.queue = RequestQueue(max_pending=max_pending, overload_policy=overload_policy,
18
+ aging_threshold_seconds=aging_threshold_seconds, guild_quantum=guild_quantum)
19
+ self.executor = RequestExecutor(
20
+ max_concurrency=max_concurrency, default_timeout=default_timeout, default_retry_policy=default_retry_policy)
21
+ self.rate_coordinator = RateLimitCoordinator(global_rate_limit=global_rate_limit, rate_limit_ceiling=rate_limit_ceiling,
22
+ burst_allowance=burst_allowance, allow_custom_ceiling=allow_custom_ceiling)
23
+ self.metrics = RequestMetrics()
24
+ self.metrics.register_providers(get_currently_queued=lambda: self.queue.size, get_currently_executing=lambda: self.executor.currently_executing,
25
+ get_high_water_mark=lambda: self.queue.high_water_mark, get_per_priority=self.queue.get_priority_counts, get_active_guilds=self.queue.get_active_guild_count)
26
+ self._is_started: bool = False
27
+ self._is_closed: bool = False
28
+ self._accepting_requests: bool = False
29
+ self._dispatch_tasks: List[asyncio.Task] = []
30
+ self._in_flight_tasks: Set[asyncio.Task] = set()
31
+ self._drain_event: asyncio.Event = asyncio.Event()
32
+
33
+ @property
34
+ def is_started(self) -> bool:
35
+ return self._is_started
36
+
37
+ @property
38
+ def is_closed(self) -> bool:
39
+ return self._is_closed
40
+
41
+ async def start(self) -> None:
42
+ if self._is_closed:
43
+ raise SchedulerClosedError('cannot start a closed scheduler.')
44
+ if self._is_started:
45
+ return
46
+ self._is_started = True
47
+ self._accepting_requests = True
48
+ self._dispatch_tasks = [asyncio.create_task(
49
+ self._dispatch_loop(), name='requestflow_dispatcher')]
50
+
51
+ async def close(self, drain: bool = True, timeout: float = 5.0) -> None:
52
+ if self._is_closed:
53
+ return
54
+ self._accepting_requests = False
55
+ if drain and self._is_started and (self.queue.size > 0 or len(self._in_flight_tasks) > 0):
56
+ start_drain = time.monotonic()
57
+ while time.monotonic() - start_drain < timeout:
58
+ if self.queue.size == 0 and len(self._in_flight_tasks) == 0:
59
+ break
60
+ await asyncio.sleep(0.01)
61
+ self._is_closed = True
62
+ for t in self._dispatch_tasks:
63
+ t.cancel()
64
+ for t in self._dispatch_tasks:
65
+ try:
66
+ await t
67
+ except (asyncio.CancelledError, Exception):
68
+ pass
69
+ self._dispatch_tasks.clear()
70
+ for t in list(self._in_flight_tasks):
71
+ t.cancel()
72
+ self._in_flight_tasks.clear()
73
+ while not self.queue.is_empty():
74
+ try:
75
+ item = await self.queue.get()
76
+ if item.future and (not item.future.done()):
77
+ item.future.set_exception(SchedulerClosedError(
78
+ 'scheduler closed before item could be executed.'))
79
+ except Exception:
80
+ break
81
+
82
+ async def submit(self, operation: Callable[..., Coroutine[Any, Any, T]], *args: Any, priority: Priority = Priority.NORMAL, guild_id: Optional[int] = None, route_key: Optional[str] = None, is_interaction: bool = False, dedup_key: Optional[str] = None, timeout: Optional[float] = None, enqueue_timeout: Optional[float] = None, **kwargs: Any) -> T:
83
+ if not self._is_started:
84
+ raise SchedulerNotStartedError(
85
+ "scheduler has not been started. call 'await flow.start()'.")
86
+ if not self._accepting_requests or self._is_closed:
87
+ raise SchedulerClosedError(
88
+ 'scheduler is closed. cannot accept new submissions.')
89
+ loop = asyncio.get_running_loop()
90
+ future = loop.create_future()
91
+ item = RequestItem(sequence=0, operation=operation, args=args, kwargs=kwargs, priority=priority, effective_priority=priority, guild_id=guild_id,
92
+ route_key=route_key, is_interaction=is_interaction, dedup_key=dedup_key, timeout=timeout, future=future, enqueue_time=time.monotonic())
93
+ self.metrics.record_submit()
94
+ try:
95
+ await self.queue.put(item, timeout=enqueue_timeout)
96
+ except QueueFullError:
97
+ self.metrics.record_reject()
98
+ raise
99
+ try:
100
+ return await future
101
+ except asyncio.CancelledError:
102
+ item.cancelled = True
103
+ self.metrics.record_cancel()
104
+ self.queue.remove_item(item)
105
+ raise
106
+
107
+ async def _dispatch_loop(self) -> None:
108
+ while self._is_started and (not self._is_closed):
109
+ try:
110
+ item = await self.queue.get(is_route_limited=self.rate_coordinator.route_coordinator.is_route_cooling_down)
111
+ if item.cancelled or (item.future and item.future.cancelled()):
112
+ continue
113
+ wait_time = await self.rate_coordinator.acquire(item.route_key, is_interaction=item.is_interaction)
114
+ self.metrics.record_rate_limit_wait(wait_time)
115
+ task = asyncio.create_task(self._execute_item_task(
116
+ item), name=f'requestflow_exec_{item.sequence}')
117
+ self._in_flight_tasks.add(task)
118
+ task.add_done_callback(self._in_flight_tasks.discard)
119
+ except asyncio.CancelledError:
120
+ break
121
+ except Exception as e:
122
+ await asyncio.sleep(0.01)
123
+
124
+ async def _execute_item_task(self, item: RequestItem) -> None:
125
+ def on_complete(it: RequestItem, err: Optional[BaseException]) -> None:
126
+ self.metrics.record_completion(
127
+ queue_wait_seconds=it.queue_wait_time, exec_duration_seconds=it.execution_duration, error=err)
128
+ try:
129
+ await self.executor.execute(item, on_complete=on_complete)
130
+ except asyncio.CancelledError:
131
+ pass