feat(vision-manager): memory-pressure yield gate (+ retain analysis gate) #8

Merged
fabi merged 2 commits from feat/vision-mem-yield into main 2026-06-16 14:34:25 +00:00
2 changed files with 36 additions and 1 deletions
Showing only changes of commit 27f215066d - Show all commits

View File

@@ -64,6 +64,20 @@ crawl_running() {
AND state = 'running';" 2>/dev/null || echo "ERR"
}
# Is the analysis subsystem enabled in Admin → Settings? The runtime source of
# truth is the `app_settings` row (key='analysis'), NOT the .env seed. When
# analysis is OFF the worker drains nothing, so leftover pending jobs would
# otherwise keep vision pinned forever — we must gate on this too, not just the
# queue depth. Returns the literal 'true'/'false'; 'ERR' (or anything else) on
# query failure is treated as enabled by the caller (fail-open: never yank
# vision out from under a live analysis on a transient DB hiccup).
analysis_enabled() {
"${PSQL[@]}" -c \
"SELECT CASE WHEN COALESCE((value->>'enabled')::boolean, false)
THEN 'true' ELSE 'false' END
FROM app_settings WHERE key = 'analysis';" 2>/dev/null || echo "ERR"
}
# ---- Memory pressure -------------------------------------------------------
# Host-wide used% from MemAvailable (NOT MemFree, which sits near "full" thanks
# to page cache and would false-trigger constantly). /proc/meminfo in this
@@ -151,6 +165,7 @@ log "started: container=$VISION_CONTAINER health=$VISION_HEALTH_URL poll=${POLL_
idle_for=0 # seconds the queue has been empty while vision is running
up_for=0 # seconds vision has been running (for MAX_UPTIME backstop)
crawl_deferred=0 # 1 while a start is held off by the crawl mutex (log on flip only)
analysis_off=0 # 1 while analysis is disabled in settings (log on flip only)
mem_yield_until=0 # epoch deadline of the active memory-yield cooldown (0 = none)
mem_inhibited=0 # 1 while starts are inhibited by memory pressure (log on flip only)
@@ -169,6 +184,22 @@ while true; do
;;
esac
# Gate on the runtime enable flag. If analysis is OFF, force the backlog to 0
# so the normal idle path runs the STOP_DEBOUNCE timer and shuts vision down.
# We deliberately reuse the idle debounce (rather than stopping instantly) so
# a quick toggle off→on inside the debounce window keeps vision warm — no
# cold reload for a momentary flip. Fail-open: only a literal 'false' counts
# as disabled; 'ERR'/empty leaves the real queue depth in effect.
if [ "$(analysis_enabled)" = "false" ]; then
if [ "$analysis_off" != "1" ]; then
log "analysis disabled in settings — ignoring ${pending} queued job(s); idle timer will stop vision"
analysis_off=1
fi
pending=0
else
analysis_off=0
fi
# Memory-yield gates (before the backlog logic). The active stop handles a
# running vision the idle path can't; the inhibit forces pending=0 so the
# existing idle-debounce path keeps it down (same shape as the crawl mutex).

View File

@@ -26,7 +26,11 @@ SELECT 'CREATE ROLE vision_manager LOGIN'
-- (Re)set the password every run so rotating it is just a re-apply.
ALTER ROLE vision_manager LOGIN PASSWORD :'pw';
-- CONNECT to the current database, and read-only on the one table we poll.
-- CONNECT to the current database, and read-only on the two tables we poll:
-- crawler_jobs (backlog depth + crawl-in-flight) and app_settings (the runtime
-- analysis-enabled flag — without this the manager can't tell that analysis was
-- turned off and would keep vision pinned on stale queued jobs).
GRANT CONNECT ON DATABASE :"DBNAME" TO vision_manager;
GRANT USAGE ON SCHEMA public TO vision_manager;
GRANT SELECT ON crawler_jobs TO vision_manager;
GRANT SELECT ON app_settings TO vision_manager;