docs: решено оставить очередь своей таблицей коллекцией PocketBase
- заведены записка разведки job-queue-choice и ADR: готовые библиотеки River и goqite отвергнуты, захват сворачивается в один запрос с RETURNING - в architecture.md уточнён принцип «очередь таблицей» и закрыт открытый вопрос «Очередь», кроме холостого опроса - задача pocketbase-storage забрала очередь себе: границы, счётчик попыток, состояние «мертва» и оракулы
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# Очередь задач: своя таблица против готовой библиотеки
|
||||
|
||||
Отвечает на вопрос разведки `job-queue-choice`: брать ли готовую очередь на Go
|
||||
поверх той же встроенной базы или оставить свою таблицу, дописав к ней повторы,
|
||||
счётчик попыток и очередь мёртвых задач. Разведка шла перед `pocketbase-storage`,
|
||||
потому что смена хранилища переписывает захват задачи в любом случае.
|
||||
|
||||
Внешнего брокера — Redis, RabbitMQ, NATS — не рассматривали по рамке задачи: он
|
||||
добавляет к выкладке процесс, которого там нет, ради нагрузки в единицы записей
|
||||
в день.
|
||||
|
||||
## Как снималось
|
||||
|
||||
Дата замеров — 2026-08-11. Всё считал в каталоге вне репозитория, который
|
||||
удалён вместе с песочницей; боевые данные не участвовали.
|
||||
|
||||
- **Цена зависимости.** Завёл пустой модуль на Go 1.24 с одним PocketBase
|
||||
0.39.10, затем его копии с добавленной библиотекой. Пакеты в сборке —
|
||||
`CGO_ENABLED=0 go list -deps .`, модули в графе — `go list -m all`.
|
||||
- **Захват одним запросом.** Программа на 40 строк в той же песочнице: таблица
|
||||
из одной строки, три горутины разом выполняют один и тот же запрос
|
||||
`UPDATE … WHERE id = (SELECT … LIMIT 1) RETURNING …`. Драйвер —
|
||||
`modernc.org/sqlite` v1.55.0, тот самый, которым ходит в базу PocketBase,
|
||||
режим журнала WAL, таймаут занятости 5 секунд.
|
||||
- **Свойства библиотек** взяты из их документации, а не замерены: пометки
|
||||
«объявлено» ниже стоят именно там.
|
||||
- **Холостой опрос** не мерил, а посчитал: три воркера и пауза 1 секунда из
|
||||
[../database.md](../database.md), «Настройки с числовым значением», дают
|
||||
3 × 86 400 = **259 200 запросов к базе в сутки** независимо от того, есть ли
|
||||
работа.
|
||||
|
||||
## Готовой очереди для PocketBase на Go нет
|
||||
|
||||
Проверил по списку экосистемы `awesome-pocketbase` и по обсуждениям в
|
||||
репозитории PocketBase. Единственная очередь в списке — `pocketbase-queue`,
|
||||
написана на TypeScript и работает из JS-хуков; из Go её не подключить. Она
|
||||
заводит три коллекции (`queue_tasks`, `queue_locks`, `queue_stats`), упавшие
|
||||
задачи держит с текстом ошибки семь дней и объявляет 50–60 задач в секунду на
|
||||
четырёх воркерах. Ни нарастающей паузы, ни счётчика попыток у неё нет.
|
||||
|
||||
Автор PocketBase в обсуждении № 2101 советует ровно свою коллекцию с полями
|
||||
«имя, данные, состояние» и обход её по расписанию, а про встроенную очередь
|
||||
говорит: «очередь писем, а может и общая очередь задач, есть в моих планах, но
|
||||
пока приоритет низкий». Планировщик у PocketBase свой, `app.Cron()`.
|
||||
|
||||
Отсюда разрез сравнения: выбор идёт не между готовым и своим, а между **своим в
|
||||
коллекции PocketBase** и **чужой очередью, живущей рядом с PocketBase и мимо её
|
||||
панели**. River и goqite про PocketBase не знают.
|
||||
|
||||
## Захват чинится одним запросом
|
||||
|
||||
Сегодняшний захват — два запроса подряд без транзакции
|
||||
([../database.md](../database.md), «Представление данных»). Замер показал, что
|
||||
после перехода на PocketBase он сворачивается в один: движок за
|
||||
`modernc.org/sqlite` v1.55.0 — версии 3.53.3, `RETURNING` в нём есть, и на трёх
|
||||
горутинах разом запись получила **ровно одна**.
|
||||
|
||||
Это снимает главный довод в пользу чужой библиотеки: транзакционность захвата
|
||||
покупается одной строкой запроса, а не новой зависимостью.
|
||||
|
||||
## Кандидаты
|
||||
|
||||
| | Своя таблица коллекцией | River 0.43.0 | goqite 0.4.0 |
|
||||
| --- | --- | --- | --- |
|
||||
| Пакетов в сборке сверх PocketBase | 0 | 46 | 3 |
|
||||
| Модулей в графе сверх PocketBase | 0 | 18 | 7 |
|
||||
| Требует CGO | нет | нет | нет |
|
||||
| Видна в панели PocketBase | да, правится | нет | нет |
|
||||
| Повторы с нарастающей паузой | писать | есть | нет |
|
||||
| Счётчик попыток | писать | есть | есть, предел выдач |
|
||||
| Очередь мёртвых задач | писать | есть, состояние «отброшена» | нет |
|
||||
| Ожидание без траты попытки | писать | есть | нет |
|
||||
|
||||
Числа зависимостей: с одним PocketBase в сборке 122 внешних пакета и 72 модуля
|
||||
в графе; с River и его драйвером SQLite — 168 и 90; с goqite и его пакетом
|
||||
задач — 125 и 79. Ни в одной сборке `mattn/go-sqlite3` не участвует: у goqite он
|
||||
значится в графе, но только как зависимость его собственных проверок, и при
|
||||
`CGO_ENABLED=0` всё три варианта собираются.
|
||||
|
||||
### River
|
||||
|
||||
Драйвер SQLite (`riverdriver/riversqlite`) появился в версии 0.23.0 и авторами
|
||||
объявлен опытным: «схема ещё может быть слегка изменена, прежде чем её сочтут
|
||||
окончательной». Объявленная скорость — четверть от той, что даёт Postgres, около
|
||||
10 000 задач в секунду; для единиц записей в день это запас, которым мы не
|
||||
воспользуемся. Свою веб-панель River даёт встраиваемым обработчиком, отдельного
|
||||
процесса она не требует.
|
||||
|
||||
Ложится на нашу задачу River лучше всех по одному месту: ожидание операции
|
||||
SpeechKit длиной до суток выражается его отложением, и попытка при этом не
|
||||
тратится. Всё остальное против:
|
||||
|
||||
- очередь становится **цепочкой задач вместо состояния в таблице**, а это
|
||||
переписывание `internal/service`, а не хранилища;
|
||||
- свои таблицы River заводит сам, и панель PocketBase их не покажет: она знает
|
||||
только свои коллекции. Показать их можно коллекцией-представлением, и та
|
||||
**только для чтения** — повторить мёртвую задачу из панели не выйдет;
|
||||
- панелей становится две, и у второй свой вход, который тоже надо закрывать на
|
||||
обратном прокси;
|
||||
- документация советует пул в одно соединение, чтобы не ловить отказ по
|
||||
занятости, — поверх файла, который уже держит PocketBase.
|
||||
|
||||
### goqite
|
||||
|
||||
Самая дешёвая по зависимостям и самая бедная по существу. Сообщение — двоичное
|
||||
тело в одной колонке: в панели оно нечитаемо. По умолчанию срок невидимости 5
|
||||
секунд и предел выдач 3; нарастающей паузы нет, очереди мёртвых задач нет —
|
||||
исчерпавшее предел сообщение просто перестаёт выдаваться. Это молчаливая потеря
|
||||
принятой записи, а она запрещена инвариантом «Принятая запись не теряется молча»
|
||||
([../../CLAUDE.md](../../CLAUDE.md), «Инварианты»). То есть счётчик попыток и
|
||||
очередь мёртвых пришлось бы дописывать и поверх goqite — ровно то, ради чего
|
||||
разведка затевалась.
|
||||
|
||||
## Что решено и от чего отказались
|
||||
|
||||
Решение — **своя таблица, но коллекцией PocketBase**: захват одним запросом с
|
||||
`RETURNING`, счётчик попыток колонкой, нарастающая пауза через существующий
|
||||
`delay_time`, состояние «мертва» вместо `is_error = 1`. Записано в
|
||||
[ADR-2026-08-11-queue-as-pocketbase-collection](../adr/ADR-2026-08-11-queue-as-pocketbase-collection.md).
|
||||
|
||||
Отвергнуты:
|
||||
|
||||
- **River с драйвером SQLite** — покупает повторы, счётчик и мёртвых готовыми, но
|
||||
выносит очередь из панели PocketBase, ради которой хранилище и переезжает, и
|
||||
переписывает конвейер в цепочку задач. Опытный драйвер со сменной схемой
|
||||
добавляет к этому обязанность следить за чужими миграциями;
|
||||
- **goqite** — не отвечает ни на один из трёх вопросов задачи целиком, а его
|
||||
предел выдач молча теряет запись;
|
||||
- **`pocketbase-queue`** — на TypeScript, из Go не подключается.
|
||||
|
||||
## Чего разведка не узнала
|
||||
|
||||
- **Сколько стоит написать недостающее.** Объём работы по повторам, счётчику
|
||||
попыток и мёртвым задачам не оценивался: он входит в
|
||||
`pocketbase-storage`, которая переписывает репозиторий целиком.
|
||||
- **Ложится ли суточное ожидание операции SpeechKit на River без сюрпризов.**
|
||||
Проверка стоит написания кода, а выбранному способу она не нужна вовсе.
|
||||
- **Нужен ли отказ от холостого опроса.** 259 200 запросов в сутки посчитаны, а
|
||||
во что они обходятся на файле базы — нет. Процесс один, и разбудить воркер
|
||||
внутри него можно каналом, но задачи на это нет.
|
||||
Reference in New Issue
Block a user