telegram: сервис поднимается без бота и работает одним входом
- Клиент бота собирается один раз и достаётся отправителю и транспорту; разрез прошёл по «ответил ли Telegram»: ответ «такого бота нет» роняет старт, недоступность даёт подъём без Telegram (ADR-2026-08-13). Ожидание при сборке ограничено сроком — иначе молчащий Telegram вешал подъём. - Недоставленный ответ не роняет шаг: пишется с job_id и считается метрикой, уровень по причине — WARN для неподнятого входа, ERROR для неназванного адресата. Заведены transcriber_intake_up и transcriber_undelivered_reply_count. - Закрыта утечка токена в журнал: отказ разбора адреса рождается раньше обращения к клиенту, то есть мимо чистки на его границе.
This commit is contained in:
@@ -551,19 +551,43 @@ func (s *TranscribeService) failJob(job *entity.TranscribeJob, holder string, jo
|
||||
return s.send(job, errorMessage)
|
||||
}
|
||||
|
||||
// send отвечает отправителю там, откуда пришла запись, и отказ отправки
|
||||
// поднимает вверх: он принадлежит шагу.
|
||||
// send отвечает отправителю там, откуда пришла запись. Отказ отправки поднимает
|
||||
// вверх: он принадлежит шагу.
|
||||
//
|
||||
// Кроме недоставки — её шаг записывает и завершается без отказа. Ответ уходит
|
||||
// после того, как достигнутое состояние сохранено: работа к этой минуте
|
||||
// сделана, и объявленный отказ засчитался бы воркеру сбоем и лёг бы владельцу
|
||||
// записью отказа. Повтор делу не помогает — ни бот, ни адресат от ожидания не
|
||||
// появятся, — поэтому причина недоставки живёт в журнале, а не в состоянии
|
||||
// задачи.
|
||||
//
|
||||
// Служебные поля завершённой задачи отказ бы при этом не переписал: переход в
|
||||
// терминальное состояние снимает захват, и повторная запись натыкается на
|
||||
// «захват потерян». Довод держится на счётчике и журнале, а не на этом.
|
||||
func (s *TranscribeService) send(job *entity.TranscribeJob, text string) error {
|
||||
if job.Source != entity.SourceTelegram {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Адресата у задачи нет: отвечать некуда, и повторять нечего. Уровень здесь
|
||||
// выше, чем у неподнятого канала, и это не педантизм: пустой чат у задачи
|
||||
// из Telegram — симптом порчи записи, а самый коварный её источник назван
|
||||
// инвариантом «колонки очереди правятся в четырёх местах». Утони этот
|
||||
// сигнал в одном ряду со штатным «бот не настроен» — и обнуление колонки
|
||||
// заметит только отправитель, переставший получать ответы.
|
||||
if job.TgChatId == nil {
|
||||
s.logger.Error("Telegram chat not specified", "job_id", job.Id)
|
||||
return fmt.Errorf("tg chat id not specified, job id: %s", job.Id)
|
||||
s.undelivered(job, slog.LevelError, "chat is not specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.tgSender.Send(text, *job.TgChatId, job.TgReplyMessageId); err != nil {
|
||||
// Канал не поднят: сервис работает без этого входа, и это объявленный
|
||||
// режим, а не поломка.
|
||||
if errors.Is(err, contract.ErrDeliveryChannelDown) {
|
||||
s.undelivered(job, slog.LevelWarn, "delivery channel is down")
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Error("Failed to sent message to client", "job_id", job.Id)
|
||||
return fmt.Errorf("failed to sent message to client, job id: %s, err: %w", job.Id, err)
|
||||
}
|
||||
@@ -571,6 +595,31 @@ func (s *TranscribeService) send(job *entity.TranscribeJob, text string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// undelivered записывает недоставленный ответ и считает его в метрику. Уровень
|
||||
// приходит от причины: объявленный режим — «может стать проблемой», порча
|
||||
// записи — событие для разбора.
|
||||
//
|
||||
// Идентификатор задачи обязателен, иначе владелец видит, что ответ не ушёл, но
|
||||
// не может найти, чей; текста ответа в записи нет — он содержимое чужой записи.
|
||||
//
|
||||
// Счётчик нужен потому, что журнал контейнера живёт до ротации, а вопрос «кому
|
||||
// не ответили за последние сутки» задают позже.
|
||||
func (s *TranscribeService) undelivered(job *entity.TranscribeJob, level slog.Level, reason string) {
|
||||
metrics.UndeliveredReplyCounter.WithLabelValues(reason).Inc()
|
||||
|
||||
// Уровень выбирается ветвлением, а не передачей контекста: контекст здесь
|
||||
// брать неоткуда — ответ идёт после сохранения состояния, — а выдуманный
|
||||
// `context.Background()` соврал бы про отмену и цеплялся бы правилами.
|
||||
switch level {
|
||||
case slog.LevelError:
|
||||
s.logger.Error(undeliveredMessage, "job_id", job.Id, "reason", reason)
|
||||
default:
|
||||
s.logger.Warn(undeliveredMessage, "job_id", job.Id, "reason", reason)
|
||||
}
|
||||
}
|
||||
|
||||
const undeliveredMessage = "Reply was not delivered"
|
||||
|
||||
// notify отвечает отправителю там, где поднимать отказ некуда: задача уже
|
||||
// доведена до конца, и отказ отправки остаётся записью в журнале владельца.
|
||||
func (s *TranscribeService) notify(job *entity.TranscribeJob, text string) {
|
||||
|
||||
Reference in New Issue
Block a user