- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
20 KiB
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 в журнале владельца сервиса есть запись об остановке с причиной