fix(agent): один процесс на журнал — блокировка journal.jsonl.lock; README: остановить старый агент перед запуском (I-1, #9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a95f7d936c
commit
058a78b0d6
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ data.json
|
||||
tests/fixtures/*.m4a
|
||||
tests/fixtures/*.webm
|
||||
.DS_Store
|
||||
journal.jsonl.lock
|
||||
|
||||
@ -170,7 +170,10 @@ python3 agent.py prune
|
||||
|
||||
## Обновление агента
|
||||
|
||||
Скопировать новый `agent.py` поверх старого и перезапустить процесс (или `launchctl unload` + `load`).
|
||||
Остановить старый процесс (Ctrl-C или `kill <pid>`, дождаться «остановлен»), скопировать новый `agent.py` поверх
|
||||
старого и запустить снова (или `launchctl unload` + `load`). Два процесса с одним журналом работать не могут: хаб
|
||||
довыдаёт команду без отчёта, и второй процесс выполнял бы её одновременно с первым. Начиная с версии 3 второй процесс
|
||||
не стартует («агент уже запущен…», код 1) — блокировка `journal.jsonl.lock` рядом с журналом.
|
||||
`agent.json` и `journal.jsonl` не трогать: журнал переживает обновление, а неподтверждённые отчёты досылаются при старте.
|
||||
|
||||
## Известные ограничения / эксплуатация
|
||||
|
||||
24
src/agent.py
24
src/agent.py
@ -12,6 +12,7 @@ Python 3.9. Спека: jira-hub/openspec/specs/agent/spec.md.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import fcntl
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
@ -1118,7 +1119,30 @@ class Runner:
|
||||
self.sleep(self.poll)
|
||||
|
||||
|
||||
def acquire_lock(journal_path: str):
|
||||
"""Исключительная блокировка <журнал>.lock: один процесс агента на журнал. Хаб довыдаёт команду без отчёта при
|
||||
следующем claim, поэтому второй процесс выполнял бы ту же команду одновременно с первым. None — уже занято."""
|
||||
f = open(journal_path + ".lock", "a")
|
||||
try:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
f.close()
|
||||
return None
|
||||
return f
|
||||
|
||||
|
||||
def cmd_run(cfg: dict, journal_path: str) -> int:
|
||||
lock = acquire_lock(journal_path)
|
||||
if lock is None:
|
||||
log("агент уже запущен с журналом %s — второй процесс не стартует" % journal_path)
|
||||
return 1
|
||||
try:
|
||||
return _run(cfg, journal_path)
|
||||
finally:
|
||||
lock.close()
|
||||
|
||||
|
||||
def _run(cfg: dict, journal_path: str) -> int:
|
||||
runner = Runner(cfg, HubClient(cfg), JiraClient(cfg), Journal(journal_path))
|
||||
|
||||
def on_signal(signum, frame):
|
||||
|
||||
@ -17,7 +17,7 @@ class Py39Test(unittest.TestCase):
|
||||
def test_no_third_party_imports(self):
|
||||
with open(os.path.join(SRC, "agent.py"), encoding="utf-8") as f:
|
||||
tree = ast.parse(f.read())
|
||||
allowed = {"argparse", "base64", "json", "os", "signal", "ssl", "http", "sys", "time", "urllib", "datetime", "typing", "uuid"}
|
||||
allowed = {"argparse", "base64", "json", "os", "signal", "ssl", "http", "sys", "time", "urllib", "datetime", "typing", "uuid", "fcntl"}
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
|
||||
@ -428,5 +428,21 @@ class RunnerTest(unittest.TestCase):
|
||||
self.assertEqual(seen["code"], 130)
|
||||
|
||||
|
||||
def test_second_process_refused(self):
|
||||
"""I-1 финального ревью: второй run с тем же журналом не стартует — иначе довыдача хаба отдаст ему ту же команду."""
|
||||
from unittest import mock
|
||||
held = agent.acquire_lock(self.journal.path)
|
||||
self.assertIsNotNone(held)
|
||||
try:
|
||||
self.assertIsNone(agent.acquire_lock(self.journal.path))
|
||||
cfg = jira_cfg(self.fake, hub_url=self.hub.base_url, hub_token=self.hub.token)
|
||||
with mock.patch.object(agent.Runner, "loop", lambda r: self.fail("цикл не должен стартовать")):
|
||||
self.assertEqual(agent.cmd_run(cfg, self.journal.path), 1)
|
||||
finally:
|
||||
held.close()
|
||||
again = agent.acquire_lock(self.journal.path)
|
||||
self.assertIsNotNone(again) # после выхода первого — снова можно
|
||||
again.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user