// collect all threads to join in one pass while the lock is held:
// - monitoring threads from just-unloaded models (to_unload)
+ // - threads of finished downloads (DOWNLOADED), they acquire the mutex on exit
// - threads of already-UNLOADED models that are being removed from source
std::vector<std::thread> threads_to_join;
for (const auto & name : to_unload) {
if (inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) {
continue; // downloading models are not from config sources, leave them alone
}
+ if (inst.meta.status == SERVER_MODEL_STATUS_DOWNLOADED) {
+ // joining this thread under the lock deadlocks: it locks the mutex on its way out
+ if (inst.th.joinable()) {
+ threads_to_join.push_back(std::move(inst.th));
+ }
+ continue;
+ }
if (final_presets.find(name) == final_presets.end() && !inst.meta.is_running() && inst.th.joinable()) {
threads_to_join.push_back(std::move(inst.th));
}
if (it->second.meta.status == SERVER_MODEL_STATUS_DOWNLOADING) {
++it; // download thread is still busy, skip
} else if (it->second.meta.status == SERVER_MODEL_STATUS_DOWNLOADED) {
- // download finished, safe to erase
- if (it->second.th.joinable()) {
- it->second.th.join();
- }
+ // download finished, thread is joined above, safe to erase
+ GGML_ASSERT(!it->second.th.joinable());
it = mapping.erase(it);
} else if (final_presets.find(it->first) == final_presets.end()) {
SRV_INF("(reload) removing model name=%s (no longer in source)\n", it->first.c_str());
return False
-@pytest.mark.skip(reason="sse_thread sometimes hangs on GH actions, to be investigated")
def test_router_download_model():
"""Case 1: download a model, verify SSE events and GET /models."""
global server
assert MODEL_DOWNLOAD_ID in ids, f"{MODEL_DOWNLOAD_ID} not found in /models after download"
-@pytest.mark.skip(reason="sse_thread sometimes hangs on GH actions, to be investigated")
def test_router_delete_model():
"""Case 2: delete the downloaded model, verify it disappears from GET /models."""
global server
DEFAULT_HTTP_TIMEOUT = 60
+# per-request timeout, a hung server fails the test instead of stalling the CI for hours
+DEFAULT_REQUEST_TIMEOUT = 600
+
class ServerResponse:
headers: dict
path: str,
data: dict | Any | None = None,
headers: dict | None = None,
- timeout: float | None = None,
+ timeout: float | None = DEFAULT_REQUEST_TIMEOUT,
) -> ServerResponse:
url = f"http://{self.server_host}:{self.server_port}{path}"
parse_body = False
path: str,
data: dict | None = None,
headers: dict | None = None,
- timeout: float | None = None,
+ timeout: float | None = DEFAULT_REQUEST_TIMEOUT,
) -> dict:
stream = data.get('stream', False)
if stream: