reloading the configuration must actually reload it (v0.4.24)

/admin/api/system/reload built a bare `Request({})` in order to reach
`app.state.config_mgr`, which raises on the first attribute: the endpoint
answered 500 to every call it ever received and had never reloaded anything.
An operator editing a config file by hand therefore had no way to apply the
edit except restarting the server.

That matters most for a pod pool. A pool reads its ceilings -- max_pods,
gpu_types, the cost caps -- out of models_data on every acquisition, so an
edit the process has not re-read is an edit that does not apply, and the
operator's only lever was a restart that drops whatever is in flight.

The handler now takes the injected Request and falls back to the manager that
set_config_manager installed, because the front proxy and the engine do not
both populate app state. It reloads BOTH files and reports how many model
entries it re-read, so a caller can see that the reload reached models.json
and not only config.json.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnfkxBpcZ1v6ypRFFyhrVM
parent 1ccc44f8
......@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here.
__version__ = "0.4.23"
__version__ = "0.4.24"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
......@@ -3847,24 +3847,40 @@ async def api_task_restart(task_id: str, username: str = Depends(require_admin))
# --- System endpoints ---
@router.post("/admin/api/system/reload", summary="Reload server configuration")
async def api_reload_config(username: str = Depends(require_admin)):
"""Reload configuration from disk."""
async def api_reload_config(request: Request, username: str = Depends(require_admin)):
"""Re-read config.json and models.json from disk, in place.
This is the alternative to restarting the server after an operator edits a
config file by hand, so it has to reload BOTH files: a pod pool reads its
ceilings (``max_pods``, ``gpu_types``, the cost caps) out of
``models_data`` on every acquisition, and an edit the process has not
re-read is an edit that does not apply.
It used to build a bare ``Request({})`` to reach ``app.state`` and so
raised on every call -- the endpoint had never reloaded anything. The
manager is taken from the injected request, falling back to the module
global that ``set_config_manager`` sets, because the front proxy and the
engine do not both populate app state.
"""
mgr = getattr(getattr(request, "app", None), "state", None)
mgr = getattr(mgr, "config_mgr", None) or config_manager
if mgr is None:
raise HTTPException(status_code=503, detail="Config manager not initialized")
try:
from fastapi import Request
# config_mgr is stored in app state
request = Request({})
config = request.app.state.config_mgr.reload()
return {
"success": True,
"message": "Configuration reloaded",
"config": {
"loaded": config.models.loaded,
"preload": config.models.preload,
"load_mode": config.models.default_load_mode
}
}
cfg = mgr.reload()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=f"reload failed: {e}")
md = getattr(mgr, "models_data", None) or {}
return {
"success": True,
"message": "Configuration reloaded",
"models_reloaded": sum(len(v) for v in md.values() if isinstance(v, list)),
"config": {
"loaded": cfg.models.loaded,
"preload": cfg.models.preload,
"load_mode": cfg.models.default_load_mode,
},
}
from datetime import datetime
......
"""/admin/api/system/reload must actually re-read the files.
The endpoint existed for a long time and could never have worked: it built a
bare ``Request({})`` to reach ``app.state``, which raises before anything is
read. An operator who edited a pod pool's ``max_pods`` by hand and called
reload got a 500 and, worse, no reload -- so the only way to apply a config
edit was a restart.
These tests pin the two things that matter: the call succeeds without an HTTP
request object carrying app state, and ``models_data`` is re-read, because a
pod pool takes its ceilings from that dict on every acquisition.
"""
import json
import asyncio
import pytest
from codai.admin import routes
class _Mgr:
"""Minimal ConfigManager stand-in: reload() re-reads the file."""
def __init__(self, path):
self.path = path
self.models_data = {}
self.reloads = 0
class _M:
loaded = []
preload = []
default_load_mode = "on-request"
class _C:
models = _M()
self.config = _C()
def reload(self):
self.reloads += 1
self.models_data = json.load(open(self.path))
return self.config
def _call(mgr_in_state=None):
"""Invoke the handler the way FastAPI would, with an injected request."""
class _State:
pass
class _App:
pass
class _Req:
pass
req = _Req()
app = _App()
st = _State()
if mgr_in_state is not None:
st.config_mgr = mgr_in_state
app.state = st
req.app = app
return asyncio.run(routes.api_reload_config(req, username="admin"))
@pytest.fixture
def models_file(tmp_path):
p = tmp_path / "models.json"
p.write_text(json.dumps({"text_models": [{"id": "a", "runpod": {"max_pods": 128}}]}))
return p
def test_reload_succeeds_from_app_state(models_file):
mgr = _Mgr(models_file)
out = _call(mgr_in_state=mgr)
assert out["success"] is True
assert mgr.reloads == 1
def test_reload_falls_back_to_the_module_manager(models_file, monkeypatch):
"""The front proxy does not populate app.state.config_mgr."""
mgr = _Mgr(models_file)
monkeypatch.setattr(routes, "config_manager", mgr)
out = _call(mgr_in_state=None)
assert out["success"] is True
assert mgr.reloads == 1
def test_models_json_is_re_read_not_just_config(models_file):
"""The regression that mattered: a hand-edited pool ceiling must apply."""
mgr = _Mgr(models_file)
_call(mgr_in_state=mgr)
assert mgr.models_data["text_models"][0]["runpod"]["max_pods"] == 128
models_file.write_text(json.dumps(
{"text_models": [{"id": "a", "runpod": {"max_pods": 1}}]}))
_call(mgr_in_state=mgr)
assert mgr.models_data["text_models"][0]["runpod"]["max_pods"] == 1
def test_reload_reports_how_many_models_it_read(models_file):
mgr = _Mgr(models_file)
out = _call(mgr_in_state=mgr)
assert out["models_reloaded"] == 1
def test_no_manager_is_503_not_500(monkeypatch):
from fastapi import HTTPException
monkeypatch.setattr(routes, "config_manager", None)
with pytest.raises(HTTPException) as e:
_call(mgr_in_state=None)
assert e.value.status_code == 503
def test_a_failing_reload_is_reported_not_swallowed(tmp_path):
from fastapi import HTTPException
bad = tmp_path / "models.json"
bad.write_text("{not json")
mgr = _Mgr(bad)
with pytest.raises(HTTPException) as e:
_call(mgr_in_state=mgr)
assert e.value.status_code == 500
assert "reload failed" in str(e.value.detail)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment