docs: решено оставить очередь своей таблицей коллекцией PocketBase
- заведены записка разведки job-queue-choice и ADR: готовые библиотеки River и goqite отвергнуты, захват сворачивается в один запрос с RETURNING - в architecture.md уточнён принцип «очередь таблицей» и закрыт открытый вопрос «Очередь», кроме холостого опроса - задача pocketbase-storage забрала очередь себе: границы, счётчик попыток, состояние «мертва» и оракулы
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
# Очередь остаётся своей таблицей, но коллекцией PocketBase
|
||||||
|
|
||||||
|
- **Дата:** 2026-08-11
|
||||||
|
- **Источник:** [../research/job-queue.md](../research/job-queue.md) — записка
|
||||||
|
разведки `job-queue-choice`
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Очередь задач остаётся своей таблицей и становится коллекцией PocketBase: захват
|
||||||
|
идёт одним запросом с `RETURNING`, число попыток лежит колонкой, нарастающая
|
||||||
|
пауза выражается существующим `delay_time`, а исчерпавшая попытки задача
|
||||||
|
переходит в состояние «мертва» вместо сегодняшнего `is_error = 1`. Готовую
|
||||||
|
библиотеку очереди не берём.
|
||||||
|
|
||||||
|
## Почему
|
||||||
|
|
||||||
|
Разведка искала готовую очередь и нашла, что для PocketBase её нет:
|
||||||
|
|
||||||
|
> Единственная очередь в списке — `pocketbase-queue`, написана на TypeScript и
|
||||||
|
> работает из JS-хуков; из Go её не подключить.
|
||||||
|
|
||||||
|
Отсюда разрез, который и решил дело:
|
||||||
|
|
||||||
|
> выбор идёт не между готовым и своим, а между **своим в коллекции PocketBase** и
|
||||||
|
> **чужой очередью, живущей рядом с PocketBase и мимо её панели**. River и goqite
|
||||||
|
> про PocketBase не знают.
|
||||||
|
|
||||||
|
Главный довод в пользу чужой библиотеки — транзакционный захват — снялся
|
||||||
|
замером:
|
||||||
|
|
||||||
|
> движок за `modernc.org/sqlite` v1.55.0 — версии 3.53.3, `RETURNING` в нём
|
||||||
|
> есть, и на трёх горутинах разом запись получила **ровно одна**. Это снимает
|
||||||
|
> главный довод в пользу чужой библиотеки: транзакционность захвата покупается
|
||||||
|
> одной строкой запроса, а не новой зависимостью.
|
||||||
|
|
||||||
|
Отвергнуты два кандидата, и оба с названной ценой:
|
||||||
|
|
||||||
|
> **River с драйвером SQLite** — покупает повторы, счётчик и мёртвых готовыми, но
|
||||||
|
> выносит очередь из панели PocketBase, ради которой хранилище и переезжает, и
|
||||||
|
> переписывает конвейер в цепочку задач.
|
||||||
|
>
|
||||||
|
> **goqite** — не отвечает ни на один из трёх вопросов задачи целиком, а его
|
||||||
|
> предел выдач молча теряет запись.
|
||||||
|
|
||||||
|
Запись попадает в журнал как **намеренный отказ от очевидного подхода**: взять
|
||||||
|
готовую библиотеку вместо своего кода — первое, что предлагают на такой вопрос, и
|
||||||
|
без записанной причины его предложат снова. Принцип «очередь таблицей» из
|
||||||
|
[../architecture.md](../architecture.md) этим решением подтверждён, а не
|
||||||
|
пересмотрен, поэтому парного статуса «заменено на» никакая запись не получает.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- `+` очередь видна и правится в панели администратора: мёртвая задача повторяется
|
||||||
|
снятием состояния, а не запросом в консоли сервера. Ровно за это и куплен
|
||||||
|
перевод хранилища на PocketBase
|
||||||
|
([ADR-2026-08-11-pocketbase-storage-with-admin-panel](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)).
|
||||||
|
- `+` ни одной новой зависимости: River добавил бы 46 пакетов в сборку, goqite — 3.
|
||||||
|
- `+` захват перестаёт быть двумя запросами без транзакции, и это перестаёт быть
|
||||||
|
местом, которое держится на том, что три воркера читают три разных состояния.
|
||||||
|
- `−` повторы, счётчик попыток и очередь мёртвых пишем сами, и корректность
|
||||||
|
захвата наша. Проверяется это тестами задачи `pocketbase-storage`, а не
|
||||||
|
чужим набором проверок.
|
||||||
|
- `−` захват идёт сырым запросом мимо записей PocketBase: хуки коллекции на нём
|
||||||
|
не срабатывают, и поле времени изменения проставляет наш код.
|
||||||
|
- `−` приборной панели очереди — числа ждущих, упавших, среднего времени — не
|
||||||
|
появляется. Смотрим таблицу коллекции в панели PocketBase, отбирая фильтром.
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
| Дата | Запись | Статус |
|
| Дата | Запись | Статус |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | |
|
||||||
| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | |
|
| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | |
|
||||||
| 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | |
|
| 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | |
|
||||||
|
|
||||||
|
|||||||
+14
-9
@@ -20,7 +20,10 @@
|
|||||||
делят одну базу. Отдельного воркер-процесса нет намеренно.
|
делят одну базу. Отдельного воркер-процесса нет намеренно.
|
||||||
- **Очередь таблицей.** Состояние задачи лежит в SQLite, воркер забирает работу
|
- **Очередь таблицей.** Состояние задачи лежит в SQLite, воркер забирает работу
|
||||||
запросом с захватом. Внешний брокер не заводим: нагрузка — единицы записей в
|
запросом с захватом. Внешний брокер не заводим: нагрузка — единицы записей в
|
||||||
день (оценка владельца, не замер: `research/` пуст).
|
день (оценка владельца, не замер). Готовую библиотеку очереди тоже не заводим —
|
||||||
|
решено 2026-08-11,
|
||||||
|
[ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md), сравнение
|
||||||
|
кандидатов в [research/job-queue.md](research/job-queue.md).
|
||||||
<!-- канон: поведение → openspec/specs/pipeline -->
|
<!-- канон: поведение → openspec/specs/pipeline -->
|
||||||
- **Шаг конвейера идемпотентен по повтору.** Задача, брошенная на середине,
|
- **Шаг конвейера идемпотентен по повтору.** Задача, брошенная на середине,
|
||||||
достаётся снова по истечении срока захвата и проходит шаг заново.
|
достаётся снова по истечении срока захвата и проходит шаг заново.
|
||||||
@@ -123,9 +126,8 @@
|
|||||||
раскладку на диске — решено 2026-08-11,
|
раскладку на диске — решено 2026-08-11,
|
||||||
[ADR](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md), замер панели
|
[ADR](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md), замер панели
|
||||||
в [research/pocketbase.md](research/pocketbase.md). Требование CGO этим
|
в [research/pocketbase.md](research/pocketbase.md). Требование CGO этим
|
||||||
снимается. Не решено, чем становится конвейер задач: таблицей PocketBase с тем
|
снимается. Чем становится конвейер задач, решено 2026-08-11 — см. «Очередь»
|
||||||
же захватом или чем-то другим — разведка `job-queue-choice`. Данные не
|
ниже. Данные не переносим — начинаем с чистого листа.
|
||||||
переносим — начинаем с чистого листа.
|
|
||||||
- **Учётные записи.** Вход через OIDC, провайдер — Authelia, а ответ провайдера
|
- **Учётные записи.** Вход через OIDC, провайдер — Authelia, а ответ провайдера
|
||||||
обрабатывает PocketBase, а не наш код (тот же ADR). Не решено, где живёт сессия
|
обрабатывает PocketBase, а не наш код (тот же ADR). Не решено, где живёт сессия
|
||||||
и как связываются пользователь Telegram и пользователь веба. Панель
|
и как связываются пользователь Telegram и пользователь веба. Панель
|
||||||
@@ -166,11 +168,14 @@
|
|||||||
сервис определяет содержимое сам, то ли часть записей теряется на этом.
|
сервис определяет содержимое сам, то ли часть записей теряется на этом.
|
||||||
- **Видео.** Дорожку из видеофайла бот принимает по MIME-типу `video/`, но
|
- **Видео.** Дорожку из видеофайла бот принимает по MIME-типу `video/`, но
|
||||||
конвертер этот случай не проверялся.
|
конвертер этот случай не проверялся.
|
||||||
- **Очередь.** Принцип «очередь таблицей» и захват `FindAndAcquire` написаны
|
- **Очередь.** Модель очереди решена 2026-08-11: остаётся своей таблицей и
|
||||||
вручную; чем именно захват несовершенен — [database.md](database.md),
|
становится коллекцией PocketBase, захват сворачивается в один запрос с
|
||||||
«Представление данных». Не решено здесь другое: пересматривать ли модель
|
`RETURNING`, число попыток ложится колонкой, а исчерпавшая их задача переходит
|
||||||
очереди целиком — разведка `job-queue-choice`, раньше смены хранилища, чтобы
|
в состояние «мертва» вместо `is_error = 1`
|
||||||
не переписывать захват дважды.
|
([ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md)). Пишет это
|
||||||
|
`pocketbase-storage` тем же заходом, что и хранилище. Не решено, отказываться
|
||||||
|
ли от холостого опроса: три воркера дают 259 200 запросов в сутки при нагрузке
|
||||||
|
в единицы записей в день, и во что это обходится, никто не мерил.
|
||||||
- **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать
|
- **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать
|
||||||
счётчики через `client_golang` или перейти на OpenTelemetry с трассировкой —
|
счётчики через `client_golang` или перейти на OpenTelemetry с трассировкой —
|
||||||
решает разведка `opentelemetry-fit`. Коллектор был бы процессом, которого в
|
решает разведка `opentelemetry-fit`. Коллектор был бы процессом, которого в
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
## Как снималось
|
## Как снималось
|
||||||
|
|
||||||
На живом потоке не снималось ничего: поведение внешних сервисов на границах не
|
На живом потоке не снималось ничего: поведение внешних сервисов на границах не
|
||||||
проверяли. Единственная запись сделана на пустой базе в песочнице.
|
проверяли. Обе записи сделаны в песочнице на пустой базе.
|
||||||
|
|
||||||
Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex
|
Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex
|
||||||
SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит
|
SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит
|
||||||
@@ -21,4 +21,5 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
|||||||
|
|
||||||
| Дата | Запись | О чём |
|
| Дата | Запись | О чём |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2026-08-11 | [Очередь задач: своя таблица против готовой библиотеки](job-queue.md) | Цена River и goqite в пакетах, захват одним запросом, чего нет для PocketBase |
|
||||||
| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 |
|
| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 |
|
||||||
|
|||||||
@@ -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 запросов в сутки посчитаны, а
|
||||||
|
во что они обходятся на файле базы — нет. Процесс один, и разбудить воркер
|
||||||
|
внутри него можно каналом, но задачи на это нет.
|
||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
- [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
|
- [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
|
||||||
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
|
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
|
||||||
- [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами.
|
- [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами.
|
||||||
- [🧹 Перевести хранилище и файлы записей на встроенный PocketBase](items/pocketbase-storage.md) — Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем, кроме консоли на сервере: панель PocketBase покажет и то и другое, только если они переедут к ней.
|
- [🧹 Перевести хранилище и файлы записей на встроенный PocketBase](items/pocketbase-storage.md) — Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем, кроме консоли на сервере: панель PocketBase покажет их, только если они переедут к ней. Заодно переписывается захват задачи: сегодня это два запроса без транзакции, а падающая всегда задача падает вечно.
|
||||||
- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору.
|
- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору.
|
||||||
- [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
|
- [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
|
||||||
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- **Тип:** chore
|
- **Тип:** chore
|
||||||
- **Категория:** Очередь
|
- **Категория:** Очередь
|
||||||
- **Зачем:** Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем, кроме консоли на сервере: панель PocketBase покажет и то и другое, только если они переедут к ней.
|
- **Зачем:** Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем, кроме консоли на сервере: панель PocketBase покажет их, только если они переедут к ней. Заодно переписывается захват задачи: сегодня это два запроса без транзакции, а падающая всегда задача падает вечно.
|
||||||
|
|
||||||
PocketBase встраивается библиотекой в тот же бинарник и приносит хранилище,
|
PocketBase встраивается библиотекой в тот же бинарник и приносит хранилище,
|
||||||
учётные записи и панель администратора разом. Конвейер и оба входа работают
|
учётные записи и панель администратора разом. Конвейер и оба входа работают
|
||||||
@@ -20,12 +20,22 @@ PocketBase встраивается библиотекой в тот же бин
|
|||||||
Данные не переносим — база заводится с чистого листа, и это решение принято
|
Данные не переносим — база заводится с чистого листа, и это решение принято
|
||||||
сознательно.
|
сознательно.
|
||||||
|
|
||||||
|
**Очередь переписывается тем же заходом.** Разведка `job-queue-choice` 2026-08-11
|
||||||
|
[отвергла готовые библиотеки](../../docs/adr/ADR-2026-08-11-queue-as-pocketbase-collection.md):
|
||||||
|
очередь остаётся своей таблицей, но становится коллекцией PocketBase. Захват
|
||||||
|
сворачивается в один запрос с `RETURNING`, число попыток ложится колонкой,
|
||||||
|
нарастающая пауза выражается существующим `delay_time`, а исчерпавшая попытки
|
||||||
|
задача переходит в состояние «мертва» вместо `is_error = 1`. Сравнение
|
||||||
|
кандидатов — [docs/research/job-queue.md](../../docs/research/job-queue.md).
|
||||||
|
|
||||||
## Затрагивает
|
## Затрагивает
|
||||||
|
|
||||||
- таблицы `files` и `transcribe_jobs`, каталог `migrations/` и весь механизм
|
- таблицы `files` и `transcribe_jobs`, каталог `migrations/` и весь механизм
|
||||||
goose;
|
goose;
|
||||||
- `internal/adapter/repo/sqlite` целиком, включая захват задачи через
|
- `internal/adapter/repo/sqlite` целиком, включая захват задачи через
|
||||||
`FindAndAcquire`;
|
`FindAndAcquire`;
|
||||||
|
- состав колонок очереди: прибавляется число попыток, а `is_error` уступает
|
||||||
|
место состоянию «мертва» в перечне состояний задачи;
|
||||||
- `internal/contract`, интерфейсы `FileRepository` и `TranscriptJobRepository`;
|
- `internal/contract`, интерфейсы `FileRepository` и `TranscriptJobRepository`;
|
||||||
- ключ конфигурации `[database] path`, ключ `[storage] path` и раскладка
|
- ключ конфигурации `[database] path`, ключ `[storage] path` и раскладка
|
||||||
каталога `data/`;
|
каталога `data/`;
|
||||||
@@ -47,14 +57,19 @@ PocketBase встраивается библиотекой в тот же бин
|
|||||||
- Сервис поднимается на чистом каталоге данных, накатывает свою схему сам и
|
- Сервис поднимается на чистом каталоге данных, накатывает свою схему сам и
|
||||||
принимает запись обоими входами. Оракул — запуск на пустом `data/` и прогон
|
принимает запись обоими входами. Оракул — запуск на пустом `data/` и прогон
|
||||||
записи из Telegram и через `POST /api/audio` до состояния `done`.
|
записи из Telegram и через `POST /api/audio` до состояния `done`.
|
||||||
- Захват задачи воркером не выдаёт одну запись двум вызывающим. Оракул — тест
|
- Захват задачи воркером идёт одним запросом и не выдаёт одну запись двум
|
||||||
на трёх параллельных вызовах захвата по одному состоянию: ровно один
|
вызывающим. Оракул — тест на трёх параллельных вызовах захвата по одному
|
||||||
получает запись.
|
состоянию: ровно один получает запись.
|
||||||
- Задача, брошенная на середине, достаётся снова по истечении срока захвата.
|
- Задача, брошенная на середине, достаётся снова по истечении срока захвата, а
|
||||||
Оракул — тест с проставленным задним числом `acquire_time`.
|
падающая всегда — уходит в «мертва» и из выборки исчезает. Оракулы — тест с
|
||||||
- Принятая запись видна в панели строкой и скачивается из неё файлом. Оракул —
|
проставленным задним числом `acquire_time` и тест с шагом, падающим на каждой
|
||||||
прогон записи через `POST /api/audio` на пустом каталоге, затем запрос
|
попытке: после заданного их числа задача не выдаётся, а её состояние видно
|
||||||
`/api/files/...` за тем же файлом: длина совпадает с загруженной.
|
отбором.
|
||||||
|
- Принятая запись видна в панели строкой и скачивается по ссылке
|
||||||
|
`/api/files/...` тем же файлом. Оракулы — прогон записи через
|
||||||
|
`POST /api/audio` на пустом каталоге, затем поиск её строки в коллекции задач
|
||||||
|
на `/_/` по идентификатору и запрос `/api/files/...` за тем же файлом: длина
|
||||||
|
совпадает с загруженной.
|
||||||
- `docs/database.md` описывает новую схему, а старые упоминания goose и goqu из
|
- `docs/database.md` описывает новую схему, а старые упоминания goose и goqu из
|
||||||
документов канона убраны. Оракул — `task gate`, шаг `docs.py check`.
|
документов канона убраны. Оракул — `task gate`, шаг `docs.py check`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user