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,
}