хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
+255
@@ -0,0 +1,255 @@
|
||||
## 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** результат не записывается
|
||||
|
||||
#### Scenario: Человек снял остановку под работающим шагом
|
||||
|
||||
- **GIVEN** шаг работает над захваченной записью
|
||||
- **AND** человек за это время снял с неё признак остановки, освободив захват
|
||||
- **AND** запись досталась другому воркеру
|
||||
- **WHEN** первый шаг доходит до записи результата
|
||||
- **THEN** результат не записывается
|
||||
|
||||
### Requirement: Остановка записи — признак, а не рубеж
|
||||
|
||||
Сервис SHALL останавливать запись отдельным признаком с причиной и MUST не
|
||||
стирать при этом достигнутый рубеж. Признак MUST нести время остановки, причину
|
||||
и машинный текст отказа.
|
||||
|
||||
Снятие признака SHALL возвращать запись в работу **с того рубежа, где она
|
||||
стояла**, и MUST сбрасывать **всё, чем прошлый прогон её удерживал**:
|
||||
|
||||
- признак остановки — время остановки, причину и машинный текст отказа;
|
||||
- признак захвата и срок его протухания;
|
||||
- число отказов;
|
||||
- паузу перед повтором;
|
||||
- время входа в рубеж.
|
||||
|
||||
Перечень назван целиком и в одном месте, потому что забытое поле не даёт ни
|
||||
отказа, ни строки в журнале. Оставленный признак захвата держит запись занятой до
|
||||
протухания срока и отдаёт её потом чужому шагу — тому, чей результат условен по
|
||||
прежнему значению. Оставленное время входа в рубеж останавливает запись снова
|
||||
первым же захватом, если остановленной она простояла дольше предела, и перезапуск
|
||||
не работает вовсе. Оставленные отказы и пауза откладывают первый же прогон на
|
||||
накопленный срок.
|
||||
|
||||
Возврат в работу MUST идти **через домен**: тот, кто его делает, называет запись,
|
||||
а поля выше сбрасывает домен одним действием. Правка колонок мимо домена
|
||||
повторяет перечень вторым местом, и второе место расходится с первым молча.
|
||||
|
||||
Возврат в работу MUST писать событие журнала записи с происхождением «человек».
|
||||
Иначе запись, побывавшая остановленной и вернувшаяся в работу, неотличима в
|
||||
журнале от записи, которую конвейер вёл без остановок, а происхождение события
|
||||
перестаёт различать что-либо: другого писателя, кроме конвейера, у журнала не
|
||||
остаётся.
|
||||
|
||||
Инструментом возврата сегодня служит подкоманда набора инструментов
|
||||
разработчика: панели у сервиса нет, а экраны владельца приносят отдельные задачи.
|
||||
Норма написана про поля и про домен, а не про инструмент, и появление экрана её
|
||||
не трогает.
|
||||
|
||||
Прежние состояния отказа и смерти MUST не заводиться заново: обе причины
|
||||
восстанавливаются одинаково — снятием признака, — и различие между ними
|
||||
перестаёт быть структурным, оставаясь причиной остановки. Состояние, называющее
|
||||
отказ, стирает достигнутый рубеж, и продолжение с места остановки становится
|
||||
невозможным.
|
||||
|
||||
**Способ вывести запись из выборки MUST быть один — этот признак.** Второго
|
||||
признака, исключающего запись из работы помимо рубежа и паузы, MUST не
|
||||
заводиться: два способа расходятся, и молчаливо теряется тот, который забыли
|
||||
проверить. Условие отбора MUST не выводить запись из выборки молча — запись,
|
||||
переставшая браться в работу, обязана нести признак остановки с причиной.
|
||||
|
||||
Остановку MUST ставить тот, кто запись захватил. Перевод принадлежит одному
|
||||
месту: условие отбора, молча пропускающее запись мимо выборки, оставило бы её
|
||||
без следа.
|
||||
|
||||
Остановленная запись MUST не выдаваться захвату.
|
||||
|
||||
#### Scenario: Остановленная запись продолжает с места остановки
|
||||
|
||||
- **GIVEN** шаг остановил запись на рубеже приведения
|
||||
- **WHEN** признак остановки снимают
|
||||
- **THEN** следующим идёт отправка на распознавание, а не повторное приведение
|
||||
|
||||
#### Scenario: Остановленная запись не выдаётся захвату
|
||||
|
||||
- **GIVEN** у записи стоит признак остановки
|
||||
- **WHEN** за её рубежом приходит захват
|
||||
- **THEN** запись ему не выдаётся
|
||||
|
||||
#### Scenario: Снятие признака сбрасывает всех сторожей
|
||||
|
||||
- **GIVEN** запись остановлена с накопленными отказами и паузой
|
||||
- **AND** остановленной она простояла дольше предела времени в рубеже
|
||||
- **WHEN** признак остановки снимают
|
||||
- **THEN** число отказов, пауза и время входа в рубеж сброшены
|
||||
- **AND** ближайший захват выдаёт запись, а не останавливает её снова
|
||||
|
||||
#### Scenario: Возврат в работу освобождает захват
|
||||
|
||||
- **GIVEN** запись остановлена, и признак прошлого захвата на ней стоит
|
||||
- **WHEN** признак остановки снимают
|
||||
- **THEN** признака захвата и срока его протухания на записи нет
|
||||
- **AND** ближайший захват выдаёт запись, не дожидаясь протухания прежнего срока
|
||||
|
||||
#### Scenario: Возврат в работу виден в журнале записи
|
||||
|
||||
- **GIVEN** запись остановлена
|
||||
- **WHEN** её возвращают в работу
|
||||
- **THEN** в журнале событий записи есть событие с происхождением «человек»
|
||||
|
||||
### Requirement: Конвейер ответа отправителю не шлёт
|
||||
|
||||
Шаг конвейера SHALL доводить запись до достигнутого рубежа и MUST не обращаться
|
||||
к отправителю вовсе — ни с готовым текстом, ни с сообщением о неудаче. Исход
|
||||
своей записи владелец узнаёт **карточкой записи**; адрес карточки и содержимое
|
||||
ответа нормирует capability `archive`.
|
||||
|
||||
Владелец сервиса узнаёт исход журналом и журналом событий записи. Панели, где он
|
||||
видел бы то же строкой таблицы, у сервиса нет — она ушла вместе со встроенным
|
||||
хранилищем, и второго канала наблюдения это не отняло: журнал событий пишется
|
||||
по-прежнему, а читается запросом к базе, пока экрана нет.
|
||||
|
||||
Держатель нормы сменился вместе с убранным опросом готовности: прежде исход
|
||||
отдавал адрес опроса, нормированный capability `intake`, и адреса этого больше
|
||||
нет. Обязанность при этом не изменилась — изменилось только то, каким адресом
|
||||
она исполняется.
|
||||
|
||||
Требование заведено взамен доставки в чат, убранной вместе с входом Telegram.
|
||||
Без него молчание конвейера читалось бы как недоделка: прежде ответ уходил, и
|
||||
всякий, кто помнит это, ищет в шаге отправку, а её отсутствие принимает за
|
||||
потерянную ветку.
|
||||
|
||||
Инвариант проекта «Принятая запись не теряется молча» держится теперь карточкой
|
||||
записи — там остановка видна признаком и причиной — и журналом владельца, где у
|
||||
неё стоит причина. Обязанность при этом сменила направление: прежде об отказе
|
||||
сообщали, теперь отказ доступен спросившему. Отправитель, который не
|
||||
спрашивает, об остановке не узнаёт.
|
||||
|
||||
Записи, которой этот канал недоступен, не бывает: у каждой записи есть владелец,
|
||||
и карточка отдаёт ему её исход. Держится это обязательностью владельца в схеме —
|
||||
норму держит capability `storage`.
|
||||
|
||||
#### Scenario: Готовый текст отправителю не уходит
|
||||
|
||||
- **GIVEN** запись дошла до конечного рубежа
|
||||
- **WHEN** шаг конвейера её завершает
|
||||
- **THEN** ни одного обращения наружу с текстом расшифровки не уходит
|
||||
- **AND** текст достаётся отдельным адресом текста записи
|
||||
|
||||
#### Scenario: Остановка видна карточкой, а не сообщением
|
||||
|
||||
- **GIVEN** запись остановлена по исчерпании отказов
|
||||
- **WHEN** владелец записи спрашивает её карточку
|
||||
- **THEN** ответ несёт достигнутый рубеж, признак остановки и её причину
|
||||
- **AND** в журнале владельца сервиса есть запись об остановке с причиной
|
||||
Reference in New Issue
Block a user