114 lines
5.3 KiB
Python
114 lines
5.3 KiB
Python
"""Сторожевое кольцо с чужим агентом: своя отметка и слежка за его молчанием.
|
|
|
|
Два джоба, оба раз в сутки и оба в отдельной комнате вайбграма (второй
|
|
экземпляр клиента, `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()
|