feat(agent): время последнего вызова Jira восстанавливается из журнала после перезапуска (M-9, #9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b045aed8b6
commit
12858c2718
31
src/agent.py
31
src/agent.py
@ -345,6 +345,20 @@ class Journal:
|
||||
log("журнал: строка %d не читается, пропускаю" % n)
|
||||
return good
|
||||
|
||||
def last_at(self) -> Optional[datetime]:
|
||||
"""Самое позднее время записи журнала (UTC). Записи без времени или с непонятным временем пропускаются."""
|
||||
latest = None
|
||||
for r in self.records():
|
||||
try:
|
||||
t = datetime.fromisoformat(r["at"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
if t.tzinfo is None:
|
||||
continue
|
||||
if latest is None or t > latest:
|
||||
latest = t
|
||||
return latest
|
||||
|
||||
def claimed(self, command_id) -> None:
|
||||
"""Команда получена из claim (до паузы): с этого момента хаб считает её выданной агенту."""
|
||||
self.append({"kind": "claimed", "command_id": int(command_id)})
|
||||
@ -916,6 +930,22 @@ class Runner:
|
||||
self.executor.sleep = self.sleep
|
||||
log("настройки хаба v%s (проект %s)" % (self.settings.get("settings_version"), self.settings.get("project_key")))
|
||||
|
||||
def restore_pace(self) -> None:
|
||||
"""После перезапуска время последнего вызова Jira неизвестно (живёт в памяти). Берём самое позднее время записи
|
||||
журнала: finished пишется сразу после последнего вызова команды, оценка не раньше настоящего вызова (M-9)."""
|
||||
if self.jira.last_call_at is not None:
|
||||
return
|
||||
at = self.journal.last_at()
|
||||
if at is None:
|
||||
return
|
||||
ago = (datetime.now(timezone.utc) - at).total_seconds()
|
||||
if ago < 0:
|
||||
log("журнал: время последней записи в будущем (%s) — темп не восстанавливаю" % at.isoformat())
|
||||
return
|
||||
self.jira.last_call_at = time.monotonic() - ago
|
||||
self.jira.last_call_iso = at.astimezone(timezone.utc).replace(microsecond=0).isoformat()
|
||||
log("темп: последний вызов Jira не позже %s (по журналу)" % self.jira.last_call_iso)
|
||||
|
||||
def send_result(self, rec: dict) -> bool:
|
||||
"""Отчёт хабу; повторяется каждые 10 с, пока хаб не подтвердит. 404/409/422 — хаб отверг (команда отменена
|
||||
или удалена): считаем закрытой, чтобы не зациклиться. При остановке по сигналу делается одна попытка."""
|
||||
@ -1056,6 +1086,7 @@ class Runner:
|
||||
self.sleep(wait)
|
||||
|
||||
def loop(self) -> None:
|
||||
self.restore_pace()
|
||||
idle, recovered = 0, False
|
||||
while not self.stop:
|
||||
if self.settings is None:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
@ -134,6 +135,20 @@ class JournalTest(unittest.TestCase):
|
||||
mode = os.stat(self.path).st_mode & 0o777
|
||||
self.assertEqual(mode & 0o077, 0)
|
||||
|
||||
def test_last_at_is_latest_record_time(self):
|
||||
self.assertIsNone(self.j.last_at())
|
||||
self.j.append({"kind": "claimed", "command_id": 1, "at": "2026-10-13T10:00:00+00:00"})
|
||||
self.j.append({"kind": "finished", "command_id": 1, "at": "2026-10-13T10:05:00+00:00"})
|
||||
self.j.append({"kind": "acked", "command_id": 1, "at": "2026-10-13T10:04:00+00:00"})
|
||||
self.assertEqual(self.j.last_at(), datetime(2026, 10, 13, 10, 5, tzinfo=timezone.utc))
|
||||
|
||||
def test_last_at_skips_bad_and_missing_times(self):
|
||||
self.j.append({"kind": "claimed", "command_id": 1, "at": "2026-10-13T10:00:00+00:00"})
|
||||
self.j.append({"kind": "started", "command_id": 1, "at": "не время"})
|
||||
self.j.append({"kind": "started", "command_id": 1, "at": "2026-10-13T11:00:00"}) # без зоны — пропуск
|
||||
with open(self.path, "a", encoding="utf-8") as f:
|
||||
f.write("битая строка\n")
|
||||
self.assertEqual(self.j.last_at(), datetime(2026, 10, 13, 10, 0, tzinfo=timezone.utc))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -2,6 +2,7 @@ import os
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from _common import SETTINGS, agent, jira_cfg, no_sleep
|
||||
from fake_hub import FakeHub
|
||||
@ -50,6 +51,31 @@ class RunnerTest(unittest.TestCase):
|
||||
self.assertEqual(self.kinds(), [("claimed", 1), ("started", 1), ("finished", 1), ("acked", 1), ("claimed", 2), ("started", 2), ("finished", 2), ("acked", 2)])
|
||||
self.assertEqual(self.sleeps, []) # пауз не было: last_call_at пуст, потом pause 0
|
||||
|
||||
def test_restart_pace_from_old_finished(self):
|
||||
at = (datetime.now(timezone.utc) - timedelta(seconds=10)).replace(microsecond=0).isoformat()
|
||||
self.journal.append({"kind": "finished", "command_id": 99, "status": "done", "result": None, "error": None,
|
||||
"jira_calls": 1, "rate_limited": 0, "at": at})
|
||||
self.journal.append({"kind": "acked", "command_id": 99, "at": at})
|
||||
self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}, pause=30)
|
||||
self.runner.loop()
|
||||
self.assertAlmostEqual(sum(self.sleeps), 20, delta=1.5)
|
||||
|
||||
def test_future_journal_time_ignored(self):
|
||||
at = (datetime.now(timezone.utc) + timedelta(hours=1)).replace(microsecond=0).isoformat()
|
||||
self.journal.append({"kind": "acked", "command_id": 99, "at": at})
|
||||
self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}, pause=30)
|
||||
self.runner.loop()
|
||||
self.assertEqual(self.sleeps, []) # время из будущего — как пустой журнал
|
||||
self.assertEqual(self.hub.results[1]["status"], "done")
|
||||
|
||||
def test_restore_pace_keeps_existing_last_call(self):
|
||||
at = (datetime.now(timezone.utc) - timedelta(seconds=100)).replace(microsecond=0).isoformat()
|
||||
self.journal.append({"kind": "acked", "command_id": 99, "at": at})
|
||||
mark = time.monotonic()
|
||||
self.runner.jira.last_call_at = mark
|
||||
self.runner.restore_pace()
|
||||
self.assertEqual(self.runner.jira.last_call_at, mark)
|
||||
|
||||
def test_pause_waits_from_last_jira_call(self):
|
||||
self.runner.jira.last_call_at = time.monotonic()
|
||||
self.hub.enqueue("ensure_fields", {"key": "AURORA-1", "duedate": "2026-10-23"}, pause=3)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user