fix(worker+scheduler+inbox): три фикса часовой сводки (t-260930162615-fb58be)
1) per-call timeout LLM-вызова (SIMBIOT_LLM_CALL_TIMEOUT, 90с default, max_retries=1): роутер-лог показал — зависший тик = POST в ollama принят, но байт стрима не пришёл (DONE без TTFT), соседние вызовы ходили за 1-3с. Один висящий вызов больше не убивает весь тик по уровневому таймауту (300/2880с) — режется сам вызов, event-loop завершает тик штатно, spend-хвост и задачи не теряются (e-260930161900-baaecb, e-260930162600-cbb7e2, e-260930153143-85246b). 2) обрезка TG-инбокса (e-260930154125-082a2a): приём 4000→10000 симв., показ в контексте тика 300→1200 — длинные сообщения Alex больше не теряют хвост. 3) loss-of-claim в батче: TimeoutExpired от isolated_runner пролетал мимо TaskFailed-блока run_level_tick — claimed-задачи висели в running до сирот-чистки (час+). Теперь claim сразу возвращается TaskPromoted в очередь (reason с причиной смерти воркера). Тесты 8/8 (hermetic), живой вызов модели с timeout=90 — ok.
This commit is contained in:
parent
2f4e3d22a7
commit
3361b671ed
3 changed files with 39 additions and 9 deletions
|
|
@ -88,7 +88,16 @@ def _wire_tool_budget(agent, max_calls: int = 30):
|
||||||
|
|
||||||
def build_level_model_from_payload(payload: dict):
|
def build_level_model_from_payload(payload: dict):
|
||||||
"""Модель для воркера — та же сборка, что и в runtime.build_level_model:
|
"""Модель для воркера — та же сборка, что и в runtime.build_level_model:
|
||||||
9router понимает chat/completions (usage там же), Responses API — шим без usage."""
|
9router понимает chat/completions (usage там же), Responses API — шим без usage.
|
||||||
|
|
||||||
|
SIMBIOT_LLM_CALL_TIMEOUT (e-260930161900-6da5c9, e-260930153143-85246b):
|
||||||
|
per-call таймаут LLM-вызова в сек. Роутер-лог показал: зависший тик = POST
|
||||||
|
в ollama-апстрим принят, но байт стрима так и не пришёл (DONE без TTFT),
|
||||||
|
при этом соседние вызовы ходили за 1-3с. Таймаут уровня (300/2880с) убивает
|
||||||
|
ВЕСЬ тик из-за одного висящего вызова; теперь вызов режется (до 90с,
|
||||||
|
hour 180с), исключение отлавливает event-loop — тик получает результат
|
||||||
|
с пометкой «не успел», spend-хвост и задачи не теряются.
|
||||||
|
"""
|
||||||
from strands.models.openai import OpenAIModel
|
from strands.models.openai import OpenAIModel
|
||||||
import os
|
import os
|
||||||
params = {}
|
params = {}
|
||||||
|
|
@ -98,9 +107,15 @@ def build_level_model_from_payload(payload: dict):
|
||||||
model_id = payload.get("model") or "auto"
|
model_id = payload.get("model") or "auto"
|
||||||
if model_id == "none":
|
if model_id == "none":
|
||||||
model_id = "auto"
|
model_id = "auto"
|
||||||
|
# per-call таймаут: один зависший апстрим-вызов не должен убивать весь тик
|
||||||
|
call_timeout = int(os.environ.get("SIMBIOT_LLM_CALL_TIMEOUT", "90"))
|
||||||
return OpenAIModel(
|
return OpenAIModel(
|
||||||
|
# timeout открытого клиента (openai SDK) режет каждый chat.completions
|
||||||
|
# вызов; при зависании апстрима получаем APITimeoutError за N сек и
|
||||||
|
# strands event-loop завершает тик штатно (без потери предыдущих шагов)
|
||||||
client_args={"api_key": os.environ.get("OPENAI_API_KEY", ""),
|
client_args={"api_key": os.environ.get("OPENAI_API_KEY", ""),
|
||||||
"base_url": os.environ.get("OPENAI_BASE_URL", "http://127.0.0.1:20128/v1")},
|
"base_url": os.environ.get("OPENAI_BASE_URL", "http://127.0.0.1:20128/v1"),
|
||||||
|
"timeout": call_timeout, "max_retries": 1},
|
||||||
model_id=model_id,
|
model_id=model_id,
|
||||||
**({"params": params} if params else {}),
|
**({"params": params} if params else {}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,7 @@ def build_context(level_cfg, model: dict, batch=None):
|
||||||
tag = (" [голосовое, расшифровка не удалась — попроси прислать текстом]"
|
tag = (" [голосовое, расшифровка не удалась — попроси прислать текстом]"
|
||||||
if m.get("transcribed") else
|
if m.get("transcribed") else
|
||||||
" [голосовое, ещё не расшифровано]")
|
" [голосовое, ещё не расшифровано]")
|
||||||
lines.append(f"{i}. [{m_id}] от {who}{tag}: {m.get('text','')[:300]}")
|
lines.append(f"{i}. [{m_id}] от {who}{tag}: {m.get('text','')[:1200]}")
|
||||||
batch_ids = ",".join(ids)
|
batch_ids = ",".join(ids)
|
||||||
# t-260930064924: батч-триаж — ВСЕ неотвеченные обрабатываются ОДНИМ тиком.
|
# t-260930064924: батч-триаж — ВСЕ неотвеченные обрабатываются ОДНИМ тиком.
|
||||||
# Раньше мин-уровень отвечал по одному за тик (m-516173222..228 — 7 тиков
|
# Раньше мин-уровень отвечал по одному за тик (m-516173222..228 — 7 тиков
|
||||||
|
|
@ -417,11 +417,26 @@ def run_level_tick(level_cfg, seq: int, model: dict):
|
||||||
if not claimed:
|
if not claimed:
|
||||||
claimed = None
|
claimed = None
|
||||||
prompt = build_context(level_cfg, model, batch)
|
prompt = build_context(level_cfg, model, batch)
|
||||||
|
try:
|
||||||
text, usage, trace_id = isolated_runner.run_llm_tick(
|
text, usage, trace_id = isolated_runner.run_llm_tick(
|
||||||
level_cfg, prompt,
|
level_cfg, prompt,
|
||||||
session_id=f"s-{level_cfg.name}-{seq}",
|
session_id=f"s-{level_cfg.name}-{seq}",
|
||||||
trace_prefix=f"{level_cfg.name}-{seq}",
|
trace_prefix=f"{level_cfg.name}-{seq}",
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# e-260930162600-cbb7e2/cbb7e2-семейство: TimeoutExpired от isolated_runner
|
||||||
|
# пролетал МИМО блока text-is-None ниже — claimed-задачи висели в running
|
||||||
|
# до сирот-чистки (час+), очередь казалась «не стартующей» (85246b).
|
||||||
|
# Возвращаем claim в очередь сразу (TaskPromoted — тот же механизм, что
|
||||||
|
# orphan-requeue: ready + сброс уровня; следующий тик возьмёт заново),
|
||||||
|
# исключение летит дальше — CycleError/эскалация как раньше.
|
||||||
|
for tid in (claimed or []):
|
||||||
|
size = next((t.get("size", "M") for t in batch if t.get("id") == tid), "M")
|
||||||
|
es.append_event("task", tid, "TaskPromoted",
|
||||||
|
{"to": size, "from": size,
|
||||||
|
"reason": f"тик-воркер умер ({str(e)[:120]}) — "
|
||||||
|
f"claim возвращён в очередь без потери"})
|
||||||
|
raise
|
||||||
if text is None:
|
if text is None:
|
||||||
# воркер упал (таймаут/краш) — НИ одна задача не выполнена с его
|
# воркер упал (таймаут/краш) — НИ одна задача не выполнена с его
|
||||||
# точки зрения, но executor мог частично поработать инструментами.
|
# точки зрения, но executor мог частично поработать инструментами.
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ def receive_message(update: dict):
|
||||||
"user_id": user.get("id"),
|
"user_id": user.get("id"),
|
||||||
"username": user.get("username"),
|
"username": user.get("username"),
|
||||||
"first_name": user.get("first_name"),
|
"first_name": user.get("first_name"),
|
||||||
"text": (msg.get("text") or msg.get("caption") or "")[:4000],
|
"text": (msg.get("text") or msg.get("caption") or "")[:10000],
|
||||||
"kind": kind,
|
"kind": kind,
|
||||||
"file_id": file_id,
|
"file_id": file_id,
|
||||||
# file_unique_id: ключ аудио-кэша и идемпотентности расшифровки
|
# file_unique_id: ключ аудио-кэша и идемпотентности расшифровки
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue