Files
transcriber/openspec/specs/pipeline/spec.md
T
av 8f7c3a057a удалён вход Telegram, владелец записи стал обязателен в схеме
- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка
  входа при старте, секция настроек и зависимость go-telegram-bot-api; из
  конвейера ушла доставка ответа отправителю — исход виден опросом готовности.
  Колонки адресата и значение источника остались в схеме: применённые шаги не
  переписываются
- шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла;
  существующие строки он не проверяет, и это принято сознательно — искать их
  надо запросом до выкладки
- ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ
  распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала
  быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image
  до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
2026-08-15 07:24:35 +03:00

49 KiB

pipeline Specification

Purpose

Конвейер расшифровки: как аудиозапись движется по рубежам, что делает воркер, когда работы нет, и что считается отказом шага.

Описаны цепочка рубежей и смысл рубежа, остановка признаком и её причины, оба сторожа — число отказов и время в рубеже, — откладывание работы отдельно от перехода, неделимость захвата и срок его протухания, условие записи результата держателем захвата, нарастающая пауза перед повтором, число воркеров настройкой, журнал событий записи и молчание конвейера наружу: обращений к отправителю он не делает вовсе, и свой исход тот узнаёт опросом готовности.

Сознательно не описаны: освобождение ресурсов внешних клиентов и какие отказы считаются приговором записи, а какие поводом к повтору. Второе — не пробел формулировки, а неразобранный вопрос: сегодня отказ приведения останавливает запись с первой попытки, и предел отказов на нём не работает никогда. Это не значит, что поведения нет: оно живёт в коде, а требования на него не написаны, потому что требование без проверки — предположение, а не норма. Первая задача, которая трогает любое из перечисленного, дописывает его сюда.

Requirements

Requirement: Пустой прогон воркера — не отказ

Воркер SHALL отличать «пригодной к работе записи сейчас нет» от отказа шага. На пустом прогоне он MUST не считать прогон отказом: не увеличивать счётчик работы и не писать о нём на уровне владельца сервиса. Признак пустого прогона MUST узнаваться по смыслу значения, а не по его точной форме, и MUST переживать пояснения, добавленные к этому значению на любом промежуточном шаге пути.

Формулировка сменилась вместе с моделью: воркер больше не привязан к рубежу и опрашивает не «своё состояние», а очередь целиком, поэтому пустой прогон значит «работы нет ни на одном рубеже», а не «работы нет в этом состоянии».

Требование стоит на инварианте проекта «NoopJobError — не ошибка»: воркеры опрашивают базу раз в секунду, и пустой прогон, принятый за отказ, даёт от каждого запись отказа в секунду и столько же засчитанных сбоев, которых не было.

Признак пустого прогона MUST рождаться только ответом хранилища на опрос этим же шагом. Слой, придающий отказу собственный смысл, MUST не сохранять чужой признак в цепочке своей ошибки. Воркер узнаёт признак по смыслу на любой глубине, поэтому отказ, к которому признак примешался, тоже зачёл бы пустым прогоном: запись осталась бы на своём рубеже и переопрашивалась раз в секунду без единой записи — ровно то, что запрещает инвариант «Принятая запись не теряется молча».

Отказ шага, наоборот, MUST быть виден владельцу сервиса записью в журнале и MUST быть засчитан в счётчик работы с пометкой отказа и с меткой рубежа.

Сколько раз он записывается и каким уровнем — это требование не нормирует, и умолчанием тут считать нечего. Сегодня один отказ даёт две записи: пишет шаг конвейера и следом воркер, — а уровень стоит ERROR там, где конвенция просит WARN для повторяющегося сбоя фонового цикла. И то и другое записано долгом в docs/conventions/logging.md, раздел «Ошибки», строкой «Расхождение, и оно системное». Долгом оно и остаётся: требование, объявившее одиночную запись нормой, сделало бы недостижимое обязательным, а требование, объявившее нормой двойную, — закрыло бы долг контрактом. Задача, которая возьмётся за этот долг, дописывает норму сюда.

Scenario: Пригодной к работе записи нет

  • GIVEN ни одной записи, пригодной к работе, нет ни на одном рубеже
  • WHEN воркер делает свой прогон
  • THEN на уровне владельца сервиса об этом прогоне не пишется ничего
  • AND счётчик работы воркера не растёт

