хранилище, файлы записей и очередь переведены на встроенную PocketBase
- записи, метаданные и файлы съехались под один каталог данных; появилась панель владельца, а gin, goqu, goose и требование CGO ушли - захват задачи стал одним запросом с RETURNING; заведены число попыток, состояние dead и нарастающая пауза вместо признака is_error - имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с идентификатором записи оно собирало бы ссылку на скачивание
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
## Purpose
|
||||
|
||||
Конвейер расшифровки: как задача движется по состояниям, что делает воркер,
|
||||
когда работы нет, и что считается отказом шага.
|
||||
|
||||
Описаны пустой прогон воркера, захват задачи и срок его протухания, число
|
||||
попыток и выбывание задачи из очереди, пауза перед повтором. Сознательно не
|
||||
описаны переходы состояний и сама цепочка `created → converted → transcribe →
|
||||
done | failed`, отмена контекста посреди шага, освобождение ресурсов внешних
|
||||
клиентов. Это не значит, что такого поведения нет: оно живёт в коде, а
|
||||
требования на него не написаны, потому что требование без проверки —
|
||||
предположение, а не норма. Первая задача, которая трогает любое из
|
||||
перечисленного, дописывает его сюда.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Захват задачи неделим
|
||||
|
||||
Захват задачи воркером SHALL быть одним неделимым шагом хранилища: выбор
|
||||
подходящей задачи и пометка её захваченной MUST происходить вместе, и захваченная
|
||||
задача MUST возвращаться тем же шагом.
|
||||
|
||||
Одна и та же задача MUST доставаться ровно одному захватившему. Двум вызывающим,
|
||||
пришедшим за одним состоянием одновременно, запись MUST достаться одному, а
|
||||
второй MUST получить признак «работы в этом состоянии нет».
|
||||
|
||||
Порядок выборки MUST быть определён однозначно: сравнения по неуникальному
|
||||
значению для этого мало, и к нему MUST добавляться ключ записи. Иначе порядок
|
||||
обработки невоспроизводим, а проверка, опирающаяся на «следующую» задачу,
|
||||
зелена через раз.
|
||||
|
||||
Требование стоит на инварианте проекта «Принятая запись не теряется молча»:
|
||||
захват, разделённый на два шага, отдаёт одну запись двум воркерам, и работа
|
||||
одного из них теряется без следа.
|
||||
|
||||
Признак «работы нет» этим требованием не переопределяется — его нормирует
|
||||
требование «Пустой прогон воркера — не отказ».
|
||||
|
||||
#### Scenario: За задачей пришли трое разом
|
||||
|
||||
- **GIVEN** в опрашиваемом состоянии лежит ровно одна задача
|
||||
- **WHEN** три захвата этого состояния идут одновременно
|
||||
- **THEN** запись получает ровно один из них
|
||||
- **AND** двое остальных получают признак «работы в этом состоянии нет»
|
||||
|
||||
#### Scenario: Захваченная задача не выдаётся второй раз
|
||||
|
||||
- **GIVEN** задача захвачена и срок захвата не истёк
|
||||
- **WHEN** за тем же состоянием приходит следующий захват
|
||||
- **THEN** эта задача ему не выдаётся
|
||||
|
||||
### Requirement: Результат пишет только держатель захвата
|
||||
|
||||
Шаг конвейера SHALL записывать свой результат только тогда, когда захват задачи
|
||||
всё ещё принадлежит ему. Запись MUST быть условна по признаку захвата, а шаг,
|
||||
чей захват за время работы достался другому, MUST завершиться без записи
|
||||
результата и без ответа отправителю.
|
||||
|
||||
Требование закрывает то, чего неделимость захвата не закрывает: захват протухает
|
||||
не только у мёртвого воркера, но и у живого — шаг, идущий дольше своего срока,
|
||||
теряет задачу, продолжая работать. Без этого условия два воркера пишут в одну
|
||||
задачу по очереди, счётчик попыток сбрасывает тот, кто уже не владелец, а
|
||||
отправитель получает два ответа на одну запись.
|
||||
|
||||
Шаг MUST записывать только те поля, которыми распоряжается сам. Задачу он держит
|
||||
снимком с момента захвата и до записи — это часы, — и безусловная запись снимка
|
||||
стёрла бы всё, что владелец правил в панели за это время: молча, без строки в
|
||||
журнале и без отказа в панели. Владелец увидел бы успешное сохранение и был бы
|
||||
уверен, что правка на месте.
|
||||
|
||||
#### Scenario: Правка владельца пережила сохранение шага
|
||||
|
||||
- **GIVEN** шаг держит захваченную задачу
|
||||
- **AND** владелец за это время изменил в панели поле, которого шаг не касается
|
||||
- **WHEN** шаг записывает свой результат
|
||||
- **THEN** результат шага записан
|
||||
- **AND** правка владельца на месте
|
||||
|
||||
#### Scenario: Захват ушёл под работающим шагом
|
||||
|
||||
- **GIVEN** шаг работает над захваченной задачей
|
||||
- **AND** за это время та же задача досталась другому захвату
|
||||
- **WHEN** первый шаг доходит до записи результата
|
||||
- **THEN** результат не записывается
|
||||
- **AND** отправителю ничего не отправляется
|
||||
|
||||
### Requirement: Брошенная задача возвращается в работу
|
||||
|
||||
Задача, захваченная и брошенная на середине, SHALL доставаться снова по
|
||||
истечении срока захвата. Срок MUST считаться от времени захвата, а истёкший
|
||||
захват MUST не мешать выдать задачу следующему.
|
||||
|
||||
Срок задаётся шагом конвейера и MUST быть не меньше того времени, которое этот
|
||||
шаг может занять на самом длинном допустимом входе. Срок короче делает
|
||||
протухание штатным событием живого шага, а не признаком беды.
|
||||
|
||||
Все значения времени, по которым идёт этот отбор, MUST записываться и сравниваться
|
||||
в одном виде — том же, в каком хранилище пишет собственные времена записи.
|
||||
Сравнение идёт побайтово, и вид, разошедшийся хоть разделителем, обращает
|
||||
условие в постоянную истину или постоянную ложь, причём молча.
|
||||
|
||||
#### Scenario: Захват протух
|
||||
|
||||
- **GIVEN** задача захвачена, а время захвата отстоит дальше срока
|
||||
- **WHEN** за её состоянием приходит захват
|
||||
- **THEN** задача выдаётся ему
|
||||
|
||||
#### Scenario: Срок сравнивается с временем, записанным хранилищем
|
||||
|
||||
- **GIVEN** задача захвачена, и время захвата записано в том же виде, в каком
|
||||
хранилище пишет время изменения записи
|
||||
- **WHEN** за её состоянием приходит захват до истечения срока
|
||||
- **THEN** задача ему не выдаётся
|
||||
|
||||
### Requirement: Число попыток и состояние «мертва»
|
||||
|
||||
У задачи SHALL быть число попыток. Оно MUST расти при каждом захвате и MUST
|
||||
возвращаться к нулю, когда шаг завершился без отказа. Рост при захвате, а не при
|
||||
отказе, засчитывает попытку и задаче, брошенной на середине: шаг, уносящий с
|
||||
собой процесс, до объявления отказа не доходит никогда, и без этого такая задача
|
||||
крутилась бы вечно.
|
||||
|
||||
Задача, захваченная с числом попыток сверх заданного предела, MUST переводиться в
|
||||
состояние «мертва» тем, кто её захватил, и MUST не отдаваться шагу в работу. Перевод
|
||||
принадлежит одному месту: условие отбора, молча пропускающее задачу мимо выборки,
|
||||
оставило бы её без состояния и без следа.
|
||||
|
||||
Мёртвая задача MUST отбираться владельцем по своему состоянию и MUST
|
||||
возвращаться в работу правкой этого состояния — без запроса в консоли сервера.
|
||||
|
||||
Переход в «мертва» MUST сообщать отправителю о неудаче ровно так же, как
|
||||
сообщает о ней отказ шага. Иначе он становится третьим исходом там, где инвариант
|
||||
проекта «Принятая запись не теряется молча» допускает два: задача не пригодна к
|
||||
повтору и об отказе никто не сказал.
|
||||
|
||||
От состояния отказа «мертва» отличается тем, чей это приговор. В `failed` задачу
|
||||
переводит шаг, рассудивший об этой записи окончательно: конвертация не удалась,
|
||||
распознавание вернуло ошибку. В «мертва» задача уходит без такого суждения — мы
|
||||
повторяли и перестали. Ни один шаг конвейера в «мертва» не переводит сам.
|
||||
|
||||
Прежний признак «задача с ошибкой», исключавший задачу из выборки навсегда и
|
||||
отдельный от перечня состояний, MUST не заводиться заново: два способа вывести
|
||||
задачу из выборки расходятся, и молчаливо теряется тот, который забыли проверить.
|
||||
|
||||
#### Scenario: Задача падает на каждой попытке
|
||||
|
||||
- **GIVEN** шаг конвейера отказывает на каждой попытке
|
||||
- **WHEN** задача проходит заданное число попыток
|
||||
- **THEN** она переходит в состояние «мертва»
|
||||
- **AND** следующий захват её не выдаёт
|
||||
- **AND** отправитель получает сообщение о неудаче
|
||||
|
||||
#### 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** число попыток задачи не растёт
|
||||
Reference in New Issue
Block a user