|
1 | 1 | import io |
2 | 2 | import os |
3 | 3 | import sys |
| 4 | +import time as _time |
4 | 5 | import contextvars |
5 | 6 | from pathlib import Path |
6 | 7 | import psutil |
@@ -227,6 +228,65 @@ def can_admit_new_session(current_count=None, used_pct=None, estimate_pct=None): |
227 | 228 | return projected < get_safe_max_ram_pct() |
228 | 229 |
|
229 | 230 |
|
| 231 | +# --- Recent-max-estimate tracking (used by /health to know how heavy the |
| 232 | +# next session is likely to be) --- |
| 233 | +# |
| 234 | +# Problem: ``EphysGuiView.__init__`` computes a per-dataset RAM estimate |
| 235 | +# (via the analyzer's unit_ids) and passes it to ``can_admit_new_session``. |
| 236 | +# But ``/health`` doesn't know the next session's size, so without help it |
| 237 | +# falls back to the static ``PER_SESSION_ESTIMATE_PCT`` (35%) — which can |
| 238 | +# be much smaller than reality on heavy recordings. |
| 239 | +# |
| 240 | +# Result: the GUI rejects a heavy incoming session ("would exceed safe |
| 241 | +# ceiling"), but ``/health`` says the task is healthy, so ECS doesn't |
| 242 | +# know to scale up. |
| 243 | +# |
| 244 | +# Fix: every time the GUI computes an estimate, record it here. ``/health`` |
| 245 | +# reads the max-of-(static, recent-max) so its admission check tightens |
| 246 | +# as soon as a heavy session is observed. The recorded value decays |
| 247 | +# automatically after a few minutes so a one-off heavy session doesn't |
| 248 | +# permanently bias the health signal. |
| 249 | +_RECENT_MAX_ESTIMATE_PCT = 0.0 |
| 250 | +_RECENT_MAX_ESTIMATE_TS = 0.0 |
| 251 | +_RECENT_MAX_ESTIMATE_TTL = 300.0 # 5 minutes |
| 252 | + |
| 253 | + |
| 254 | +def record_session_estimate(estimate_pct): |
| 255 | + """Record a per-session RAM estimate observed by the GUI. |
| 256 | +
|
| 257 | + The maximum seen in the last :data:`_RECENT_MAX_ESTIMATE_TTL` seconds |
| 258 | + is what :func:`get_health_estimate_pct` reports. Called from |
| 259 | + ``EphysGuiView.__init__`` for every admission attempt (admit or reject) |
| 260 | + so ``/health`` learns the realistic upper bound of incoming sessions. |
| 261 | + """ |
| 262 | + global _RECENT_MAX_ESTIMATE_PCT, _RECENT_MAX_ESTIMATE_TS |
| 263 | + if estimate_pct is None or estimate_pct <= 0: |
| 264 | + return |
| 265 | + now = _time.monotonic() |
| 266 | + # Decay: if the last observation was a long time ago, reset before |
| 267 | + # comparing — a stale max shouldn't keep biasing /health forever. |
| 268 | + if now - _RECENT_MAX_ESTIMATE_TS > _RECENT_MAX_ESTIMATE_TTL: |
| 269 | + _RECENT_MAX_ESTIMATE_PCT = 0.0 |
| 270 | + if estimate_pct > _RECENT_MAX_ESTIMATE_PCT: |
| 271 | + _RECENT_MAX_ESTIMATE_PCT = float(estimate_pct) |
| 272 | + _RECENT_MAX_ESTIMATE_TS = now |
| 273 | + |
| 274 | + |
| 275 | +def get_health_estimate_pct(): |
| 276 | + """Return the estimate ``/health`` should feed into ``can_admit_new_session``. |
| 277 | +
|
| 278 | + Takes ``max(static_fallback, recent_observed_max)``: in steady state |
| 279 | + with light sessions this is just the static fallback, but a recent |
| 280 | + heavy session pushes it up so ``/health`` correctly reports the task |
| 281 | + as full / triggers the inflate scale-up signal. |
| 282 | + """ |
| 283 | + static = get_per_session_estimate_pct() |
| 284 | + now = _time.monotonic() |
| 285 | + if now - _RECENT_MAX_ESTIMATE_TS > _RECENT_MAX_ESTIMATE_TTL: |
| 286 | + return static |
| 287 | + return max(static, _RECENT_MAX_ESTIMATE_PCT) |
| 288 | + |
| 289 | + |
230 | 290 | def get_max_number_of_gui_sessions(): |
231 | 291 | """Backward-compatible alias for :func:`get_hard_cap_sessions`. |
232 | 292 |
|
|
0 commit comments