Scenario: Признак пустого прогона дошёл с пояснением

  • GIVEN пригодной к работе записи нет
  • AND промежуточный шаг добавил к этому признаку своё пояснение
  • WHEN воркер делает свой прогон
  • THEN прогон по-прежнему считается пустым: счётчик не растёт, записи на уровне владельца нет

Scenario: Шаг отказал

  • GIVEN шаг конвейера вернул отказ
  • WHEN воркер завершает прогон
  • THEN отказ виден владельцу сервиса записью в журнале
  • AND счётчик работы воркера растёт с пометкой отказа и меткой рубежа

Scenario: Шаг сделал работу

  • GIVEN шаг конвейера отработал запись без отказа
  • WHEN воркер завершает прогон
  • THEN счётчик работы воркера растёт с пометкой успеха
  • AND записи об отказе в журнале нет

Requirement: Захват задачи неделим

Захват записи воркером SHALL быть одним неделимым шагом хранилища: выбор подходящей записи и пометка её захваченной MUST происходить вместе.

Захват MUST возвращать идентификатор записи и признак этого захвата, а не перечень её колонок. Колонки записи шаг читает сам, обычным чтением. Иначе всякая новая колонка аудиозаписи попадала бы под инвариант проекта о колонках очереди, и забытая в захвате колонка приезжала бы нулевой, а первое же сохранение писало бы этот ноль поверх сохранённого значения.

Признак захвата MUST быть значением, уникальным для каждого захвата, а не признаком занятости. Условие записи результата сверяет именно это значение: захват, перевыданный другому — по протуханию срока или после того, как человек снял признак остановки в панели, — обязан обращать запись первого в отказ. Условие, проверяющее лишь непустоту признака или срок, пропустило бы обоих, и два шага записали бы в одну запись по очереди, испортив её результат.

Одна и та же запись MUST доставаться ровно одному захватившему. Двум вызывающим, пришедшим за работой одновременно, запись MUST достаться одному, а второй MUST получить признак «работы сейчас нет».

Срок протухания захвата MUST ехать с рубежом записи, а не с воркером: воркер не привязан к шагу и не знает заранее, что вытянет. Срок MUST записываться числом при самом захвате.

Порядок выборки MUST быть определён однозначно: сравнения по неуникальному значению для этого мало, и к нему MUST добавляться ключ записи. Иначе порядок обработки невоспроизводим, а проверка, опирающаяся на «следующую» запись, зелена через раз.

Требование стоит на инварианте проекта «Принятая запись не теряется молча»: захват, разделённый на два шага, отдаёт одну запись двум воркерам, и работа одного из них теряется без следа.

Признак «работы нет» этим требованием не переопределяется — его нормирует требование «Пустой прогон воркера — не отказ».

Scenario: За работой пришли трое разом

  • GIVEN к работе пригодна ровно одна запись
  • WHEN три захвата идут одновременно
  • THEN запись получает ровно один из них
  • AND двое остальных получают признак «работы сейчас нет»

Scenario: Захваченная запись не выдаётся второй раз

  • GIVEN запись захвачена и срок захвата не истёк
  • WHEN приходит следующий захват
  • THEN эта запись ему не выдаётся

Scenario: Захват отдаёт идентификатор и свой признак

  • GIVEN к работе пригодна запись
  • WHEN воркер её захватывает
  • THEN захват возвращает идентификатор записи и признак этого захвата
  • AND колонки записи шаг читает отдельным чтением

Scenario: Признак перевыданного захвата отличается от прежнего

  • GIVEN запись захвачена, и признак первого захвата известен
  • WHEN человек снимает признак остановки, и запись захватывает другой воркер
  • THEN признак нового захвата отличается от признака первого

Requirement: Результат пишет только держатель захвата

Шаг конвейера SHALL записывать свой результат только тогда, когда захват записи всё ещё принадлежит ему. Запись MUST быть условна по признаку этого захвата — значению, уникальному для каждого захвата, — а не по занятости записи вообще. Шаг, чей захват за время работы достался другому, MUST завершиться без записи результата.

