внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
@@ -0,0 +1,714 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### 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 не отменять остановку: её нормирует требование «Недоставленный ответ
|
||||
не роняет шаг».
|
||||
|
||||
#### Scenario: Остановка по отказам сообщает отправителю
|
||||
|
||||
- **GIVEN** запись остановлена по исчерпании отказов
|
||||
- **WHEN** шаг доходит до ответа отправителю
|
||||
- **THEN** отправитель получает сообщение о неудаче
|
||||
|
||||
#### Scenario: Остановка по времени сообщает отправителю
|
||||
|
||||
- **GIVEN** запись остановлена по пределу времени в рубеже
|
||||
- **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 не отдаваться шагу в
|
||||
работу. Об этой остановке отправителю сообщается наравне с прочими — норму
|
||||
держит требование «Всякая остановка сообщает отправителю».
|
||||
|
||||
Этот сторож MUST отвечать только за повторы внутри шага. Время, проведённое
|
||||
записью в рубеже, MUST мериться отдельным сторожем: одно число не справляется ни
|
||||
с одной из двух обязанностей — опрос, вернувший «ещё в работе», обнуляет его, и
|
||||
зависшая чужая операция опрашивается вечно, а не обнулял бы — убивал бы здоровую
|
||||
запись.
|
||||
|
||||
#### Scenario: Запись отказывает на каждой попытке
|
||||
|
||||
- **GIVEN** шаг конвейера отказывает на каждой попытке
|
||||
- **WHEN** запись проходит заданное число отказов
|
||||
- **THEN** у неё появляется признак остановки
|
||||
- **AND** следующий захват её не выдаёт
|
||||
- **AND** отправитель получает сообщение о неудаче
|
||||
|
||||
#### Scenario: Шаг уносит процесс, не объявив отказа
|
||||
|
||||
- **GIVEN** шаг конвейера обрывается вместе с процессом на каждой попытке
|
||||
- **WHEN** запись захватывается снова заданное число раз
|
||||
- **THEN** у неё появляется признак остановки
|
||||
|
||||
#### Scenario: Остановка сервиса отказа не тратит
|
||||
|
||||
- **GIVEN** шаг работает над записью
|
||||
- **WHEN** сервис останавливают, и шаг прерывается отменой
|
||||
- **THEN** число отказов записи прежнее
|
||||
- **AND** признака остановки у записи не появляется
|
||||
|
||||
#### Scenario: Прошедшая запись отказов не копит
|
||||
|
||||
- **GIVEN** запись прошла подряд несколько рубежей без единого отказа
|
||||
- **WHEN** смотрят её число отказов
|
||||
- **THEN** оно не приблизилось к пределу
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### 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** результат не записывается
|
||||
- **AND** отправителю ничего не отправляется
|
||||
|
||||
#### 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 не считать прогон отказом: не увеличивать счётчик работы
|
||||
и не писать о нём на уровне владельца сервиса. Признак пустого прогона 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 считаться отдельной метрикой с причиной
|
||||
меткой.
|
||||
|
||||
Причин у недоставки две, и исход у них общий: **вход отправителя не поднят** —
|
||||
запись заведена прошлым запуском, а сервис поднялся без этого входа; и **адресат
|
||||
у записи не назван** — источником значится Telegram, а чата в записи нет.
|
||||
|
||||
Уровень записи MUST различать эти причины. Неподнятый вход — объявленный режим,
|
||||
и его уровень «может стать проблемой». Неназванный адресат — симптом порчи
|
||||
записи: у записи из Telegram чат есть всегда, и пропасть он может только от
|
||||
дефекта, самый коварный источник которого назван инвариантом проекта про колонки
|
||||
очереди. Один уровень на обе причины утопил бы этот сигнал в потоке штатных
|
||||
записей о ненастроенном боте.
|
||||
|
||||
Общий исход — не упрощение, а следствие момента: ответ уходит **после** того, как
|
||||
достигнутый рубеж сохранён. Работа к этой минуте сделана, и объявленный отказ
|
||||
засчитался бы воркеру сбоем и лёг бы владельцу записью отказа — то есть соврал бы
|
||||
про исход дважды. Повтор делу не помогает: ни бот, ни адресат от ожидания не
|
||||
появятся. Поэтому запись остаётся на достигнутом рубеже, в повтор не уходит и
|
||||
**признака остановки не получает**, а причина недоставки живёт в записи журнала,
|
||||
а не в рубеже записи.
|
||||
|
||||
То же MUST относиться к недоставке сообщения об **остановке**: остановка уже
|
||||
сохранена, и недоставка её MUST не отменять.
|
||||
|
||||
Идентификатор записи в этой строке обязателен: без него владелец видит, что
|
||||
ответ не ушёл, но не может найти, чей. Текст расшифровки и сообщение отправителя
|
||||
в эту запись MUST не попадать — приватность содержимого записи требование не
|
||||
ослабляет.
|
||||
|
||||
Отложенной доставки это требование не заводит: ответ, не ушедший сегодня, не
|
||||
уходит и потом. Забрать расшифровку можно там же, где лежат остальные.
|
||||
|
||||
#### Scenario: Вход отправителя не поднят
|
||||
|
||||
- **GIVEN** запись принята входом Telegram прошлым запуском сервиса
|
||||
- **AND** сервис поднялся без этого входа
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем
|
||||
- **AND** запись остаётся на достигнутом рубеже, в повтор не уходит и признака
|
||||
остановки не получает
|
||||
- **AND** в журнале есть запись уровня `WARN` о недоставке с идентификатором
|
||||
записи и причиной
|
||||
- **AND** счётчик недоставленных ответов вырос с этой причиной меткой
|
||||
- **AND** ни текста расшифровки, ни сообщения отправителя в этой записи нет
|
||||
|
||||
#### Scenario: Адресат у записи не назван
|
||||
|
||||
- **GIVEN** у записи источником значится Telegram, а чат не назван
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа, и воркер не считает прогон сбоем
|
||||
- **AND** запись остаётся на достигнутом рубеже
|
||||
- **AND** в журнале есть запись уровня `ERROR` о недоставке с идентификатором
|
||||
записи и причиной: неназванный адресат — симптом порчи записи
|
||||
|
||||
#### Scenario: Не доехало сообщение об остановке
|
||||
|
||||
- **GIVEN** запись остановлена признаком
|
||||
- **AND** вход отправителя не поднят
|
||||
- **WHEN** шаг доходит до ответа отправителю
|
||||
- **THEN** признак остановки у записи остаётся
|
||||
- **AND** в журнале есть запись о недоставке с идентификатором записи и причиной
|
||||
|
||||
#### Scenario: Отвечать некуда, потому что запись пришла не из Telegram
|
||||
|
||||
- **GIVEN** запись принята по HTTP
|
||||
- **WHEN** шаг конвейера доходит до ответа отправителю
|
||||
- **THEN** шаг завершается без отказа и без записи о недоставке
|
||||
|
||||
### Requirement: Выборка воркера владельцем не сужается
|
||||
|
||||
Воркер SHALL брать записи всех владельцев подряд и MUST не учитывать владельца
|
||||
при выборе очередной записи. Запись без владельца — принятая ботом — MUST
|
||||
обрабатываться наравне с прочими.
|
||||
|
||||
Владелец решает, кому запись показывать, а не кому её считать. Сужение выборки
|
||||
владельцем остановило бы расшифровку записей бота вовсе, а записи остальных
|
||||
поставило бы в зависимость от того, кто первым завёл учётную запись.
|
||||
|
||||
Владелец записи MUST переживать работу конвейера: шаг, сохраняющий свой
|
||||
результат, владельца не трогает и не затирает.
|
||||
|
||||
#### Scenario: Записи двух владельцев проходят одним воркером
|
||||
|
||||
- **GIVEN** заведены записи двух разных владельцев на одном рубеже
|
||||
- **WHEN** воркер забирает работу
|
||||
- **THEN** ему достаются обе, в порядке заведения
|
||||
|
||||
#### Scenario: Запись без владельца обрабатывается
|
||||
|
||||
- **GIVEN** заведена запись, принятая ботом, — без владельца
|
||||
- **WHEN** воркер забирает работу
|
||||
- **THEN** она достаётся ему наравне с прочими
|
||||
|
||||
#### Scenario: Шаг конвейера владельца не затирает
|
||||
|
||||
- **GIVEN** запись с владельцем прошла шаг конвейера
|
||||
- **WHEN** шаг сохраняет свой результат
|
||||
- **THEN** владелец записи остаётся прежним
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Число попыток и состояние «мертва»
|
||||
|
||||
**Reason**: Одно число несло две обязанности сразу — ограничивать повторы внутри
|
||||
шага и ограничивать застревание, — и не справлялось ни с одной: опрос, вернувший
|
||||
«ещё в работе», обнулял его, и зависшая чужая операция опрашивалась вечно.
|
||||
Состояние «мертва», как и состояние отказа, стирало достигнутый рубеж, и
|
||||
продолжить с места остановки было не с чего.
|
||||
|
||||
**Migration**: Обязанности разведены по двум требованиям — «Число отказов
|
||||
ограничивает повторы шага» и «Время в рубеже ограничено». Состояния «мертва» и
|
||||
отказа заменены признаком остановки с причиной, который рубежа не стирает:
|
||||
требование «Остановка записи — признак, а не рубеж»; туда же дословно перенесены
|
||||
запрет на второй способ вывести запись из выборки и правило «перевод принадлежит
|
||||
одному месту». Обязанность сообщить отправителю вынесена в общее требование
|
||||
«Всякая остановка сообщает отправителю»: причин остановки стало больше одной, и
|
||||
обязанность, записанная у одной из них, у остальных читалась бы как снятая.
|
||||
Возврат в работу по-прежнему делает владелец, но снятием признака, а не правкой
|
||||
состояния.
|
||||
Reference in New Issue
Block a user