хранилище, файлы записей и очередь переведены на встроенную PocketBase
- записи, метаданные и файлы съехались под один каталог данных; появилась панель владельца, а gin, goqu, goose и требование CGO ушли - захват задачи стал одним запросом с RETURNING; заведены число попыток, состояние dead и нарастающая пауза вместо признака is_error - имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с идентификатором записи оно собирало бы ссылку на скачивание
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
Telegram делит с ним общий шаг заведения задачи, но требований на него нет:
|
||||
требование, написанное без проверки, — предположение, а не норма. Первая задача,
|
||||
которая трогает поведение приёма из Telegram, дописывает его сюда.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Приём записи по HTTP
|
||||
|
||||
@@ -25,13 +24,16 @@ MUST нести идентификатор задачи полем `job_id` и
|
||||
Приём не судит о годности записи сам: расширение он берёт из имени файла, а
|
||||
пригодность содержимого узнаёт у источника метаданных.
|
||||
|
||||
Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает
|
||||
хранилище, и нормирует её capability `storage`.
|
||||
|
||||
#### Scenario: Запись принята
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **WHEN** программа шлёт `POST /api/audio` с полем `audio`
|
||||
- **THEN** ответ имеет код `201`, а в теле лежат непустой `job_id` и `status`
|
||||
со значением `created`
|
||||
- **AND** содержимое записи целиком лежит в каталоге хранения одним файлом
|
||||
- **AND** содержимое записи целиком лежит в хранилище одним файлом
|
||||
|
||||
#### Scenario: Поля с записью нет
|
||||
|
||||
@@ -53,17 +55,28 @@ MUST нести идентификатор задачи полем `job_id` и
|
||||
своим приёму не подконтрольно.
|
||||
|
||||
Расширения в присланном имени нет — сервис MUST подставить `.audio`, чтобы у
|
||||
файла на диске расширение было всегда.
|
||||
файла в хранилище расширение было всегда.
|
||||
|
||||
Требование переживает смену раскладки. Умолчание хранилища, строящее имя из
|
||||
имени отправителя, MUST не применяться: имя отправителя в журнал не пишется по
|
||||
инварианту приватности, а изъятие из него кончается расширением — хвостом после
|
||||
последней точки.
|
||||
|
||||
#### Scenario: Расширение взято из имени отправителя
|
||||
|
||||
- **WHEN** программа шлёт запись с именем `test.mp3`
|
||||
- **THEN** файл в каталоге хранения имеет расширение `.mp3`
|
||||
- **THEN** имя файла в хранилище оканчивается на `.mp3`
|
||||
|
||||
#### Scenario: Имени без расширения назначено своё
|
||||
|
||||
- **WHEN** программа шлёт запись с именем `test` без расширения
|
||||
- **THEN** файл в каталоге хранения имеет расширение `.audio`
|
||||
- **THEN** имя файла в хранилище оканчивается на `.audio`
|
||||
|
||||
#### Scenario: Имя отправителя в хранилище не попало
|
||||
|
||||
- **WHEN** программа шлёт запись с именем `секретное-слово.mp3`
|
||||
- **THEN** имя файла в хранилище не содержит `секретное-слово`
|
||||
- **AND** путь к этому файлу не содержит его тоже
|
||||
|
||||
### Requirement: Отказ чтения метаданных
|
||||
|
||||
@@ -86,10 +99,10 @@ MUST нести идентификатор задачи полем `job_id` и
|
||||
личной переписки наравне с текстом расшифровки; журнал уезжает в собранные логи,
|
||||
откуда строку не убрать.
|
||||
|
||||
Расширение, взятое из этого имени, в журнале остаётся: оно стоит в собственном
|
||||
имени файла на диске, и по нему прослеживается путь записи. Что именно попадает в
|
||||
журнал ради прослеживаемости, нормирует требование ниже; наружу расширение
|
||||
выходит только приведённым к известному виду — этому отдано отдельное требование.
|
||||
Расширение, взятое из этого имени, в журнале остаётся собственным полем: по нему
|
||||
прослеживается путь записи. Что именно попадает в журнал ради прослеживаемости,
|
||||
нормирует требование ниже; наружу расширение выходит только приведённым к
|
||||
известному виду — этому отдано отдельное требование.
|
||||
|
||||
Сценарии судят приём по HTTP, потому что имя, данное отправителем, доходит до
|
||||
сервиса только оттуда: из Telegram приходит путь, выданный самим Telegram, а не
|
||||
@@ -119,8 +132,10 @@ MUST нести идентификатор задачи полем `job_id` и
|
||||
записи и её размер в байтах. По ним путь записи собирается отбором по журналу, и
|
||||
удаление имени отправителя прослеживаемости не отнимает.
|
||||
|
||||
Расширение засчитывается присутствием собственного имени файла в хранилище:
|
||||
отдельного поля под него приём не заводит.
|
||||
Расширение засчитывается собственным полем журнальной строки. Имя, под которым
|
||||
файл лёг в хранилище, приём MUST в журнал не писать: это имя — последняя часть
|
||||
ссылки на скачивание, и записанное вместе с идентификатором записи оно собирает
|
||||
ссылку целиком. Норму держит capability `storage`.
|
||||
|
||||
#### Scenario: Идентификатор, расширение и размер на месте
|
||||
|
||||
@@ -129,6 +144,12 @@ MUST нести идентификатор задачи полем `job_id` и
|
||||
- **THEN** журнал приёма несёт идентификатор заведённого файла, расширение
|
||||
принятой записи и её размер в байтах
|
||||
|
||||
#### Scenario: Имени файла в хранилище в журнале нет
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **WHEN** программа шлёт `POST /api/audio` с записью
|
||||
- **THEN** имени, под которым файл лёг в хранилище, в журнале приёма нет
|
||||
|
||||
### Requirement: Метка метрики несёт только известное расширение
|
||||
|
||||
Сервис SHALL приводить расширение принятой записи к известному виду прежде, чем
|
||||
@@ -147,12 +168,12 @@ MUST нести идентификатор задачи полем `job_id` и
|
||||
Когда конвертацию нормируют своей capability, обязанность переезжает туда вместе
|
||||
с ней.
|
||||
|
||||
Имя файла на диске это требование не трогает: там расширение остаётся тем, каким
|
||||
пришло, — это уже нормировано требованием «Имя файла в хранилище».
|
||||
Имя файла в хранилище это требование не трогает: там расширение остаётся тем,
|
||||
каким пришло, — это уже нормировано требованием «Имя файла в хранилище».
|
||||
|
||||
Настоящий формат записи, попавшей в `other`, остаётся видимым в журнале: значение
|
||||
`other` в метке означает «расширение не из перечня», а само оно стоит в поле
|
||||
пути журнальной строки приёма и в поле формата строки конвертации.
|
||||
`other` в метке означает «расширение не из перечня», а само оно стоит полем
|
||||
журнальной строки приёма и полем формата строки конвертации.
|
||||
|
||||
#### Scenario: Незнакомое расширение наружу не выходит
|
||||
|
||||
@@ -160,7 +181,7 @@ MUST нести идентификатор задачи полем `job_id` и
|
||||
- **WHEN** программа шлёт запись с именем, чей хвост после последней точки не
|
||||
принадлежит перечню known-форматов
|
||||
- **THEN** метка метрики принимает значение `other`
|
||||
- **AND** файл в каталоге хранения сохраняет пришедшее расширение
|
||||
- **AND** имя файла в хранилище сохраняет пришедшее расширение
|
||||
|
||||
#### Scenario: Известное расширение идёт как есть
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ transcribe → done | failed`, захват задачи и срок его пр
|
||||
что такого поведения нет: оно живёт в коде, а требования на него не написаны,
|
||||
потому что требование без проверки — предположение, а не норма. Первая задача,
|
||||
которая трогает любое из перечисленного, дописывает его сюда.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Пустой прогон воркера — не отказ
|
||||
|
||||
@@ -76,3 +75,188 @@ transcribe → done | failed`, захват задачи и срок его пр
|
||||
- **THEN** счётчик работы воркера растёт с пометкой успеха
|
||||
- **AND** записи об отказе в журнале нет
|
||||
|
||||
### 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** число попыток задачи не растёт
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# storage Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change pocketbase-storage. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Сервис поднимается на чистом каталоге данных
|
||||
|
||||
Сервис SHALL приводить хранилище в рабочий вид сам: на пустом каталоге данных он
|
||||
MUST завести свою схему и принимать записи обоими входами без единого ручного
|
||||
шага до первого запуска.
|
||||
|
||||
Прежние данные не переносятся. Каталог, оставшийся от прежней раскладки, MUST не
|
||||
читаться и не считаться источником: сервис начинает с чистого листа, и это
|
||||
решение задачи, а не следствие отказа.
|
||||
|
||||
Схема MUST заводиться версионированными шагами, а применённый шаг MUST не
|
||||
переписываться — только новым шагом. Иначе повторный запуск на уже заведённом
|
||||
каталоге разошёлся бы с первым молча.
|
||||
|
||||
Каталог данных у сервиса MUST быть один: база и файлы записей лежат под ним
|
||||
вместе, и второго пути к ним не заводится.
|
||||
|
||||
#### Scenario: Первый запуск на пустом каталоге
|
||||
|
||||
- **GIVEN** каталог данных пуст
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он заводит своё хранилище и продолжает работу
|
||||
- **AND** принятая следом запись доходит до состояния `done`
|
||||
|
||||
#### Scenario: Повторный запуск на заведённом каталоге
|
||||
|
||||
- **GIVEN** сервис уже запускался на этом каталоге и завёл хранилище
|
||||
- **WHEN** он запускается снова
|
||||
- **THEN** он не заводит схему второй раз и не теряет прежние записи
|
||||
|
||||
### Requirement: Файл записи живёт в хранилище
|
||||
|
||||
Сервис SHALL держать файл записи в хранилище, а не отдельным каталогом рядом с
|
||||
ним. Файл MUST попадать туда вместе с записью, которой принадлежит, и MUST
|
||||
адресоваться этой записью, а не путём на диске.
|
||||
|
||||
Раскладку файлов на диске выбирает хранилище. Собственного плоского каталога
|
||||
записей у сервиса MUST не оставаться: файл, лежащий мимо хранилища, не попадёт
|
||||
ни в панель владельца, ни в резервную копию, а ради этих двух вещей перевод и
|
||||
делается.
|
||||
|
||||
Содержимое записи MUST не читаться в память целиком ни при укладке в хранилище,
|
||||
ни при чтении из него: расчётный потолок записи — шесть часов, и такая запись в
|
||||
память не помещается.
|
||||
|
||||
**Потолок размера записи MUST быть задан числом, выведенным из этого расчётного
|
||||
потолка**, и задан он MUST быть везде, где иначе действует чужое умолчание: и у
|
||||
поля файла в хранилище, и у тела запроса приёма. Умолчания здесь не «без
|
||||
предела», а величины на два-три порядка меньше нужного, и оставленные как есть
|
||||
они отвергают штатную запись сервиса — приём отказывает, а уже принятая запись
|
||||
исчерпывает попытки на шаге конвертации.
|
||||
|
||||
Отказ по этому потолку MUST быть виден отправителю ответом, а не молчанием.
|
||||
|
||||
Шаги, которым нужен файл именем на диске — конвертация и чтение метаданных
|
||||
отдают его внешней программе, — MUST получать рабочую копию **одним общим
|
||||
способом**, и у этого способа MUST быть единственный способ её убрать. Уборку
|
||||
зовёт шаг, и звать её он MUST на любом исходе, включая отказ. Заводить копию по
|
||||
месту шагам MUST не приходиться: иначе обязанность прибрать переписывается
|
||||
столько раз, сколько шагов, а забытая копия — это шестичасовая запись,
|
||||
оставшаяся во временном каталоге, и узнать о ней неоткуда.
|
||||
|
||||
#### Scenario: Принятая запись легла в хранилище
|
||||
|
||||
- **WHEN** запись принята любым входом
|
||||
- **THEN** её файл лежит в хранилище и связан со своей записью
|
||||
- **AND** отдельного каталога записей рядом с хранилищем не появляется
|
||||
|
||||
#### Scenario: Запись длиннее чужого умолчания принимается
|
||||
|
||||
- **WHEN** в хранилище кладут запись длиннее умолчания, действующего у поля файла
|
||||
- **THEN** она ложится в хранилище, а не отвергается
|
||||
|
||||
#### Scenario: Шаг конвейера берёт файл по записи
|
||||
|
||||
- **GIVEN** запись принята и её файл лежит в хранилище
|
||||
- **WHEN** шаг конвейера берётся за эту запись
|
||||
- **THEN** он получает файл по самой записи, а не по пути на диске
|
||||
|
||||
#### Scenario: Рабочая копия убрана после отказа шага
|
||||
|
||||
- **GIVEN** шагу выдана рабочая копия файла
|
||||
- **WHEN** шаг завершается отказом
|
||||
- **THEN** рабочей копии во временном каталоге не остаётся
|
||||
|
||||
### Requirement: Файл отдаётся ссылкой
|
||||
|
||||
Сервис SHALL отдавать файл записи ссылкой, которую строит хранилище по самой
|
||||
записи. Отданный файл MUST совпадать с принятым по длине.
|
||||
|
||||
Ссылка на несуществующую запись MUST отвечать отказом, а не пустым файлом.
|
||||
|
||||
**Ссылка сама по себе и есть право пройти по ней**, и потому она MUST не попадать
|
||||
ни в журнал, ни в метку метрики, ни в ответ отправителю. Имя, под которым файл
|
||||
лёг в хранилище, из журнала выводимо быть не должно: журнал уезжает в собранные
|
||||
логи, откуда строку не убрать, и оттуда ссылка на чужую запись работала бы
|
||||
бессрочно.
|
||||
|
||||
Отсюда требование к отказам: сообщение об отказе хранилища MUST не выходить за
|
||||
пределы хранилища дословно. Отказ чтения и отказ укладки называют ключ файла
|
||||
целиком, а отказ выгрузки во внешнее хранилище — полный адрес объекта; и то и
|
||||
другое кончается в журнале и собирает ссылку не хуже успешного пути.
|
||||
|
||||
Что именно журнал приёма пишет ради прослеживаемости, нормирует capability
|
||||
`intake`.
|
||||
|
||||
#### Scenario: Файл забирают по ссылке
|
||||
|
||||
- **GIVEN** запись принята и её файл лежит в хранилище
|
||||
- **WHEN** ссылку на файл запрашивают
|
||||
- **THEN** приходит тот же файл, и его длина совпадает с длиной принятого
|
||||
|
||||
#### Scenario: Ссылка ведёт в никуда
|
||||
|
||||
- **WHEN** запрашивают ссылку на запись, которой нет
|
||||
- **THEN** приходит отказ, а не пустой ответ
|
||||
|
||||
#### Scenario: По журналу ссылку не собрать
|
||||
|
||||
- **GIVEN** запись принята и прошла конвейер
|
||||
- **WHEN** читают журнал сервиса целиком
|
||||
- **THEN** имени, под которым файл лёг в хранилище, в нём нет
|
||||
|
||||
#### Scenario: Отказ чтения файла не называет его ключ
|
||||
|
||||
- **GIVEN** файл записи не читается из хранилища
|
||||
- **WHEN** шаг конвейера берётся за эту запись и отказывает
|
||||
- **THEN** отказ называет запись её идентификатором и не несёт имени файла
|
||||
|
||||
### Requirement: Наружу хранилище отдаёт только то, что заказано
|
||||
|
||||
Сервис SHALL держать закрытыми собственные разделы хранилища, которые тот
|
||||
публикует тем же портом. Запрос без прав владельца MUST получать отказ на
|
||||
перечисление и чтение записей коллекций, на служебные разделы хранилища —
|
||||
журналы запросов, резервные копии, настройки, расписание — и на правку чего бы
|
||||
то ни было.
|
||||
|
||||
Требование заводится потому, что порт опубликован в интернет, а вместе с
|
||||
переводом наружу выходит поверхность, которой у сервиса не было. Что API сервиса
|
||||
сегодня открыт всякому — известно и записано моделью угроз; новая поверхность под
|
||||
это знание не подпадает и закрывается здесь.
|
||||
|
||||
Правило доступа, оставленное пустым, значит «только владелец панели». Именно
|
||||
пустым оно MUST и оставаться: непустое правило, поставленное будущей правкой
|
||||
схемы, открыло бы перечисление всех записей анонимному запросу и не нарушило бы
|
||||
при этом ни одного другого требования.
|
||||
|
||||
#### Scenario: Аноним перечисляет записи
|
||||
|
||||
- **WHEN** запрос без прав владельца просит список записей коллекции задач
|
||||
- **THEN** приходит отказ
|
||||
|
||||
#### Scenario: Аноним читает служебный раздел
|
||||
|
||||
- **WHEN** запрос без прав владельца просит журнал запросов или список резервных
|
||||
копий хранилища
|
||||
- **THEN** приходит отказ
|
||||
|
||||
### Requirement: Владелец видит записи в панели
|
||||
|
||||
Сервис SHALL давать владельцу панель, где задача видна строкой, отбирается по
|
||||
своему идентификатору и правится, а её файл слушается и скачивается.
|
||||
|
||||
Панель MUST отдаваться тем же сервисом по своему адресу и MUST не требовать
|
||||
второго процесса.
|
||||
|
||||
Панель — вход в задачу наравне с конвейером, а не окно просмотра, и правка
|
||||
состояния задачи в ней MUST подчиняться тем же правилам перехода, что и правка
|
||||
из кода: служебные поля прошлого состояния — признак захвата, время захвата,
|
||||
пауза, число попыток — MUST очищаться. Иначе владелец, вернувший мёртвую задачу в
|
||||
работу, получит задачу, которая не выдаётся захвату до конца прежнего срока и
|
||||
умирает от первого же отказа, — и не узнает об этом.
|
||||
|
||||
Задача, заведённая в панели руками, MUST не уносить сервис: поля, без которых
|
||||
шаг конвейера не может работать, MUST быть обязательными в самой схеме, а
|
||||
перечень состояний — закрытым.
|
||||
|
||||
Панель разграничению доступа сервиса не подчиняется: вошедший в неё видит все
|
||||
записи, все файлы и всех пользователей разом. Закрывает её контур выкладки, а не
|
||||
сервис — это записано моделью угроз проекта.
|
||||
|
||||
#### Scenario: Принятая запись видна владельцу
|
||||
|
||||
- **GIVEN** запись принята и её задача заведена
|
||||
- **WHEN** владелец отбирает задачи по идентификатору принятой
|
||||
- **THEN** он видит её строкой со своим состоянием
|
||||
- **AND** файл этой записи скачивается из той же строки
|
||||
|
||||
#### Scenario: Мёртвую задачу вернули в работу правкой в панели
|
||||
|
||||
- **GIVEN** задача в состоянии «мертва» с исчерпанными попытками и признаком
|
||||
прежнего захвата
|
||||
- **WHEN** владелец меняет её состояние на рабочее
|
||||
- **THEN** признак захвата, время захвата, пауза и число попыток очищены
|
||||
- **AND** ближайший захват выдаёт задачу
|
||||
|
||||
### Requirement: Пароль владельца от панели не лежит в конфигурации
|
||||
|
||||
Сервис SHALL не заводить в конфигурации ключа под пароль владельца от панели.
|
||||
Пароль MUST задаваться самим владельцем, а хранилище MUST держать только его
|
||||
отпечаток.
|
||||
|
||||
Требование стоит на инварианте проекта «Секрет не покидает конфиг» с другой
|
||||
стороны: секрет, которого в конфигурации нет, не утекает вместе с ней и не
|
||||
уезжает в выкладку третьим путём. Пароль от панели открывает все записи и все
|
||||
файлы разом — это самое чувствительное, что есть у сервиса.
|
||||
|
||||
Приглашение завести владельца сервис MUST печатать только пока владельца нет, и
|
||||
оно MUST истекать по времени. Приглашение равносильно паролю от панели, а
|
||||
печатается оно в журнал контейнера, откуда строку не убрать: бессрочное отдало бы
|
||||
панель всякому читателю логов навсегда.
|
||||
|
||||
Пока владелец пароля не задал, сервис MUST работать обоими входами: панель без
|
||||
владельца не мешает принимать записи.
|
||||
|
||||
#### Scenario: Владелец пароля ещё не задал
|
||||
|
||||
- **GIVEN** каталог данных пуст и владелец панели не заведён
|
||||
- **WHEN** сервис запускается
|
||||
- **THEN** он принимает записи обоими входами
|
||||
- **AND** ни один ключ конфигурации не несёт пароля от панели
|
||||
|
||||
#### Scenario: Владелец заведён, приглашение больше не печатается
|
||||
|
||||
- **GIVEN** владелец панели заведён
|
||||
- **WHEN** сервис запускается снова
|
||||
- **THEN** приглашения завести владельца в журнале нет
|
||||
|
||||
Reference in New Issue
Block a user