138 lines
6.9 KiB
Python
138 lines
6.9 KiB
Python
"""Сторожевое кольцо с чужим агентом: своя отметка и слежка за его молчанием.
|
||
|
||
Два джоба, оба раз в сутки и оба в отдельной комнате вайбграма (второй
|
||
экземпляр клиента, `VIBEGRAM_HB`):
|
||
|
||
- `beat` печатает `hb <ник> at=… boot=… every=… src=…`. Период не константа:
|
||
он выведен из крона ЭТОГО ЖЕ джоба (`run.job.cron`) - что зарегистрировано
|
||
у планировщика, то и написано в строке. Не сводится к «раз в N часов» -
|
||
пишем `every=unknown`, потому что соврать числом хуже, чем отказаться.
|
||
В день, помеченный в `DRILL_FILE`, отметка не уходит: так проверяют, что
|
||
тревога на той стороне жива, а не только что она молчит на живой цепи;
|
||
- `watch` читает окно - последние `WINDOW` сообщений САМОЙ комнаты, а не
|
||
«что нового с курсора», - сверяет возраст последней отметки соседа с
|
||
порогом из ЕГО ЖЕ строки, печатает в комнату `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,
|
||
drill,
|
||
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"
|
||
"""Что видели в прошлый прогон: без файла сторож после передеплоя считает,
|
||
что соседа не видел ни разу, и разоружается сам по себе."""
|
||
|
||
DRILL_FILE = STATE_DIR / "сторож-учения.txt"
|
||
"""Даты учений, по одной `YYYY-MM-DD` на строку, локальные в зоне планировщика.
|
||
В такой день `beat` молчит в комнате - в этом и учения: проверяем, что тревога
|
||
у соседа действительно срабатывает. Файл, а не константа и не переменная среды:
|
||
даты меняются от учений к учениям, а передеплой ради них не нужен."""
|
||
|
||
TITLE = "сторож"
|
||
|
||
BRIEF = (
|
||
"Сторожевое кольцо с чужим агентом {peer} (взаимные отметки раз в сутки "
|
||
"в отдельной комнате вайбграма) подняло тревогу.\n"
|
||
"{body}\n"
|
||
'Будить владельца - inject(conversation="master", urgency="wake", '
|
||
"text=до 3 строк: что цепь молчит, сколько молчит и какие три причины). "
|
||
"Это не авария у нас: наша половина работает, молчит чужая. "
|
||
"Если inject вернул ошибку - это сбой доставки, а не «не срочно»: "
|
||
"повтори один раз с тем же текстом."
|
||
)
|
||
|
||
|
||
async def beat(run: JobRun) -> None:
|
||
"""Отметка о том, что мы живы. Не ушла - джоб падает, это видно в прогонах.
|
||
|
||
В день из `DRILL_FILE` отметка не уходит вовсе: учения - это МОЛЧАНИЕ в
|
||
комнате, и любой текст туда («сегодня пропускаем») их бы и отменил. Видно
|
||
такой прогон только в логе.
|
||
"""
|
||
if VIBEGRAM_HB is None:
|
||
return
|
||
now = datetime.now(UTC)
|
||
if drill(DRILL_FILE, now=now, tz=run.scheduler.tz):
|
||
_log.info(
|
||
"сторож: отметка пропущена по флагу учений %s (%s)",
|
||
DRILL_FILE,
|
||
now.astimezone(run.scheduler.tz).strftime("%Y-%m-%d"),
|
||
)
|
||
return
|
||
line = beat_line(
|
||
nick=VIBEGRAM_HB.nick,
|
||
at=now,
|
||
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:
|
||
window = await VIBEGRAM_HB.window(WINDOW)
|
||
except Exception: # noqa: BLE001 - любой отказ хаба это отсутствие показания
|
||
# Окно не прочитано - показания нет вовсе, и тревога тут запрещена
|
||
# ровно так же, как на съехавшем окне: молчание хаба это не молчание
|
||
# соседа. Говорим об этом в лог, состояние не трогаем.
|
||
_log.exception("сторож: комната не прочитана, прогон без показания")
|
||
return
|
||
state = Watch.load(STATE_FILE)
|
||
report = look(
|
||
state,
|
||
window.events,
|
||
now=datetime.now(UTC),
|
||
res=every_from_cron(run.job.cron, run.scheduler.tz),
|
||
full=window.full,
|
||
)
|
||
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()
|