Требование закрывает то, чего неделимость захвата не закрывает: захват протухает не только у мёртвого воркера, но и у живого — шаг, идущий дольше своего срока, теряет запись, продолжая работать. Снять захват может и человек, вернувший остановленную запись в работу. Без условия по уникальному признаку два воркера пишут в одну запись по очереди, а счётчик отказов сбрасывает тот, кто уже не владелец.

Довод про два ответа отправителю из требования ушёл вместе с доставкой: обращений наружу шаг не делает. Требование от этого не ослабло — порча записи двумя пишущими остаётся его предметом целиком.

Шаг MUST записывать только те поля, которыми распоряжается сам. Запись он держит снимком с момента захвата и до записи — это часы, — и безусловная запись снимка стёрла бы всё, что владелец правил в панели за это время: молча, без строки в журнале и без отказа в панели. Владелец увидел бы успешное сохранение и был бы уверен, что правка на месте. Владелец записи, заголовок, краткое описание и темы конвейер MUST не трогать.

Scenario: Правка владельца пережила сохранение шага

  • GIVEN шаг держит захваченную запись
  • AND владелец за это время изменил в панели поле, которого шаг не касается
  • WHEN шаг записывает свой результат
  • THEN результат шага записан
  • AND правка владельца на месте

Scenario: Захват ушёл под работающим шагом

  • GIVEN шаг работает над захваченной записью
  • AND за это время та же запись досталась другому захвату
  • WHEN первый шаг доходит до записи результата
  • THEN результат не записывается

Scenario: Человек снял остановку под работающим шагом

  • GIVEN шаг работает над захваченной записью
  • AND человек за это время снял с неё признак остановки, освободив захват
  • AND запись досталась другому воркеру
  • WHEN первый шаг доходит до записи результата
  • THEN результат не записывается

Requirement: Брошенная задача возвращается в работу

Запись, захваченная и брошенная на середине, SHALL доставаться снова по истечении срока захвата. Срок MUST считаться от времени захвата, а истёкший захват MUST не мешать выдать запись следующему.

Срок задаётся рубежом, с которого запись взята, и MUST быть не меньше того времени, которое шаг этого рубежа может занять на самом длинном допустимом входе. Срок короче делает протухание штатным событием живого шага, а не признаком беды. Срок MUST записываться в саму запись при захвате: воркер шага не знает и вывести срок из себя не может.

Все значения времени, по которым идёт этот отбор, MUST записываться и сравниваться в одном виде — том же, в каком хранилище пишет собственные времена записи. Сравнение идёт побайтово, и вид, разошедшийся хоть разделителем, обращает условие в постоянную истину или постоянную ложь, причём молча.

Scenario: Захват протух

  • GIVEN запись захвачена, а время захвата отстоит дальше срока
  • WHEN приходит захват
  • THEN запись выдаётся ему

Scenario: Срок сравнивается с временем, записанным хранилищем

  • GIVEN запись захвачена, и время захвата записано в том же виде, в каком хранилище пишет время изменения записи
  • WHEN приходит захват до истечения срока
  • THEN запись ему не выдаётся

Scenario: Срок протухания приехал с рубежом

  • GIVEN записи двух рубежей с разными сроками захвата пригодны к работе
  • WHEN их захватывает один и тот же воркер
  • THEN у каждой записан срок её рубежа

Requirement: Пауза перед повтором нарастает

Перед повтором отказавшей записи сервис SHALL выдерживать паузу, и пауза MUST расти с числом её отказов до объявленного потолка. Запись MUST не выдаваться захвату, пока пауза не кончилась.

Ожидание чужой операции этой паузой MUST не выражаться. Шаг, увидевший, что внешняя операция ещё идёт, отработал без отказа: он откладывает работу своей задержкой, заданной числом, и отказов при этом не тратит. Пауза, выведенная из числа отказов, на таком шаге вырождается в наименьшее своё значение и учащает опрос внешнего сервиса во столько раз, во сколько задержка опроса длиннее секунды.

Scenario: Отказавшая запись ждёт

  • GIVEN запись отказала на шаге конвейера
  • WHEN захват приходит раньше конца её паузы
  • THEN запись ему не выдаётся

Scenario: Вторая пауза длиннее первой

  • GIVEN запись отказала дважды подряд
  • WHEN сравнивают паузу после второго отказа с паузой после первого
  • THEN вторая длиннее

