feat(hands,jobs): watchdog ring with a neighbour agent, daily heartbeat and silence alarm
This commit is contained in:
1 parent
0475e09c81
commit
f6cb17acaf
9 files changed
+1029
No files matched your search
@@ -0,0 +1,113 @@
|
||||
"""Сторожевое кольцо с чужим агентом: своя отметка и слежка за его молчанием.
|
||||
|
||||
Два джоба, оба раз в сутки и оба в отдельной комнате вайбграма (второй
|
||||
экземпляр клиента, `VIBEGRAM_HB`):
|
||||
|
||||
- `beat` печатает `hb <ник> at=… boot=… every=… src=…`. Период не константа:
|
||||
он выведен из крона ЭТОГО ЖЕ джоба (`run.job.cron`) - что зарегистрировано
|
||||
у планировщика, то и написано в строке. Не сводится к «раз в N часов» -
|
||||
пишем `every=unknown`, потому что соврать числом хуже, чем отказаться;
|
||||
- `watch` читает окно комнаты, сверяет возраст последней отметки соседа с
|
||||
порогом из ЕГО ЖЕ строки, печатает в комнату `ack` и - если цепь молчит -
|
||||
отдаёт тревогу мастеру через `run.spawn_job`, как это делает триаж
|
||||
вайбграма: будить ли владельца, решает диспетчер.
|
||||
|
||||
Токена сторожевой комнаты нет - оба джоба молча ничего не делают, а о том,
|
||||
что сторож не сконфигурирован, один раз сказано в логе при старте
|
||||
(`beaver_agent/hands/__init__.py`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from beaver_agent.hands import VIBEGRAM_HB
|
||||
from beaver_agent.hands.heartbeat import (
|
||||
BOOT,
|
||||
PEER,
|
||||
WINDOW,
|
||||
Watch,
|
||||
alarm_text,
|
||||
beat_line,
|
||||
every_from_cron,
|
||||
look,
|
||||
)
|
||||
from beaver_agent.vault import STATE_DIR
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver_gateway.jobs.scheduler import JobRun
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
STATE_FILE = STATE_DIR / "сторож.json"
|
||||
"""Что видели в прошлый прогон: без файла сторож после передеплоя считает,
|
||||
что соседа не видел ни разу, и разоружается сам по себе."""
|
||||
|
||||
TITLE = "сторож"
|
||||
|
||||
BRIEF = (
|
||||
"Сторожевое кольцо с чужим агентом {peer} (взаимные отметки раз в сутки "
|
||||
"в отдельной комнате вайбграма) подняло тревогу.\n"
|
||||
"{body}\n"
|
||||
'Будить владельца - inject(conversation="master", urgency="wake", '
|
||||
"text=до 3 строк: что цепь молчит, сколько молчит и какие три причины). "
|
||||
"Это не авария у нас: наша половина работает, молчит чужая. "
|
||||
"Если inject вернул ошибку - это сбой доставки, а не «не срочно»: "
|
||||
"повтори один раз с тем же текстом."
|
||||
)
|
||||
|
||||
|
||||
async def beat(run: JobRun) -> None:
|
||||
"""Отметка о том, что мы живы. Не ушла - джоб падает, это видно в прогонах."""
|
||||
if VIBEGRAM_HB is None:
|
||||
return
|
||||
line = beat_line(
|
||||
nick=VIBEGRAM_HB.nick,
|
||||
at=datetime.now(UTC),
|
||||
boot=BOOT,
|
||||
every=every_from_cron(run.job.cron, run.scheduler.tz),
|
||||
)
|
||||
await VIBEGRAM_HB.say(line)
|
||||
_log.info("сторож: отметка ушла - %s", line)
|
||||
|
||||
|
||||
async def watch(run: JobRun) -> None:
|
||||
"""Прогон сторожа: окно → решение → `ack` в комнату → тревога, если молчит."""
|
||||
if VIBEGRAM_HB is None:
|
||||
return
|
||||
try:
|
||||
events = await VIBEGRAM_HB.pending(limit=WINDOW)
|
||||
except Exception: # noqa: BLE001 - любой отказ хаба это отсутствие показания
|
||||
# Окно не прочитано - показания нет вовсе, и тревога тут запрещена
|
||||
# ровно так же, как на съехавшем окне: молчание хаба это не молчание
|
||||
# соседа. Говорим об этом в лог, состояние не трогаем.
|
||||
_log.exception("сторож: комната не прочитана, прогон без показания")
|
||||
return
|
||||
state = Watch.load(STATE_FILE)
|
||||
report = look(
|
||||
state,
|
||||
events,
|
||||
now=datetime.now(UTC),
|
||||
res=every_from_cron(run.job.cron, run.scheduler.tz),
|
||||
)
|
||||
sent = True
|
||||
try:
|
||||
await VIBEGRAM_HB.say(report.ack)
|
||||
except Exception as exc: # noqa: BLE001 - своя запись не прошла, это различитель
|
||||
sent = False
|
||||
_log.warning("сторож: свой ack в комнату не ушёл: %s", exc)
|
||||
_log.info("сторож: %s | %s", report.ack, report.note)
|
||||
if report.alarm is not None:
|
||||
await run.spawn_job(
|
||||
agent="beaver-triage",
|
||||
title=TITLE,
|
||||
text=BRIEF.format(peer=PEER, body=alarm_text(report, mine_sent=sent)),
|
||||
)
|
||||
now = datetime.now(UTC)
|
||||
if report.alarm == "silence":
|
||||
state.alerted = now
|
||||
else:
|
||||
state.unarmed_told = True
|
||||
state.save()
|
||||
Reference in new issue
Block a user