Spaces:
Running
Running
File size: 5,847 Bytes
28a08e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | """
backend/api/priority.py β Priority job semaphores (S-DUAL-1)
Definisce classi di job con concorrenza controllata via asyncio.Semaphore:
HIGH β agent steps interattivi, exec code da UI, terminal commands
Semaphore(6): latency-sensitive, risposta attesa < 30s
NORMAL β task di media prioritΓ , elaborazioni non critiche
Semaphore(4): bilanciamento tra latenza e throughput
LOW β task a bassa prioritΓ , come pre-calcoli o aggiornamenti in background
Semaphore(2): puΓ² attendere, non blocca job piΓΉ importanti
BACKGROUND β benchmark, research multi-URL, pip-install headless
Semaphore(2): best-effort, puΓ² attendere, non blocca mai HIGH/NORMAL/LOW
I contatori live sono esposti da /api/health/load per il routing adattivo CF.
"""
import asyncio, time, logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator
_logger = logging.getLogger("api.priority")
_boot_time = time.monotonic()
# ββ Semaphores βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_HIGH_LIMIT = 6
_NORMAL_LIMIT = 4
_LOW_LIMIT = 2
_BACKGROUND_LIMIT = 2
_high_sem = asyncio.Semaphore(_HIGH_LIMIT)
_normal_sem = asyncio.Semaphore(_NORMAL_LIMIT)
_low_sem = asyncio.Semaphore(_LOW_LIMIT)
_background_sem = asyncio.Semaphore(_BACKGROUND_LIMIT)
# Contatori atomici per metriche /api/health/load
_high_active = 0
_normal_active = 0
_low_active = 0
_background_active = 0
@asynccontextmanager
async def high_priority_job(timeout_s: float = 300.0) -> AsyncGenerator[None, None]:
"""
Context manager per job HIGH (agent steps, exec interattivo, terminal).
Acquisisce il semaphore con timeout β rilancia asyncio.TimeoutError
se non ci sono slot liberi entro timeout_s (default 300s = non dovrebbe mai
scadere per richieste UI normali, ma protegge da leak di semaphore).
Uso:
async with high_priority_job():
result = await run_subprocess(...)
"""
global _high_active
try:
await asyncio.wait_for(_high_sem.acquire(), timeout=timeout_s)
except asyncio.TimeoutError:
_logger.warning("[priority] HIGH priority semaphore timeout dopo %.0fs", timeout_s)
raise
_high_active += 1
try:
yield
finally:
_high_active = max(0, _high_active - 1)
_high_sem.release()
@asynccontextmanager
async def normal_priority_job(timeout_s: float = 120.0) -> AsyncGenerator[None, None]:
"""
Context manager per job NORMAL (task di media prioritΓ ).
Uso:
async with normal_priority_job():
result = await process_data(...)
"""
global _normal_active
try:
await asyncio.wait_for(_normal_sem.acquire(), timeout=timeout_s)
except asyncio.TimeoutError:
_logger.warning("[priority] NORMAL priority semaphore timeout dopo %.0fs", timeout_s)
raise
_normal_active += 1
try:
yield
finally:
_normal_active = max(0, _normal_active - 1)
_normal_sem.release()
@asynccontextmanager
async def low_priority_job(timeout_s: float = 60.0) -> AsyncGenerator[None, None]:
"""
Context manager per job LOW (task a bassa prioritΓ ).
Uso:
async with low_priority_job():
result = await update_cache(...)
"""
global _low_active
try:
await asyncio.wait_for(_low_sem.acquire(), timeout=timeout_s)
except asyncio.TimeoutError:
_logger.warning("[priority] LOW priority semaphore timeout dopo %.0fs", timeout_s)
raise
_low_active += 1
try:
yield
finally:
_low_active = max(0, _low_active - 1)
_low_sem.release()
@asynccontextmanager
async def background_job(timeout_s: float = 30.0) -> AsyncGenerator[None, None]:
"""
Context manager per job BACKGROUND (benchmark, research, pip-install).
Timeout piΓΉ aggressivo (default 30s): se entrambi gli slot BACKGROUND sono
occupati e non si liberano in 30s β 429 Too Many Requests al chiamante.
Garantisce che il benchmark non blocchi mai i job REALTIME.
Uso:
async with background_job(timeout_s=30.0):
result = await run_benchmark(...)
"""
global _background_active
try:
await asyncio.wait_for(_background_sem.acquire(), timeout=timeout_s)
except asyncio.TimeoutError:
_logger.warning("[priority] BACKGROUND semaphore timeout dopo %.0fs β job rifiutato", timeout_s)
raise
_background_active += 1
try:
yield
finally:
_background_active = max(0, _background_active - 1)
_background_sem.release()
def get_load_metrics() -> dict:
"""
Metriche live per /api/health/load.
"""
return {
"high_active": _high_active,
"high_capacity": _HIGH_LIMIT,
"high_available": _high_sem._value,
"normal_active": _normal_active,
"normal_capacity": _NORMAL_LIMIT,
"normal_available": _normal_sem._value,
"low_active": _low_active,
"low_capacity": _LOW_LIMIT,
"low_available": _low_sem._value,
"background_active": _background_active,
"background_capacity": _BACKGROUND_LIMIT,
"background_available": _background_sem._value,
"uptime_s": int(time.monotonic() - _boot_time),
}
# Mapping per la selezione del context manager in base alla stringa di prioritΓ
PRIORITY_CONTEXT_MANAGERS = {
"high": high_priority_job,
"normal": normal_priority_job,
"low": low_priority_job,
"background": background_job,
}
|