Scenario: Ожидание операции не учащается и не тратит отказов

  • GIVEN внешняя операция распознавания ещё идёт
  • WHEN шаг опроса отрабатывает подряд несколько раз
  • THEN задержка до следующей проверки каждый раз одна и та же
  • AND число отказов записи не растёт

Requirement: Выборка воркера владельцем не сужается

Воркер SHALL брать записи всех владельцев подряд и MUST не учитывать владельца при выборе очередной записи.

Владелец решает, кому запись показывать, а не кому её считать. Сужение выборки владельцем поставило бы записи одних людей в зависимость от того, кто первым завёл учётную запись.

Оговорка про записи без владельца из требования ушла: заводить их стало нечем — колонка владельца пустого значения не принимает, и норму держит capability storage.

Владелец записи MUST переживать работу конвейера: шаг, сохраняющий свой результат, владельца не трогает и не затирает.

Scenario: Записи двух владельцев проходят одним воркером

  • GIVEN заведены записи двух разных владельцев на одном рубеже
  • WHEN воркер забирает работу
  • THEN ему достаются обе, в порядке заведения

Scenario: Шаг конвейера владельца не затирает

  • GIVEN запись с владельцем прошла шаг конвейера
  • WHEN шаг сохраняет свой результат
  • THEN владелец записи остаётся прежним

Requirement: Рубеж записи называет достигнутое

Аудиозапись SHALL нести рубеж — состояние, называющее достигнутое, а не предстоящее. Цепочка рубежей: uploaded, normalized, submitted, transcribed, done. Какой шаг делать дальше, сервис MUST выбирать по рубежу одним общим местом, а не тем, какой воркер пришёл за записью.

Прежние состояния называли предстоящую работу (created, converted, transcribe), и потому по состоянию нельзя было сказать, что с записью уже сделано: продолжить с места остановки было не с чего.

Конечный рубеж MUST зваться done. Доставка ответа отправителю в конвейер не входит, и слово описывает пройденный конвейер, а не полученный человеком текст.

Перечень рубежей, из которых запись берётся в работу, MUST выводиться из одного объявления рубежа, а не перечисляться отдельно каждым потребителем. Рубеж, забытый в отборе захвата, не выдаётся ни одному воркеру никогда, а пустой прогон по инварианту проекта не пишется в журнал и не считается в метрику: запись встала бы без единого следа. Потребителей у перечня больше двух — выбор шага, отбор захвата, срок захвата, предел времени, закрытый перечень значений в схеме, — и человеческая сверка между ними не механизируема.

Записи на конечном рубеже MUST не браться в работу и MUST не подпадать под предел времени в рубеже: done не ждёт работы, и стоять в нём запись будет вечно по построению.

Scenario: Рубеж называет сделанное

  • GIVEN запись прошла приведение к рабочему формату
  • WHEN смотрят её рубеж
  • THEN он называет приведение сделанным, а не предстоящим

Scenario: Следующий шаг выбирается по рубежу

  • GIVEN запись стоит на рубеже приведения
  • WHEN её берёт воркер
  • THEN идёт отправка на распознавание, а не повторное приведение

Scenario: Запись на конечном рубеже не берут и не останавливают

  • GIVEN запись стоит на конечном рубеже дольше любого предела
  • WHEN приходит захват
  • THEN запись ему не выдаётся
  • AND признака остановки у неё не появляется

Requirement: Остановка записи — признак, а не рубеж

Сервис SHALL останавливать запись отдельным признаком с причиной и MUST не стирать при этом достигнутый рубеж. Признак MUST нести время остановки, причину и машинный текст отказа.

Снятие признака SHALL возвращать запись в работу с того рубежа, где она стояла, и MUST сбрасывать число отказов, паузу и время входа в рубеж. Время входа сбрасывается по той же причине, что и остальные сторожа: запись, простоявшая остановленной дольше предела, иначе останавливалась бы снова первым же захватом, и перезапуск не работал бы вовсе.

Прежние состояния отказа и смерти MUST не заводиться заново: обе причины восстанавливаются одинаково — снятием признака, — и различие между ними перестаёт быть структурным, оставаясь причиной остановки. Состояние, называющее отказ, стирает достигнутый рубеж, и продолжение с места остановки становится невозможным.

