Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
This commit is contained in:
+80
-1
@@ -223,9 +223,24 @@ HRV); у накопительных — только `date`. Поэтому то
|
||||
```
|
||||
запрос → токен → лимит тела, gzip → проверка формы JSON
|
||||
→ запись тела в архив → строка в delivery → 200
|
||||
→ разбор → запись в витрину
|
||||
↓
|
||||
фоновый воркер: разбор → запись в витрину
|
||||
```
|
||||
|
||||
**Ответ отдаётся до свёртки, и это контракт, а не деталь реализации.** `200`
|
||||
означает «тело сохранено и учтено»; разобрано ли оно, говорит
|
||||
`delivery.parse_status`, и говорит позже. Причина измерена: свёртка 16 тысяч
|
||||
точек занимает 11 секунд, а `WriteTimeout` в Go ставится в `readRequest` — то
|
||||
есть до вызова обработчика — и потому является общим бюджетом на чтение тела,
|
||||
запись архива, учёт и свёртку. Исчерпав его, сервер считает, что отдал `200`,
|
||||
клиент получает обрыв, а `accessLog` пишет `status_code=200`: единственный канал
|
||||
наблюдаемости врёт. Бьёт это по широким проходам — ровно по тем, ради которых
|
||||
заведён инвариант «дыры закрываются сами».
|
||||
|
||||
Отсюда же второй бюджет: длинный дедлайн ответа выставляет **сам обработчик
|
||||
приёма**, а не конфиг сервера. `write_timeout` глобален, и поднять его значило бы
|
||||
снять защиту от застрявшей записи со всех маршрутов ради одного.
|
||||
|
||||
Код ответа определяется **доставкой**, не разбором:
|
||||
|
||||
- **400** — тело не разбирается как JSON ожидаемой верхнеуровневой формы.
|
||||
@@ -236,6 +251,70 @@ HRV); у накопительных — только `date`. Поэтому то
|
||||
безопасности, исход разбора виден в логе, в `delivery.parse_status` и в
|
||||
`/stats`, а доразобрать их можно командой `reindex`.
|
||||
|
||||
#### Очередь свёртки — таблица, а не структура в памяти
|
||||
|
||||
Доставка ждёт свёртки в собственном статусе `pending`; канал между приёмом и
|
||||
воркером несёт один бит «есть работа». Это **transactional outbox**, он же «база
|
||||
как очередь заданий»: состояние задания пишется той же базой, что и факт
|
||||
события, а фоновый процесс выбирает необработанные строки.
|
||||
|
||||
Три следствия, ради которых так и сделано:
|
||||
|
||||
- **переполнять нечего** — доставка `pending` всегда, пока не свёрнута, поэтому
|
||||
«очередь переполнена» невыразимо;
|
||||
- **падение процесса очереди не теряет** — транзакция свёртки откатывается,
|
||||
статус остаётся `pending`;
|
||||
- **подбор `pending` при старте не является отдельным кодом** — это обычный
|
||||
проход воркера, а не особый режим.
|
||||
|
||||
Отвергнут **канал идентификаторов в памяти**: он вводит второе, недолговечное
|
||||
представление того же факта, и эти два расходятся при каждом падении; политика
|
||||
переполнения всё равно требует подбора из базы, то есть того же кода — только в
|
||||
двух экземплярах. Отвергнут и **опрос по таймеру вместо сигнала**: полпериода
|
||||
задержки на каждую доставку без пользы. Тик при этом взят **в дополнение** к
|
||||
сигналу: доставка, оставшаяся в очереди по обстоятельствам, иначе ждала бы
|
||||
следующей доставки, а ночью телефон молчит часами.
|
||||
|
||||
Воркер один, и порядок у него тот же, что у пересборки — `(received_at, id)`:
|
||||
слой доставки без плотных метрик наследуется от предшествующей доставки той же
|
||||
автоматизации, то есть является функцией префикса журнала. Обещается достижимое:
|
||||
в этом порядке сворачивается всё, что **видно воркеру** на момент выборки;
|
||||
абсолютного порядка при конкурентных приёмах нет и быть не может без сериализации
|
||||
самого приёма.
|
||||
|
||||
Классификацию исхода свёртки воркер и пересборка делят (`internal/replay`):
|
||||
второй классификатор разошёлся бы с первым молча, а по одному из его счётчиков
|
||||
(`partial`) принимается решение о судьбе тела в архиве.
|
||||
|
||||
**Исход разбора отражает доставку, а не обстоятельства.** Отмена и занятость
|
||||
базы статус не меняют — доставка остаётся `pending` и будет свёрнута снова;
|
||||
непонятое содержимое, невыводимый слой, нечитаемое тело, исчерпанный дедлайн и
|
||||
паника свёртки дают `failed`. Различение появилось не из аккуратности: `failed`
|
||||
из очереди выбывает навсегда и возвращается только пересборкой, а конкуренция за
|
||||
базу между приёмом и свёрткой стала штатной — без него занятость стирала бы
|
||||
доставку с полки молча. По той же причине учёт доставки идёт через транзакцию с
|
||||
повторами: одиночная вставка пересиживала бы только `busy_timeout`, после чего
|
||||
приём ответил бы `500` по доставке, тело которой уже на диске.
|
||||
|
||||
**Паника свёртки перехватывается там же, где пишется исход разбора.** Пока
|
||||
свёртка шла внутри обработчика, панику ловил транспорт и стоила она одного
|
||||
ответа; из фоновой горутины она валит процесс, а перезапуск берёт ту же доставку
|
||||
первой — дефект одной доставки становится циклом перезапуска, при котором приём
|
||||
не работает вовсе.
|
||||
|
||||
**Предел порядка назван вслух.** Метка приёма фиксируется раньше, чем строка
|
||||
учёта становится видимой, поэтому две одновременные доставки могут закоммитить
|
||||
строки в обратном порядке. Доставка без плотных метрик, свёрнутая раньше своей
|
||||
предшественницы, слоя не выведет и уйдёт в `failed`: её точки доедут только
|
||||
пересборкой. Окно узкое, и изменение его сужает, а не открывает, — но закрытие
|
||||
предела требует удерживать порядок на самом приёме, и это отдельный вопрос
|
||||
(беклог, блокеры).
|
||||
|
||||
Остановка формулируется **инвариантом**: приём прекращается раньше воркера, и
|
||||
после остановки не существует доставки, которая числится разобранной, а записана
|
||||
наполовину. Обещать «текущая доставка досворачивается» нельзя — бюджет остановки
|
||||
(30 с) меньше бюджета свёртки (2 мин).
|
||||
|
||||
#### Частичный разбор
|
||||
|
||||
Разбор покрывает секцию `metrics`; `workouts`, `stateOfMind`, `symptoms`, `ecg`
|
||||
|
||||
Reference in New Issue
Block a user