Skip to content

Commit b3122bc

Browse files
authored
/health learns from GUI session estimates (#44)
1 parent 9254f46 commit b3122bc

3 files changed

Lines changed: 74 additions & 3 deletions

File tree

entrypoint.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
get_max_number_of_gui_sessions,
3636
can_admit_new_session,
3737
get_hard_cap_sessions,
38+
get_health_estimate_pct,
3839
get_container_total_memory,
3940
get_container_used_memory,
4041
get_ecs_task_id,
@@ -124,10 +125,14 @@ def get(self):
124125

125126
# "Operationally full" = at least one session AND can't fit another
126127
# without crossing the safe RAM ceiling or hitting the hard count cap.
127-
# This unifies the old (count-based) and RAM-based busy paths into one
128-
# predicate that's accurate for both light and heavy sessions.
128+
# We pass `get_health_estimate_pct()` so the predicate knows how large
129+
# incoming sessions tend to be on this task — without it, /health
130+
# would use the static fallback (35%) and miss the case where the
131+
# GUI is rejecting heavy sessions that /health thinks would fit.
129132
full = num_sessions > 0 and not can_admit_new_session(
130-
current_count=num_sessions, used_pct=mem_percent
133+
current_count=num_sessions,
134+
used_pct=mem_percent,
135+
estimate_pct=get_health_estimate_pct(),
131136
)
132137

133138
if full:

src/aind_ephys_portal/panel/ephys_gui.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
get_ecs_task_id,
4848
get_container_total_memory,
4949
get_container_used_memory,
50+
record_session_estimate,
5051
remove_session,
5152
)
5253
from aind_ephys_portal.panel.utils import PostMessageListener, FullscreenResizeHandler
@@ -227,6 +228,11 @@ def __init__(
227228
f"{est_bytes / (1024**3):.2f} GB ({estimate_pct:.1f}%) "
228229
f"for {num_units} units, fast_mode={self.fast_mode}"
229230
)
231+
# Teach /health what the largest recent session looked like
232+
# — its admission predicate uses this so it can fire the
233+
# inflate signal when a *heavy* session arrives instead of
234+
# relying on the static fallback percent.
235+
record_session_estimate(estimate_pct)
230236
except Exception as e:
231237
# Pre-load failed (S3 transient, bad path, etc.) — fall back
232238
# to the static estimate. Better to occasionally reject a

src/aind_ephys_portal/session_logging.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
22
import os
33
import sys
4+
import time as _time
45
import contextvars
56
from pathlib import Path
67
import psutil
@@ -227,6 +228,65 @@ def can_admit_new_session(current_count=None, used_pct=None, estimate_pct=None):
227228
return projected < get_safe_max_ram_pct()
228229

229230

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+
230290
def get_max_number_of_gui_sessions():
231291
"""Backward-compatible alias for :func:`get_hard_cap_sessions`.
232292

0 commit comments

Comments
 (0)