Способ вывести запись из выборки MUST быть один — этот признак. Второго признака, исключающего запись из работы помимо рубежа и паузы, MUST не заводиться: два способа расходятся, и молчаливо теряется тот, который забыли проверить. Условие отбора MUST не выводить запись из выборки молча — запись, переставшая браться в работу, обязана нести признак остановки с причиной.

Остановку MUST ставить тот, кто запись захватил. Перевод принадлежит одному месту: условие отбора, молча пропускающее запись мимо выборки, оставило бы её без следа.

Остановленная запись MUST не выдаваться захвату.

Scenario: Остановленная запись продолжает с места остановки

  • GIVEN шаг остановил запись на рубеже приведения
  • WHEN признак остановки снимают
  • THEN следующим идёт отправка на распознавание, а не повторное приведение

Scenario: Остановленная запись не выдаётся захвату

  • GIVEN у записи стоит признак остановки
  • WHEN за её рубежом приходит захват
  • THEN запись ему не выдаётся

Scenario: Снятие признака сбрасывает всех сторожей

  • GIVEN запись остановлена с накопленными отказами и паузой
  • AND остановленной она простояла дольше предела времени в рубеже
  • WHEN признак остановки снимают
  • THEN число отказов, пауза и время входа в рубеж сброшены
  • AND ближайший захват выдаёт запись, а не останавливает её снова

Requirement: Время в рубеже ограничено

У аудиозаписи SHALL быть время входа в рубеж, и оно MUST ставиться только при смене рубежа и при возврате записи в работу. Запись, простоявшая в рубеже дольше предела, MUST останавливаться признаком с причиной «застряла».

Пределов MUST быть два, и граница проходит по тому, чью работу ждём: своя работа — час, ожидание чужой операции — сутки. Одно общее число пришлось бы мерить по самому долгому, и застрявшее приведение стояло бы сутки; число на каждый рубеж назвало бы разными вещи, различающиеся только исполнителем. Сколько идёт распознавание долгой записи, сервис не мерил, поэтому у чужой работы ошибка идёт в сторону долгого: ложная остановка хуже поздней. Оба числа MUST лежать в настройках.

Сторож этот ловит зависание, а не долгую работу, и час у своей работы меньше времени, которое многочасовая запись занимает на приведении. Цена решения названа прямо: длинная запись, отказавшая один раз и ждущая повтора дольше часа, будет остановлена как застрявшая. Цена ограничена тем, что остановка обратима — снятие признака возвращает запись на её рубеж, — и тем, что живой шаг проверяется по самому процессу. Решение владельца 2026-08-14.

Откладывание опроса MUST не двигать время входа в рубеж и MUST не сдвигать этот предел. Иначе запись, чью чужую операцию опрашивают раз в несколько секунд, никогда не достигнет предела, и застревание останется незамеченным.

Остановка по этому пределу MUST ничего не терять: идентификатор чужой операции остаётся в строке попытки распознавания, и снятие признака возобновляет опрос той же операции, а не заводит вторую.

Scenario: Сотня откладываний не двигает отсчёт

  • GIVEN запись стоит на рубеже отправки, и чужая операция ещё идёт
  • WHEN опрос откладывается сотню раз подряд
  • THEN время входа в рубеж не изменилось
  • AND отсчёт до предела не обнулился

Scenario: Предел достигнут

  • GIVEN запись простояла в рубеже дольше своего предела
  • WHEN за ней приходит захват
  • THEN запись получает признак остановки с причиной «застряла»

Scenario: Возобновление опроса не заводит вторую операцию

  • GIVEN запись остановлена по пределу на рубеже отправки
  • WHEN признак остановки снимают
  • THEN опрос идёт по прежнему идентификатору операции
  • AND новая операция у провайдера не заводится

Requirement: Откладывание не является переходом

Сервис SHALL различать переход на новый рубеж и откладывание работы над записью. Откладывание MUST ставить паузу и снимать захват, MUST не трогать ни рубеж, ни время входа в него, и MUST не считаться отказом: ожидание чужой операции отказом не является, поэтому число отказов оно MUST обнулять.

Сегодня шаг опроса зовёт переход с тем же состоянием, и мнимость этого перехода обнуляет счётчик. Без разделения время входа в рубеж сбрасывалось бы на каждом опросе и повторило бы ровно тот промах, ради которого заводится.

