mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
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:
1 parent
a8767488ce
commit
74f390538c
2 files changed
+48
-7
No files matched your search
+15
-7
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user