хранилище, файлы записей и очередь переведены на встроенную PocketBase
- записи, метаданные и файлы съехались под один каталог данных; появилась панель владельца, а gin, goqu, goose и требование CGO ушли - захват задачи стал одним запросом с RETURNING; заведены число попыток, состояние dead и нарастающая пауза вместо признака is_error - имя файла в хранилище задаёт сервис и в журнал не идёт: вместе с идентификатором записи оно собирало бы ссылку на скачивание
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# PocketBase: умолчания, которые ломают штатный сценарий
|
||||
|
||||
Наблюдения, снятые по ходу задачи `pocketbase-storage` уже на своём коде. От
|
||||
[записки разведки](pocketbase.md) отличаются предметом: та мерила, **что даёт
|
||||
панель**, эта — **что библиотека делает молча**, если её не переубедить.
|
||||
|
||||
Все четыре наблюдения нашлись ревью, а не чтением документации: три из них
|
||||
выглядят как «значение по умолчанию — нет ограничения», а значат обратное.
|
||||
|
||||
## Как снималось
|
||||
|
||||
Версия **0.39.10**, та же, что у первой записки. Прогоны — на пустом каталоге
|
||||
данных во временном каталоге и на поднятом сервере `127.0.0.1:18099`; боевые
|
||||
данные и ключи не участвовали. Числа ниже сняты 2026-08-11 и 2026-08-12.
|
||||
|
||||
## Нулевой потолок у поля файла значит 5 МиБ, а не «без предела»
|
||||
|
||||
`&core.FileField{MaxSize: 0}` читается библиотекой как её собственное умолчание:
|
||||
|
||||
```
|
||||
core/field_file.go:28 const DefaultFileFieldMaxSize int64 = 5 << 20
|
||||
core/field_file.go:310 if f.MaxSize <= 0 { return DefaultFileFieldMaxSize }
|
||||
```
|
||||
|
||||
Проверено укладкой: файл в 6 МиБ отвергается на сохранении записи —
|
||||
`the maximum allowed file size is 5242880 bytes`. Прогон через боевой роутер дал
|
||||
границу дословно:
|
||||
|
||||
| тело запроса | ответ |
|
||||
| --- | --- |
|
||||
| 4 194 304 байта | `201` |
|
||||
| 5 238 784 байта | `201` |
|
||||
| 5 246 976 байт | `500` |
|
||||
| 34 603 008 байт | `413` |
|
||||
|
||||
**Цена для сервиса:** 5 МиБ — это примерно 5,5 минут mp3 при 128 кбит/с. Отвергалась
|
||||
бы не только длинная запись на приёме: результат конвертации в ogg переваливает
|
||||
тот же порог примерно на пятой минуте, и **уже принятая** задача исчерпывала бы
|
||||
попытки на шаге конвертации.
|
||||
|
||||
## Тело запроса режется на 32 МиБ раньше обработчика
|
||||
|
||||
`apis/base.go:36` вешает `BodyLimit(DefaultMaxBodySize)` на **корневой** роутер,
|
||||
то есть и на чужие маршруты; `apis/middlewares_body_limit.go:14` —
|
||||
`const DefaultMaxBodySize int64 = 32 << 20`. Ответ `413` уходит мимо обработчика,
|
||||
без строки в журнале приёма (последняя строка таблицы выше).
|
||||
|
||||
Снимается на маршруте: `.Bind(apis.BodyLimit(<своё число>))`.
|
||||
|
||||
## Таймаут чтения запроса — пять минут
|
||||
|
||||
`apis/serve.go:151` ставит `ReadTimeout: 5 * time.Minute`. Заливка шестичасовой
|
||||
записи по медленному каналу его переживает: соединение рвётся на середине.
|
||||
Снимается в хуке `OnServe` — `se.Server.ReadTimeout = 0`.
|
||||
|
||||
## Суффикс к имени файла дописывает конструктор, а не укладка
|
||||
|
||||
Первая записка наблюдала `sample.ogg → sample_uztrv6wvz3.ogg` и читала это как
|
||||
свойство хранилища. Наблюдение верно **только когда имя строит сама библиотека**:
|
||||
десять случайных знаков добавляет `normalizeName`, вызываемый из
|
||||
`filesystem.NewFileFrom*`. Имя, положенное в поле `File.Name` после
|
||||
конструктора, ложится на диск дословно:
|
||||
|
||||
```
|
||||
задано 11111111-2222-3333-4444-555555555555.mp3
|
||||
на диске 11111111-2222-3333-4444-555555555555.mp3
|
||||
```
|
||||
|
||||
**Цена:** тот, кто задаёт имя сам, не получает от суффикса никакой
|
||||
неугадываемости — и защищать ссылку на файл ему приходится другим.
|
||||
|
||||
## Хук правки записи не различает, кто пишет
|
||||
|
||||
`app.OnRecordUpdate(<коллекция>)` — событие **модели**: оно срабатывает на каждом
|
||||
`app.Save`, включая сохранение из собственного кода. Хук, написанный «для
|
||||
панели», правил записи конвейера: проверено прогоном — задержка, поставленная
|
||||
шагом вместе со сменой состояния, обнулялась тем же сохранением.
|
||||
|
||||
Различает источник `app.OnRecordUpdateRequest(<коллекция>)`: оно поднимается
|
||||
только на правку запросом, а код, пишущий мимо HTTP-слоя, под него не попадает.
|
||||
|
||||
## Приглашение завести владельца панели живёт полчаса
|
||||
|
||||
`apis/installer.go:31` — `systemSuperuser.NewStaticAuthToken(30 * time.Minute)`;
|
||||
печатается только пока владельца нет (`needInstallerSuperuser`). Проверено
|
||||
прогоном: при первом запуске строка со ссылкой в журнале есть, после заведения
|
||||
владельца при следующем запуске её нет.
|
||||
|
||||
## Чего эта записка не узнала
|
||||
|
||||
- **Во что обходится потолок в 8 ГиБ на диске.** Число выбрано расчётом из
|
||||
шестичасовой записи с запасом на видео, а не замером: настоящего распределения
|
||||
длин у сервиса нет.
|
||||
- **Как ведёт себя укладка файла в несколько гигабайт.** Самая длинная проверенная
|
||||
запись — 9,6 МБ (десять минут mp3). Потоковую укладку это подтверждает, предел
|
||||
— нет.
|
||||
- **Поведение под одновременной правкой панели и конвейера в бою.** Проверено
|
||||
тестом на одной машине, не живой нагрузкой.
|
||||
Reference in New Issue
Block a user