Scenario: Откладывание не двигает рубеж

  • GIVEN шаг опроса увидел, что чужая операция ещё идёт
  • WHEN он откладывает работу
  • THEN рубеж записи прежний, и время входа в него прежнее
  • AND захват с записи снят, а пауза поставлена

Requirement: Шаг с внешней оплатой проверяет сделанное

Шаг, чьё повторение оплачивается наружу, SHALL проверять, не сделана ли работа уже, и MUST не делать её второй раз. Проверка MUST идти по наблюдаемому признаку присутствия результата, а не по сверке содержимого хешем: у составного объекта во внешнем хранилище признак целостности не равен отпечатку содержимого.

Признак MUST записываться прежде, чем оплачиваемое обращение считается состоявшимся: строка попытки распознавания заводится до обращения к провайдеру, и повторный шаг начинает с проверки, не заведена ли операция.

Полной защиты от обрыва процесса между ответом провайдера и записью признака требование не даёт и дать не может: жёсткая остановка контейнера не оставляет места ни одной записи. Это остаточный риск, названный в дизайне, а не норма: норма, обязывающая к недостижимому, не пишется.

Scenario: Работа уже сделана

  • GIVEN результат оплачиваемого шага уже на месте, и признак его записан
  • WHEN шаг повторяется
  • THEN внешнее обращение не повторяется

Requirement: Число воркеров задаётся настройкой

Сервис SHALL брать число рабочих потоков конвейера из настроек, а сами потоки MUST не быть привязаны к отдельному шагу: каждый берёт любую подходящую запись и выбирает шаг по её рубежу. Поведение записи MUST не зависеть от числа потоков.

Ноль MUST быть законным значением: сервис поднимается, записи принимаются и не двигаются. Это режим, а не поломка.

Счётчик работы воркера MUST различать шаги: метка счётчика MUST нести рубеж, с которого запись взята, а не имя или номер потока. У одинаковых потоков имя перестаёт что-либо значить, а счётчик отказов — единственный сигнал, по которому владелец сервиса замечает поломку; без разреза по шагу «падает приведение» и «падает распознавание» становятся неразличимы.

Опрос чужой операции MUST оставаться работой очереди, а не отдельного смотрителя: очередь даёт ему неделимость захвата и возврат брошенного даром, а единственный смотритель умирает молча и уносит с собой целый класс записей.

Scenario: Запись доходит при одном потоке и при нескольких

  • GIVEN число потоков конвейера равно одному
  • WHEN запись проходит конвейер
  • THEN она доходит до конечного рубежа
  • AND при числе потоков больше одного исход тот же

Scenario: Потоков нет вовсе

  • GIVEN число потоков конвейера равно нулю
  • WHEN запись принимают
  • THEN сервис принимает её и не теряет
  • AND запись остаётся на первом рубеже

Scenario: Отказ виден с разрезом по шагу

  • GIVEN шаг приведения отказал
  • WHEN наблюдатель читает счётчик работы воркера
  • THEN отказ засчитан с меткой рубежа приведения

Requirement: Журнал событий записи пишется на смену рубежа

Сервис SHALL вести журнал событий аудиозаписи и MUST писать в него строку на смену рубежа, на остановку и на снятие остановки. Строка MUST называть источник события — шаг конвейера или человека, — сам шаг, исход и длительность.

Журнал MUST не писаться на каждое откладывание опроса: часовая запись дала бы сотни строк ни о чём.

Ни один шаг конвейера MUST не читать этот журнал, чтобы решить, что делать дальше: решение принимается по рубежу записи, и второй источник решения разошёлся бы с первым молча.

Содержимое записи в журнал событий MUST не попадать — инвариант приватности действует здесь наравне с журналом сервиса.

Scenario: Смена рубежа записана

  • GIVEN шаг конвейера довёл запись до нового рубежа
  • WHEN смотрят журнал событий этой записи
  • THEN в нём есть строка с шагом, исходом и длительностью

Scenario: Откладывание строки не пишет

  • GIVEN опрос чужой операции откладывается многократно
  • WHEN смотрят журнал событий записи
  • THEN строк об откладываниях в нём нет

