Files

138 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Сторожевое кольцо с чужим агентом: своя отметка и слежка за его молчанием.
Два джоба, оба раз в сутки и оба в отдельной комнате вайбграма (второй
экземпляр клиента, `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()