Skip to content

Commit d191e64

Browse files
committed
fix sandbox search mediator liveness
1 parent 3377c5f commit d191e64

7 files changed

Lines changed: 59 additions & 4 deletions

File tree

deploy/sandbox/compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ services:
172172
"CMD",
173173
"/opt/venv/bin/python",
174174
"-c",
175-
"from urllib.request import urlopen; import sys; sys.exit(0 if urlopen('http://127.0.0.1:8485/health', timeout=3).status == 200 else 1)",
175+
"from urllib.request import urlopen; import sys; sys.exit(0 if urlopen('http://127.0.0.1:8485/live', timeout=3).status == 200 else 1)",
176176
]
177177
interval: 15s
178178
timeout: 5s

services/search-mediator/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,5 @@ COPY --from=build /app /app
103103

104104
USER 65534:65534
105105
EXPOSE 8485
106-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=5 CMD ["/opt/venv/bin/python", "-c", "from urllib.request import urlopen; import os, sys; port=os.getenv('BIND_ADDR','127.0.0.1:8485').rsplit(':',1)[-1]; sys.exit(0 if urlopen(f'http://127.0.0.1:{port}/health', timeout=3).status == 200 else 1)"]
106+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=5 CMD ["/opt/venv/bin/python", "-c", "from urllib.request import urlopen; import os, sys; port=os.getenv('BIND_ADDR','127.0.0.1:8485').rsplit(':',1)[-1]; sys.exit(0 if urlopen(f'http://127.0.0.1:{port}/live', timeout=3).status == 200 else 1)"]
107107
ENTRYPOINT ["/opt/venv/bin/python", "/app/entrypoint.py"]

services/search-mediator/app.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,18 @@ def health():
688688
})
689689

690690

691+
@app.route("/live")
692+
def live():
693+
"""Fast liveness probe that does not depend on optional upstream services."""
694+
return jsonify({
695+
"status": "ok",
696+
"search_enabled": _is_search_enabled(),
697+
"session_mode": _get_session_mode(),
698+
"searxng_reachable": None,
699+
"tor_routed": True,
700+
})
701+
702+
691703
@app.route("/v1/search", methods=["POST"])
692704
def search():
693705
"""Perform a sanitized, Tor-routed web search."""

services/ui/ui/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2371,7 +2371,9 @@ def search_status():
23712371
try:
23722372
resp = requests.get(f"{SEARCH_MEDIATOR_URL}/health", timeout=5)
23732373
data = resp.json()
2374-
if data.get("search_enabled") is False and _is_sandbox_deployment():
2374+
available = data.get("search_enabled") is not False and data.get("searxng_reachable") is not False
2375+
data["search_available"] = available
2376+
if not available and _is_sandbox_deployment():
23752377
data.setdefault("message", "Web search is available after starting the sandbox with the search profile.")
23762378
data.setdefault("command", _sandbox_launch_command("search"))
23772379
data.setdefault("automation_available", _sandbox_control_configured())

services/ui/ui/templates/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ <h3>Local-first. Private by default.</h3>
227227
try {
228228
var resp = await fetch('/api/search/status');
229229
var data = await resp.json();
230-
searchAvailable = data.search_enabled !== false;
230+
searchAvailable = data.search_available !== undefined
231+
? data.search_available === true
232+
: data.search_enabled !== false && data.searxng_reachable !== false;
231233
searchAutomationAvailable = data.automation_available === true;
232234
if (searchAvailable) {
233235
modeWeb.disabled = false;

tests/test_search.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,26 @@ def fail_get(*args, **kwargs):
280280

281281
assert resp.status_code == 200
282282

283+
def test_live_never_probes_optional_upstream(self, monkeypatch):
284+
import app as sm
285+
286+
with monkeypatch.context() as m:
287+
m.setattr(sm, "_is_search_enabled", lambda: True)
288+
m.setattr(sm, "_get_session_mode", lambda: "normal")
289+
290+
def fail_get(*args, **kwargs):
291+
raise AssertionError("liveness probe must not depend on SearXNG")
292+
293+
m.setattr(sm.requests, "get", fail_get)
294+
295+
with search_app.test_client() as client:
296+
resp = client.get("/live")
297+
298+
assert resp.status_code == 200
299+
data = resp.get_json()
300+
assert data["status"] == "ok"
301+
assert data["searxng_reachable"] is None
302+
283303
def test_search_forwards_policy_allowed_engines(self, monkeypatch):
284304
import app as sm
285305

tests/test_ui.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@ def test_models_endpoint_empty_when_registry_down(self, client):
5757
assert resp.status_code == 200
5858
assert resp.get_json() == []
5959

60+
def test_search_status_requires_reachable_search_backend(self, client):
61+
class SearchResp:
62+
def json(self):
63+
return {
64+
"status": "ok",
65+
"search_enabled": True,
66+
"searxng_reachable": False,
67+
}
68+
69+
with patch("ui.app.requests.get", return_value=SearchResp()), \
70+
patch("ui.app._is_sandbox_deployment", return_value=True), \
71+
patch("ui.app._sandbox_control_configured", return_value=False):
72+
resp = client.get("/api/search/status")
73+
74+
assert resp.status_code == 200
75+
data = resp.get_json()
76+
assert data["search_available"] is False
77+
assert "start --with-search" in data["command"]
78+
6079
def test_profile_status_infers_full_lab_in_sandbox_when_diffusion_is_live(self, client):
6180
class HealthyResp:
6281
status_code = 200

0 commit comments

Comments
 (0)