serve: the error of a request that ended without an answer says the next request restarts the engine only where the engine was really ended (#3961)

This commit is contained in:
Niko1221 committed 2026-11-04 13:34:06 +01:00
1 parent a8767488ce
commit 74f390538c
2 files changed
+48 -7

No files matched your search

+15 -7
View File
@@ -423,6 +423,12 @@ class MockEngine:
class EngineDied(RuntimeError):
"""The engine process ended in the middle of a request (issue #27: on Linux, the out-of-memory killer)."""
restarts = True # the engine is gone, so the next request starts it again; False for an error raised while the engine is left running (#3961)
def died_note(e: Exception) -> str:
"""What an API error says about an EngineDied: the cause, and that the next request restarts the engine - only where it really is ended."""
return f"{e}; the next request restarts it" if getattr(e, "restarts", True) else str(e)
class EngineStarting(RuntimeError):
@@ -6959,8 +6965,10 @@ def openai_chunks(svc: Service, req: dict, ids, thinking, tools, max_new, cancel
print("[strata] a request ended without an answer and without a finish reason; it is answered as an error", flush=True)
# the engine is ended so "the next request restarts it" is true - but only that engine process, and only while nobody is on it (the turn is taken
# without waiting): this runs after the run released its turn, and a request admitted since must not be killed by it
svc.end_engine_if_unchanged(born)
raise EngineDied("the engine ended the request without an answer")
ended = svc.end_engine_if_unchanged(born)
lost = EngineDied("the engine ended the request without an answer")
lost.restarts = bool(ended) # "the next request restarts it" only where the engine really was ended (not with batch slots, replicas, another request on it)
raise lost
def _is_json(text: str) -> bool:
@@ -8077,7 +8085,7 @@ def make_handler(svc: Service):
"message": f"the request is malformed ({type(e).__name__}: {e})"}}
self._json(400, responses_error_body(body["error"]["message"]) if path == "/v1/responses" else body)
except EngineDied as e: # before the answer started (not streamed)
self._json(503, {"error": {"type": "server_error", "message": f"{e}; the next request restarts it"}})
self._json(503, {"error": {"type": "server_error", "message": died_note(e)}})
except EngineStuck as e: # an unload or restart that could not end the engine
self._json(503, {"error": {"type": "server_error", "message": str(e)}})
@@ -8405,7 +8413,7 @@ def make_handler(svc: Service):
svc.note_client_disconnect(cancel) # client went away: stop the engine
chunks.close()
except EngineDied as e: # mid-stream: say so, then end the stream properly
err = {"error": {"type": "server_error", "message": f"{e}; the next request restarts it"}}
err = {"error": {"type": "server_error", "message": died_note(e)}}
self._note(error=err["error"])
self.wfile.write(b"data: " + json.dumps(err).encode() + b"\n\ndata: [DONE]\n\n")
except QueueWaitExpired as e:
@@ -8484,7 +8492,7 @@ def make_handler(svc: Service):
return self._json(429, responses_error_body(str(e), "server_error",
code="queue_wait_timeout"), retry_after=1)
except EngineDied as e:
return self._json(503, responses_error_body(f"{e}; the next request restarts it", "server_error",
return self._json(503, responses_error_body(died_note(e), "server_error",
code="server_error"))
except EngineNonFinite as e: # #879, twice: not the request's fault
return self._json(503, responses_error_body(str(e), "server_error", code="server_error"))
@@ -8517,7 +8525,7 @@ def make_handler(svc: Service):
svc.note_client_disconnect(cancel) # client went away: stop the engine
items.close()
except EngineDied as e: # mid-stream: response.failed, then the stream ends
self._responses_failed(asm, f"{e}; the next request restarts it", "server_error", send)
self._responses_failed(asm, died_note(e), "server_error", send)
except QueueWaitExpired as e:
self._responses_failed(asm, str(e), "queue_wait_timeout", send)
except StructuredOutputError as e:
@@ -8643,7 +8651,7 @@ def make_handler(svc: Service):
svc.note_client_disconnect(cancel)
events.close()
except EngineDied as e: # mid-stream: Anthropic's error event
err = {"type": "error", "error": {"type": "api_error", "message": f"{e}; the next request restarts it"}}
err = {"type": "error", "error": {"type": "api_error", "message": died_note(e)}}
self._note(error=err["error"])
self.wfile.write(b"event: error\ndata: " + json.dumps(err).encode() + b"\n\n")
except QueueWaitExpired as e:
+33
View File
@@ -6414,15 +6414,48 @@ class OpenAIChunksTerminal(unittest.TestCase):
self.assertFalse(svc.end_engine_if_unchanged(4))
self.assertEqual(engine.ended_count, 0)
def lost_run(self, engine):
"""The error a no-done run raises on this engine, and whether the engine was ended."""
from serve.frontend import Event
from serve.server import openai_chunks, died_note
svc = self.service(engine)
svc.model_for = lambda req: "m"
svc.tok = None
with contextlib.redirect_stdout(io.StringIO()):
with self.assertRaises(EngineDied) as caught:
list(openai_chunks(svc, {}, [1], False, None, 8, threading.Event(), run=iter([("event", Event("content", "hi"))])))
return caught.exception, died_note(caught.exception)
def test_batch_slots_share_the_engine_so_it_is_left_alone(self):
engine = self.Engine(gen=4, batch=2)
self.assertFalse(self.service(engine).end_engine_if_unchanged(4))
self.assertEqual(engine.ended_count, 0)
error, said = self.lost_run(engine) # the engine keeps running, so the error does not promise a restart
self.assertEqual(said, "the engine ended the request without an answer")
self.assertEqual(engine.ended_count, 0)
def test_an_engine_without_a_generation_is_left_alone(self):
engine = self.Engine(gen=4)
self.assertFalse(self.service(engine).end_engine_if_unchanged(None))
self.assertEqual(engine.ended_count, 0)
del engine.gen # replicas: no gen
error, said = self.lost_run(engine)
self.assertEqual(said, "the engine ended the request without an answer")
self.assertEqual(engine.ended_count, 0)
def test_the_restart_sentence_is_said_only_where_the_engine_was_ended(self):
engine = self.Engine(gen=4)
error, said = self.lost_run(engine)
self.assertEqual(engine.ended_count, 1)
self.assertEqual(said, "the engine ended the request without an answer; the next request restarts it")
engine = self.Engine(gen=4) # another request holds the turn: not ended, no promise
svc = self.service(engine)
svc.fifo.acquire()
from serve.server import died_note
error = EngineDied("x")
error.restarts = svc.end_engine_if_unchanged(4)
self.assertEqual(died_note(error), "x")
self.assertEqual(died_note(EngineDied("the engine stopped")), "the engine stopped; the next request restarts it") # every other EngineDied is as before
def test_the_whole_race_through_the_stream(self): # a no-done run, and the next request is on the engine when the check runs
from serve.frontend import Event