Scenario: Перезапуск человеком виден в журнале

  • GIVEN запись остановлена признаком
  • WHEN человек снимает признак
  • THEN в журнале событий есть строка с указанием, что это сделал человек

Requirement: Число отказов ограничивает повторы шага

У аудиозаписи SHALL быть число отказов. Оно MUST расти при каждом захвате и MUST возвращаться к нулю, когда шаг завершился без отказа либо отложил работу. Рост при захвате, а не при отказе, засчитывает попытку и записи, брошенной на середине: шаг, уносящий с собой процесс, до объявления отказа не доходит никогда.

Остановка сервиса отказом не считается. Шаг, прерванный отменой по собственной остановке сервиса, MUST возвращать число отказов назад и MUST не выносить записи приговора: запись не виновата в том, что нас перезапустили, и несколько выкладок подряд иначе останавливают здоровую многочасовую запись с приговором «отказы исчерпаны». Всякая другая причина, по которой шаг не дошёл до объявления исхода, отказ тратит.

Запись, захваченная с числом отказов сверх заданного предела, MUST останавливаться признаком тем, кто её захватил, и MUST не отдаваться шагу в работу. Остановка эта видна отправителю опросом готовности наравне с прочими — норму держит capability intake.

Этот сторож MUST отвечать только за повторы внутри шага. Время, проведённое записью в рубеже, MUST мериться отдельным сторожем: одно число не справляется ни с одной из двух обязанностей — опрос, вернувший «ещё в работе», обнуляет его, и зависшая чужая операция опрашивается вечно, а не обнулял бы — убивал бы здоровую запись.

Scenario: Запись отказывает на каждой попытке

  • GIVEN шаг конвейера отказывает на каждой попытке
  • WHEN запись проходит заданное число отказов
  • THEN у неё появляется признак остановки
  • AND следующий захват её не выдаёт
  • AND опрос готовности отдаёт владельцу записи признак остановки

Scenario: Шаг уносит процесс, не объявив отказа

  • GIVEN шаг конвейера обрывается вместе с процессом на каждой попытке
  • WHEN запись захватывается снова заданное число раз
  • THEN у неё появляется признак остановки

Scenario: Остановка сервиса отказа не тратит

  • GIVEN шаг работает над записью
  • WHEN сервис останавливают, и шаг прерывается отменой
  • THEN число отказов записи прежнее
  • AND признака остановки у записи не появляется

Scenario: Прошедшая запись отказов не копит

  • GIVEN запись прошла подряд несколько рубежей без единого отказа
  • WHEN смотрят её число отказов
  • THEN оно не приблизилось к пределу

Requirement: Конвейер ответа отправителю не шлёт

Шаг конвейера SHALL доводить запись до достигнутого рубежа и MUST не обращаться к отправителю вовсе — ни с готовым текстом, ни с сообщением о неудаче. Исход своей записи отправитель узнаёт опросом готовности и в панели владельца; адрес опроса и содержимое ответа нормирует capability intake.

Требование заведено взамен доставки в чат, убранной вместе с входом Telegram. Без него молчание конвейера читалось бы как недоделка: прежде ответ уходил, и всякий, кто помнит это, ищет в шаге отправку, а её отсутствие принимает за потерянную ветку.

Инвариант проекта «Принятая запись не теряется молча» держится теперь опросом готовности — там остановка видна признаком — и журналом владельца, где у неё стоит причина. Обязанность при этом сменила направление: прежде об отказе сообщали, теперь отказ доступен спросившему. Отправитель, который не спрашивает, об остановке не узнаёт.

Записи, которой этот канал недоступен, не бывает: у каждой записи есть владелец, и опрос отдаёт ему её исход. Держится это обязательностью владельца в схеме хранилища — норму держит capability storage.

Scenario: Готовый текст отправителю не уходит

  • GIVEN запись дошла до конечного рубежа
  • WHEN шаг конвейера её завершает
  • THEN ни одного обращения наружу с текстом расшифровки не уходит
  • AND текст достаётся опросом готовности

Scenario: Остановка видна опросом, а не сообщением

  • GIVEN запись остановлена по исчерпании отказов
  • WHEN владелец записи спрашивает её рубеж
  • THEN ответ несёт достигнутый рубеж и признак остановки
  • AND в журнале владельца сервиса есть запись об остановке с причиной