fix(queue): обрезка error, поздний done по failed, статус отменённого пакета, пустой полный прогон (#13)
M-4 (hub#12), T5.6 и M-12 (hub#4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a309c9bfb1
commit
4f0b873db6
@ -13,6 +13,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
TERMINAL = {"done", "failed", "skipped", "cancelled", "confirmed_by_state"}
|
||||
MAX_DELIVERIES = 3 # выдач без отчёта, после которых команда — failed (bridge-reliability, решение 2)
|
||||
ERROR_LIMIT = 1500 # символов ошибки в command.error (spec commands)
|
||||
ONLINE_MINUTES = 5
|
||||
|
||||
|
||||
@ -71,7 +72,7 @@ def _status_of(cmds, cancelled_at=None) -> str:
|
||||
if not sts:
|
||||
return "done"
|
||||
if all(s in TERMINAL for s in sts):
|
||||
return "cancelled" if cancelled_at and all(s == "cancelled" for s in sts) else "done"
|
||||
return "cancelled" if cancelled_at else "done" # отменённый пакет — отменён, даже с частью done (T5.6)
|
||||
if any(s == "claimed" for s in sts) or any(s in TERMINAL for s in sts):
|
||||
return "running"
|
||||
return "queued"
|
||||
@ -212,6 +213,7 @@ def record_state(conn, command_id, issues_count, links_count) -> None:
|
||||
def report_result(conn, command_id, status, result=None, error=None, jira_calls=0, rate_limited=0) -> dict:
|
||||
if status not in ("done", "failed"):
|
||||
raise ValueError("status — done или failed")
|
||||
error = None if error is None else str(error)[:ERROR_LIMIT]
|
||||
with db.transaction(conn):
|
||||
cmd = get_command(conn, command_id)
|
||||
if cmd is None:
|
||||
@ -219,13 +221,15 @@ def report_result(conn, command_id, status, result=None, error=None, jira_calls=
|
||||
if cmd["status"] == "queued":
|
||||
raise NotClaimedError(f"команда {command_id} не выдана")
|
||||
if cmd["status"] in TERMINAL:
|
||||
# поздний отчёт о созданной задаче по отменённой команде: ключ не теряем, статус не трогаем (M-5)
|
||||
if (cmd["status"] == "cancelled" and cmd["type"] == "ensure_issue" and status == "done"
|
||||
# поздний отчёт о созданной задаче по команде, которую хаб уже закрыл (force-отмена — M-5, предел выдач —
|
||||
# M-4 #12): ключ не теряем, статус не трогаем
|
||||
if (cmd["status"] in ("cancelled", "failed") and cmd["type"] == "ensure_issue" and status == "done"
|
||||
and result and result.get("key") and not cmd["result"]):
|
||||
conn.execute("UPDATE command SET result = ?, jira_calls = ?, rate_limited = ? WHERE id = ?",
|
||||
(_dump(result), int(jira_calls or 0), int(rate_limited or 0), command_id))
|
||||
mirror.rename_note(conn, cmd["params"]["temp_key"], result["key"])
|
||||
log.warning("команда %s отменена, но агент создал %s — ключ записан в результат", command_id, result["key"])
|
||||
log.warning("команда %s уже %s, но агент создал %s — ключ записан в результат",
|
||||
command_id, cmd["status"], result["key"])
|
||||
return get_command(conn, command_id)
|
||||
return cmd # идемпотентно; cancelled не затирается
|
||||
now = db.now()
|
||||
@ -239,9 +243,10 @@ def report_result(conn, command_id, status, result=None, error=None, jira_calls=
|
||||
conn.execute("UPDATE sync_run SET finished_at = ?, jira_calls = ?, rate_limited = ? WHERE command_id = ?",
|
||||
(now, int(jira_calls or 0), int(rate_limited or 0), command_id))
|
||||
if status == "done" and is_full_refresh(conn, command_id):
|
||||
# полный прогон не обновил задачу — в базовую JQL она больше не входит: сосед
|
||||
run = conn.execute("SELECT started_at FROM sync_run WHERE command_id = ?", (command_id,)).fetchone()
|
||||
if run:
|
||||
# полный прогон не обновил задачу — в базовую JQL она больше не входит: сосед. Пустой прогон (0 задач) —
|
||||
# почти всегда сбой выборки, а не «все задачи ушли»: соседей не трогаем (M-12)
|
||||
run = conn.execute("SELECT started_at, issues_count FROM sync_run WHERE command_id = ?", (command_id,)).fetchone()
|
||||
if run and run["issues_count"] > 0:
|
||||
conn.execute("UPDATE issue SET neighbor = 1 WHERE fetched_at < ?", (run["started_at"],))
|
||||
if cmd["type"] == "refresh" and status == "done":
|
||||
reconcile(conn)
|
||||
|
||||
@ -379,3 +379,57 @@ def test_late_failed_report_after_force_cancel_changes_nothing():
|
||||
queue.cancel_command(conn, c["id"], force=True)
|
||||
late = queue.report_result(conn, c["id"], "failed", error="HTTP 400")
|
||||
assert late["status"] == "cancelled" and late["result"] is None and late["error"] is None
|
||||
|
||||
|
||||
def _fail_by_limit(conn, cid):
|
||||
for _ in range(queue.MAX_DELIVERIES - 1):
|
||||
assert queue.claim(conn)["id"] == cid
|
||||
queue.claim(conn) # четвёртый claim: cid → failed по пределу
|
||||
assert queue.get_command(conn, cid)["status"] == "failed"
|
||||
|
||||
|
||||
def test_error_is_trimmed_to_1500():
|
||||
conn = fresh()
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
c = queue.claim(conn)
|
||||
out = queue.report_result(conn, c["id"], "failed", error="е" * 5000)
|
||||
assert out["error"] == "е" * 1500 and queue.ERROR_LIMIT == 1500
|
||||
|
||||
|
||||
def test_late_done_after_delivery_limit_keeps_issue_key():
|
||||
conn = fresh()
|
||||
mirror.set_note(conn, "new:8", {"fact_status": "План"}, "import")
|
||||
queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:8", "issue_type": "task", "summary": "Тема"}}])
|
||||
c = queue.claim(conn)
|
||||
_fail_by_limit(conn, c["id"])
|
||||
late = queue.report_result(conn, c["id"], "done", result={"key": "A-30", "created": True}, jira_calls=2)
|
||||
assert late["status"] == "failed" and late["result"] == {"key": "A-30", "created": True}
|
||||
assert late["error"] == "агент не отчитался после 3 выдач"
|
||||
assert mirror.get_note(conn, "A-30")["fact_status"] == "План" and mirror.get_note(conn, "new:8") is None
|
||||
again = queue.report_result(conn, c["id"], "done", result={"key": "A-31"})
|
||||
assert again["result"] == {"key": "A-30", "created": True} # повтор ничего не меняет
|
||||
|
||||
|
||||
def test_cancelled_batch_with_done_command_is_cancelled():
|
||||
conn = fresh()
|
||||
b = queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2"), fields("A-3")])
|
||||
first = queue.claim(conn)
|
||||
queue.report_result(conn, first["id"], "done", result={"changed": []})
|
||||
second = queue.claim(conn)
|
||||
queue.cancel_batch(conn, b["id"])
|
||||
queue.report_result(conn, second["id"], "done", result={"changed": []})
|
||||
view = queue.batch_view(conn, b["id"])
|
||||
assert view["status"] == "cancelled"
|
||||
assert view["counts"]["done"] == 2
|
||||
|
||||
|
||||
def test_empty_full_refresh_keeps_neighbors():
|
||||
conn = fresh()
|
||||
jql = settings.get(conn, "base_jql")
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "x", "labels": []}], [],
|
||||
fetched_at="2020-01-01T00:00:00+00:00") # прочитана давно — до старта прогона
|
||||
queue.create_batch(conn, "полный", "claude", [{"type": "refresh", "params": {"jql": jql}}])
|
||||
r = queue.claim(conn)
|
||||
queue.report_result(conn, r["id"], "done") # загрузок состояния не было: 0 задач
|
||||
assert conn.execute("SELECT neighbor FROM issue WHERE key = 'A-1'").fetchone()["neighbor"] == 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user