- один факт — один дом: рецепт локального входа, правило чтения X-Forwarded-For, уровень строки журнала и опись опор изъятия сведены к своим домам, копии заменены ссылками - форма [auth.test_headers] выровнена по образцу конфига в семи местах; сценарии intake и archive перестали ссылаться на сессию, которой сервис не выдаёт - поправлены протухшие факты: ключ объекта строит ULID, а не UUID; сверку адреса пира зовут трое, а не двое; обзор capability access знает о задаче 2026-08-23
44 KiB
Схема хранилища
База, таблицы, раскладка файлов, правило времени и идентификаторов.
Хранилище своё: база SQLite через modernc.org/sqlite (CGO сборке не нужен)
и файлы записей своим каталогом рядом с ней. Ключ конфигурации один —
[storage] data_dir, умолчание data. Встроенная PocketBase, державшая до
2026-08-22 и базу, и файлы, и панель, и маршрутизатор, ушла из проекта целиком —
задача storage-without-pocketbase,
ADR.
База принимает одного писателя. Пишущий пул держит одно соединение — драйвер пишет единственным, и несколько воркеров, пришедших писать разом мимо этого правила, получают отказ по занятости на записи результата шага, то есть после оплаченной работы. Чтение идёт отдельным пулом: в журнале упреждающей записи читатели не мешают писателю.
Журнал упреждающей записи, соблюдение внешних ключей и ожидание занятой базы задаются строкой подключения обоих пулов, а не запросом после открытия: две из трёх настроек в SQLite принадлежат соединению, а не базе, а пул заводит новые соединения по мере надобности — запрос настроил бы одно из многих. Операция, которая читает и следом пишет, идёт целиком по пишущему соединению: читающую транзакцию SQLite до пишущей не повышает и отказывает по занятости немедленно.
Схему двигают шаги github.com/pressly/goose/v3 — библиотекой, а не
командной строкой. Каталог internal/adapter/repo/sqlite/migrations, файл на
шаг, версия шага — число в начале имени файла. Перечень шагов приходит
провайдеру доводом, провайдер заводится в точке входа и получает пишущий пул,
накат идёт до подъёма входов и до старта воркеров, а отказ шага роняет старт.
Применённый шаг не переписывается — изменение только новым шагом.
Шаг и отметка о нём идут одной транзакцией: библиотека открывает её на том же соединении. Порядок шагов детерминирован и выводится из версии, а не из порядка чтения каталога; две одинаковых версии дают отказ сбора.
Исключающую блокировку наката держим сами. Библиотека под SQLite её не
поставляет вовсе — её запиратели объявлены только для PostgreSQL, а провайдер без
запирателя накатывает без всякой блокировки. Замок берётся на файле
data/migrate.lock (syscall.Flock, LOCK_EX) и снимается закрытием
дескриптора; с умершим процессом его снимает ядро, поэтому просроченного замка,
который надо чистить руками, не остаётся.
Каталог у шагов свой, а не файл внутри пакета репозитория, и причина внешняя: шаг гейта сверяет изменённые шаги схемы с правкой этого документа по префиксу пути, а префикс наводится только на каталог. Где этот префикс задан — conventions/go-linters.md, «Механизировано».
Идентификаторы — ULID в нижнем регистре, TEXT, 26 знаков алфавита
Crockford. Выдаёт их приложение единой точкой internal/ident; внутри одной
миллисекунды выдача монотонна, потому что колонка времени несёт секунды и
порядок записей одной секунды задаёт ключ. Идентификатор, пришедший снаружи,
разбирается на границе: разбор проверяет вид и приводит регистр, а негодный
считается несуществующей записью и до базы не доходит.
Тем же идентификатором зовётся подкаталог записи в каталоге данных, а имя
файла внутри него — <ULID><расширение>.
Время — TEXT в RFC 3339, UTC, суффикс Z, секундная точность:
2006-01-02T15:04:05Z. Ширина записи постоянная, поэтому лексикографический
порядок совпадает с хронологией. Вид один на все колонки времени, включая
те, что пишет только сам сервис: своего типа времени у SQLite нет, колонка
хранит то, что в неё положили, и колонка, заполненная то одним видом, то другим,
обратила бы условие срока протухания захвата в постоянную истину или ложь молча.
Время ставит приложение единой точкой internal/clock. Умолчаний вида
CURRENT_TIMESTAMP в схеме нет: умолчание писало бы свой вид времени, а
вставка, забывшая проставить время, при нём прошла бы молча.
Таблицы
Перечни значений держит код, а не схема. Прежде рубеж, причина остановки и
вид текста были закрыты CHECK-подобным типом хранилища, потому что панель
владельца правила запись руками и вправе была завести значение, которого сервис
не знает. Панели нет, правка идёт только нашим кодом, и закрытый перечень в схеме
остался бы ценой — новое значение стоило бы нового шага — без покупателя.
users
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID, выдаёт приложение |
provider_login |
TEXT, уникален | Логин человека у провайдера: то значение, которым его называет обратный прокси заголовком Remote-User. Ключ учётной записи |
name |
TEXT | Имя, пригодное к показу; берётся при заведении и вторым обращением не переписывается |
email |
TEXT | Адрес почты; необязателен |
created_at, updated_at |
TEXT | Время |
Уникальность почты держится частичным индексом (WHERE email <> ''), поэтому
записи без почты уживаются друг с другом. Уникальность логина — обычным.
Ключом почта не служит вовсе: адрес меняется, и первое обращение с чужим адресом досталось бы чужой записи.
files
Одна строка на одну физическую копию. Копий у аудиозаписи ровно две: принятая и приведённая к рабочему формату. Копия во внешнем хранилище файлом записи не считается — она существует только потому, что провайдер распознавания читает аудио по адресу, и её ключ живёт в строке попытки распознавания.
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID |
owner_id |
TEXT → users(id) |
Владелец копии; пустого значения не принимает |
record_id |
TEXT | Запись, которой копия принадлежит: имя её подкаталога |
file_name |
TEXT | Имя файла в этом подкаталоге; задаёт сервис |
size_bytes |
INTEGER | Размер копии в байтах |
format |
TEXT | Расширение без точки, в нижнем регистре |
duration_ms |
INTEGER | Длительность, если её удалось прочитать |
created_at |
TEXT | Время |
Внешнего ключа на аудиозапись у record_id нет намеренно. Приём заводит
файл до самой записи — подкаталог назван её идентификатором, и знать его надо
раньше, — и обязательная связь отвергала бы первую же принятую запись. Владелец
при этом лежит своей колонкой, а не выводится через запись: файл переживает свою
запись, и заведённый шагом до её сохранения остаётся с владельцем и без ссылки.
audio_records
Аудиозапись — центральная сущность сервиса. Домен, поля очереди и ссылки на приложения лежат здесь; содержимое — по ссылкам, отдельными строками.
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID |
owner_id |
TEXT → users(id) |
Владелец записи; пустого значения не принимает |
title, brief |
TEXT | Заголовок и краткое описание: читаются вместе со списком |
original_filename |
TEXT | Имя файла, данное отправителем; кладёт приём, обрезав по пределу и убрав управляющие знаки |
duration_ms |
INTEGER, обязателен | Длительность принятого, миллисекунды; ставит приём и всегда |
size_bytes |
INTEGER, обязателен | Размер принятого, байты |
state |
TEXT | Рубеж: uploaded, normalized, submitted, transcribed, done |
state_entered_at |
TEXT | Время входа в рубеж — сторож застревания |
halted_at |
TEXT | Признак остановки; рубеж при ней не стирается |
halt_reason |
TEXT | step_failed, attempts_exhausted, stuck |
error_text |
TEXT | Текст ошибки, машинный |
acquisition_id |
TEXT | Признак этого захвата, уникальный для каждого |
acquire_expires_at |
TEXT | Срок протухания захвата; приезжает с рубежом |
delay_time |
TEXT | Не брать запись раньше этого времени |
attempts |
INTEGER | Число отказов: растёт при захвате, обнуляется на шаге без отказа и на откладывании |
original_file_id |
TEXT → files(id) |
Принятая копия |
normalized_file_id |
TEXT → files(id) |
Копия, приведённая к рабочему формату |
transcript_text_id, literary_text_id |
TEXT | Тексты записи |
structure_id |
TEXT | Структура реплик |
recognition_id |
TEXT | Попытка распознавания |
created_at, updated_at |
TEXT | Время |
Индексов два. idx_audio_records_acquire — (state, halted_at, created_at, id):
по нему идёт отбор захвата, и по нему же он берёт запись в определённом порядке.
idx_audio_records_owner_page — (owner_id, created_at, id): под страницу
списка, сужаемую владельцем и режущуюся полным ключом сортировки.
Оба индекса заведены начальным шагом, а не отложены: применённый шаг схемы не
переписывается, и добавление индекса стоило бы отдельного шага. Проверено
EXPLAIN QUERY PLAN: ни отбор захвата, ни страница списка не показывают полного
сканирования таблицы.
Ведущая колонка у ленты — владелец, и потому индекс захвата ей не помогает
ничем. Замер на задаче json-api-for-spa 2026-08-15: без своего индекса
страница сканировала таблицу целиком и досортировывала результат во временном
дереве, а рост архива с 5 тысяч строк до 200 тысяч растил время одной страницы
владельца в двадцать-тридцать раз — при неизменных сорока его собственных
записях. Цена росла с чужими записями, потому что сервис объявлен архивом и
хранит их бессрочно.
Имя файла и заголовок — разные колонки. Заголовок несёт название, которое дал человек либо посчитала языковая модель; имя файла — то, по чему человек узнаёт свою запись, пока заголовка нет. Одной колонкой на оба смысла посчитанное название затирало бы имя, и вернуть затёртое было бы неоткуда. Имя приходит извне, поэтому приём режет его по пределу и убирает управляющие знаки; в имя файла на диске и в журнал оно по-прежнему не идёт.
Длительность и размер лежат и на записи, и на её файле, и равенство между ними не поддерживается никем — намеренно. На записи снимок принятого, взятый приёмом один раз; на файле — величины нынешней копии. Уточнение длительности меняет вторые и не трогает первые: это разные вопросы — «что человек прислал» и «что лежит сейчас». Колонками записи они нужны потому, что показываются в списке, а список читается без содержимого. Решение владельца от 2026-08-15.
«Неизвестно» эти колонки не выражают, и это то же решение владельца: обе величины ставит приём и ставит всегда — запись с непрочитанными метаданными отвергается отказом и не заводится вовсе. Обе объявлены обязательными: пустое значение, которое схема теперь допустить может, завело бы третий смысл, которого никто не читает.
Ссылки на файлы две и порознь. Прежняя модель держала одну и переставляла её каждым шагом: у прошедшей конвейер записи она вела на копию во внешнем хранилище, и принятого человеком файла не найти было ничем.
Остановка — признак, а не рубеж. Прежние состояния failed и dead
схлопнуты в halted_at с причиной: обе восстанавливаются одинаково — снятием
признака, — и различие между ними перестало быть структурным.
Сторожей двое. attempts ограничивает повторы внутри шага,
state_entered_at — застревание. Прежде обе обязанности несло одно число, и не
справлялось ни с одной.
record_topics
| Поле | Тип | Что |
|---|---|---|
record_id |
TEXT → audio_records(id) |
Запись |
topic_id |
TEXT → topics(id) |
Тема |
Первичный ключ — пара целиком. Потолок в пять тем на запись держит триггер: без него часовой разговор даёт два десятка тем, и словарь распухает за неделю. Число берётся у домена — то же самое, которое сервис объявляет приложению.
texts
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID |
record_id |
TEXT → audio_records(id) |
Чья это расшифровка |
kind |
TEXT | transcript или literary |
contents |
TEXT | Сам текст |
created_at, updated_at |
TEXT | Время |
Пара «запись и вид» уникальна: повтор прерванного шага не заводит второй строки.
Поле зовётся kind, а не format: словом format в этой же схеме зовут формат
файла.
structures
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID |
record_id |
TEXT → audio_records(id) |
Чья это структура |
version |
INTEGER | Версия вида разбора |
contents |
TEXT | Реплики со временем, JSON |
created_at, updated_at |
TEXT | Время |
Пара «запись и версия разбора» уникальна. Номер версии нужен потому, что разбор сохранённого ответа изменится раньше, чем архив пересчитают.
recognitions
Попытка распознавания у внешнего провайдера — всё, что зависит от него.
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID |
record_id |
TEXT → audio_records(id) |
Чья это попытка |
provider, model |
TEXT | Кем и какой моделью считано |
external_id |
TEXT | Идентификатор операции у провайдера |
source_uri |
TEXT | Адрес, по которому провайдер читает аудио |
payload_file |
TEXT | Имя файла с сохранённым ответом провайдера |
started_at, finished_at |
TEXT | Границы операции |
created_at, updated_at |
TEXT | Время |
Сохранённый ответ лежит третьим файлом в подкаталоге записи, а не колонкой. Шаг опроса читает эту строку раз в несколько секунд, а репозиторий читает строку целиком: ответ на многочасовую запись, положенный колонкой, ехал бы в память при каждом опросе. Хранится он потому, что результат операции у провайдера не переспрашивается. Копией аудио он при этом не считается — их у записи по-прежнему две, — и адреса, которым его читают снаружи, у сервиса нет вовсе.
record_events
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID |
record_id |
TEXT → audio_records(id) |
Чьё это событие |
origin |
TEXT | pipeline или human |
step |
TEXT | Имя шага |
outcome |
TEXT | done, failed, halted, resumed |
outcome_text |
TEXT | Причина, если она есть |
duration_ms |
INTEGER | Сколько шаг занял |
created_at |
TEXT | Время |
Колонка текста зовётся outcome_text, а не error_text: последнее имя названо
поимённо инвариантом о секрете, и две колонки с этим именем сделали бы инвариант
двусмысленным.
Журнал пишется на смену рубежа, на остановку и на возврат в работу — не на
каждое откладывание опроса. Ни один шаг конвейера его не читает, чтобы решить,
что делать дальше. Происхождение human пишет сегодня подкоманда оснастки,
возвращающая остановленную запись в работу: другого писателя, кроме конвейера, у
журнала не осталось.
topics
| Поле | Тип | Что |
|---|---|---|
id |
TEXT PK | ULID |
owner_id |
TEXT → users(id) |
Чей это словарь |
name |
TEXT | Название темы |
created_at, updated_at |
TEXT | Время |
Пара «владелец и название» уникальна: словарь тем свой у каждого человека. Отдельной таблицей, а не набором строк в записи, потому что перечень тем нужен целиком перед каждым обращением к языковой модели. Ни один шаг сегодняшнего сервиса тем не пишет и не читает — место заведено вперёд, чтобы задача, считающая темы, не платила вторым необратимым шагом схемы.
Чего в схеме больше нет
Каталог шагов PocketBase удалён целиком, и на его месте стоит один шаг
начальной схемы — 202608220002_init.go. Это разовое снятие инварианта
«применённая миграция не переписывается», решением владельца от 2026-08-22:
стадия проекта — стройка, на сервере данных нет, сервис остановлен, а новая база
ведёт учёт применённого своей таблицей, которой отметки прежнего каталога не
годятся вовсе. Снятие кончается этим шагом.
Колонок location и source в новой схеме нет. Обе писались одним значением
и не читались никем: в location уходило local, второго значения (s3) не
писал ни один шаг; в source всякий приём писал api, а второе значение
(telegram) держалось ссылкой из применённого шага, а не потребителем. Шаги
ушли, и держать их стало нечем. Поле, у которого появится читатель, вернётся
одним новым шагом схемы.
Колонок tg_chat_id, tg_reply_message_id и object_key нет по той же
причине: их держал применённый шаг, которого больше не существует.
Учётная запись с записями не удаляется, и держит это схема обязательной
связью, а не проверка вызывающего: audio_records, files и topics ссылаются
на users(id) без каскада, а соблюдение внешних ключей включено на каждом
соединении обоих пулов. Прежде запрет ставил слой приложения — сборка, забывшая
его позвать, теряла защиту молча, и теряла. Адреса, которым учётную запись
удаляют, у сервиса нет вовсе; способа удалить записи тоже нет, и это осознанный
тупик до задачи про удаление записи.
Представление данных
Чем физически лежит запись и что происходит при чтении и записи.
- Расшифровка лежит отдельной строкой
texts, а не колонкой записи. Захват её не тянет вовсе: он возвращает идентификатор и признак своего захвата, а колонки шаг читает отдельным чтением. - Файлы записи лежат подкаталогом на запись:
data/records/<ULID записи>/<имя>. Внутри — принятая копия, приведённая копия и сохранённый ответ провайдера. Так копии одной записи лежат вместе, а запись убирается целиком одним движением; плоский каталог, где копии различаются приставкой в имени, обращал бы уборку в перебор по маске. Имя, данное отправителем, не попадает ни в имя файла, ни в путь к нему. Ни файлы, ни объекты в Object Storage не удаляются после завершения записи: каталог и бакет растут неограниченно. - Укладка атомарна: содержимое пишется во временное имя в том же подкаталоге записи и переименовывается в рабочее только после того, как поток дочитан до конца без отказа. Строка о файле заводится после этого; содержимое легло, а строка не сохранилась — уложенный файл убирается.
- Файл отдаётся адресом приложения —
GET /app/audiorecords/{id}/file?copy=original|normalized, — и право пройти по нему даёт узнавание пришедшего и владение записью. Значений на предъявителя сервис не выдаёт вовсе: ни короткого токена файла, ни подписанной ссылки со сроком. Отзыв доступа доходит до файла сразу, а не через срок жизни выданного значения. Имя файла на диске в журнал не пишется и в ответ не идёт. - Учётная запись заводится первым обращением — поиск по
provider_loginи вставка идут одной транзакцией на пишущем соединении. Два отказа уникальности различаются повторным поиском по ключу: нашёлся — гонка двух первых обращений одним логином, не нашёлся — занятая почта, и запись заводится без неё. - Захват записи — один запрос
UPDATE … RETURNINGпо пишущему соединению: выбор подходящей записи и пометка её захваченной идут вместе. Порядок выборки — по времени заведения и по ключу: время неуникально, и без ключа порядок обработки невоспроизводим. Отбор идёт по рубежам из дескриптора, паузе, сроку протухания захвата и отсутствию признака остановки; срок протухания выбирается по рубежу самой записи прямо в запросе — воркер, ещё не знающий, что вытянет, подставить его не может. - Запись результата условна по признаку захвата — инвариант «Результат пишет только держатель захвата» в CLAUDE.md, «Инварианты» (major); норма — pipeline. Условие стоит в самом запросе правки, поэтому между проверкой и записью не остаётся окна.
- Колонки записи отображаются по имени: именованные параметры запроса и место
назначения, найденное по имени колонки. У аудиозаписи поля одного типа идут
длинным непрерывным рядом, и позиционный список дал бы сдвиг на одно поле,
который компилируется молча и кладёт идентификатор файла в колонку текста.
Перечень задан двумя местами —
writeOwnedByPipelineвместе сwriteRecordиreadRecordColumns, — плюс шагом схемы; правило правки и его серьёзность — инвариант в CLAUDE.md, сверку держат правилаinternal/archrules. - Перечень рубежей объявлен одним дескриптором —
internal/entity/stage.go. Из него выводятся выбор шага, отбор захвата, срок протухания и предел простоя: рубеж, забытый в отборе, не выдаётся ни одному воркеру никогда, а пустой прогон по инварианту проекта не пишется в журнал и не считается в метрику. - Отказ базы наружу не выходит дословно. Отказы чтения и укладки называют запись её идентификатором и не несут ни имени файла, ни пути к нему: имя — часть пути к чужому аудио. То же у выгрузки в Object Storage: отказ SDK несёт полный URL объекта.
- Обращения к базе идут с собственным контекстом, а не с контекстом запроса.
Отменять там нечего: операции местные и короткие, а единственное ожидание —
занятая база — задано числом. За отмену платили бы дважды: шаг, прерванный
остановкой сервиса, перестал бы освобождать захват и писать причину остановки —
то есть отмена ломала бы ровно ту уборку, ради которой она и делается. Отмена,
которой сервис распоряжается по-настоящему, доходит до
ffmpegи до платного распознавания.
Настройки с числовым значением
| Настройка | Значение | Где | Откуда число |
|---|---|---|---|
| Предел отказов | 5 | service/transcribe.go |
обычное умолчание, не замер |
| Пауза перед повтором | 2^(отказ−1) с, потолок 5 минут |
там же | то же |
| Срок захвата, приведение | 8 часов | entity/stage.go |
потолок записи 6 часов плюс запас |
| Срок захвата, отправка на распознавание | 8 часов | там же | то же |
| Срок захвата, опрос операции | 1 час | там же | опрос идёт секунды |
| Срок захвата, завершение | 1 час | там же | запись текста и ответ идут секунды |
| Число воркеров конвейера | 3 | конфиг, [pipeline] workers |
решение владельца; ноль — законное значение |
| Ожидание занятой базы | 5000 миллисекунд | конфиг, [storage] busy_timeout_ms |
выведено из числа воркеров, а не замерено: пишет сервис короткими операциями, и очередь из трёх воркеров укладывается в него с запасом |
| Соединений в читающем пуле | 4 | конфиг, [storage] read_connections |
число воркеров плюс запас под запросы приложения; пишущее соединение при этом всегда одно и настройкой не делается |
| Предел простоя, своя работа | 60 минут | конфиг, [pipeline] own_work_limit_minutes |
решение владельца 2026-08-14: сторож ловит зависание, а не долгую работу. Число меньше времени приведения многочасовой записи, и цена названа прямо — остановка обратима. Предел этот работает только по записи, вернувшейся в выборку: см. строку ниже |
| Предел простоя, чужая операция | 1440 минут | конфиг, [pipeline] foreign_work_limit_minutes |
сколько идёт распознавание долгой записи, никто не мерил: ошибаемся в сторону долгого |
| Версия вида структуры реплик | 1 | entity.StructureVersion |
первая |
| Умолчание размера страницы списка | 30 | controller/http.DefaultPageLimit |
столько помещается на экран телефона без прокрутки в два экрана |
| Потолок размера страницы списка | 100 | controller/http.MaxPageLimit |
против того, чтобы попросить весь архив одним запросом и тем обойти постраничность её же параметром |
Ограничитель частоты под /app/ |
120 запросов за 60 секунд | controller/http.appRateMaxRequests, appRateWindowSec |
сервисом пользуются единицы человек; бюджет считается по адресу спрашивающего, а не по учётной записи |
| Срок жизни неиспользуемого счётчика ограничителя | 10 минут | controller/http.staleBudgetAge |
карта счётчиков растёт с числом адресов, и без уборки она стала бы местом, куда спрашивающий кладёт по строке на каждый свой адрес |
| Доля бюджета под опрос карточки | 1/8 | controller/http.pollBudgetShare |
опрос идёт не один: в ту же секунду приложение листает список и грузит новую запись. Из этой доли выводится объявляемая частота опроса, и своей константы у неё нет |
| Потолок длины имени файла отправителя | 255 знаков | entity.MaxOriginalFilenameLen |
предел длины имени в распространённых файловых системах: длиннее системный диалог выбора файла не даёт |
| Потолок длины расширения | 32 знака | service/transcribe.go, maxExtLen |
сторож от патологии, а не перечень: расширения известных форматов укладываются в пять знаков, а x. с четырьмястами знаками роняет заведение временного файла |
| Потолок тем на запись | 5 | entity.MaxTopicsPerRecord |
решение владельца: без него часовой разговор даёт два десятка тем |
| Срок хранения ресурса приложения | 1 год | controller/http.assetMaxAgeSeconds |
имена ресурсов несут отпечаток содержимого, поэтому ответ устареть не может; срок ставится только файлам из каталога сборщика, всё прочее браузер спрашивает заново |
| Задержка перед первой проверкой операции | 10 секунд | service/transcribe.go |
как было |
| Задержка между проверками операции | 5 секунд | там же | как было |
| Пауза воркера между прогонами | 1 секунда | controller/worker/worker.go |
как было |
| Таймаут мягкой остановки | 5 секунд | конфиг, [server] shutdown_timeout |
— |
| Таймаут жёсткой остановки | 20 секунд | конфиг, [server] force_shutdown_timeout |
— |
| Качество кодирования vorbis | -q:a 4 |
adapter/converter/ffmpeg/ffmpeg.go |
— |
| Потолок размера одной записи | 8 ГиБ | entity.MaxRecordSize |
расчётный потолок в шесть часов с запасом на видео |
| Предел длины логина у провайдера | 255 знаков | entity.MaxProviderLoginLength |
значение приходит заголовком, то есть задаётся тем, кто шлёт запрос; число то же, что у имени, пригодного к показу |
| Предел длины имени, пригодного к показу | 255 знаков | entity.MaxDisplayNameLength |
то же |
| Длина идентификатора | 26 знаков | ident.Len |
ширина записи ULID |
Адрес спрашивающего ограничитель берёт из X-Forwarded-For — и только тогда,
когда соединение пришло с адреса из объявленного перечня доверенных. Без этого
счётчик ведётся по адресу пира, а пир с переездом входа на заголовок всегда один
и тот же — обратный прокси; бюджет тогда становится общим на весь сервис, и
восемь одновременно открытых карточек выбирают его целиком. Обратная ошибка —
верить заголовку без сверки пира — отдаёт обход ограничителя ровно тому, кого он
ограничивает. Как читается цепочка — спека
archive; здесь только числа бюджета.
Числа, ушедшие отсюда со встроенным хранилищем: потолок сохранённого ответа провайдера и потолок структуры реплик — их держало поле коллекции, а теперь ответ лежит файлом, а структура текстовой колонкой; жизнь приглашения завести владельца панели — панели нет. Прежде, вместе с собственным входом, ушли срок жизни сессии, потолок времени на вход у провайдера и таймаут обмена кода.
У сторожа простоя есть второй потолок, и он не тот, что в настройке. Предел
простоя проверяется в момент захвата, а захват не выдаёт запись, чей срок
протухания ещё не истёк. Значит для держателя, погибшего жёстко — контейнер убит
по нехватке памяти или docker kill, — запись невидима сторожу до истечения
срока захвата её рубежа, то есть восьми часов у приведения и отправки.
Замерено прогоном: до истечения срока повторный захват записи не выдаёт, и
остановка «застряла» наступает только после него. Мягкая остановка сюда не
подпадает: она снимает захват сама.
Потолок размера назван числом там, где иначе действует умолчание — у тела запроса приёма, и назван дважды: объявленная длина судится заранее, а необъявленная и солгавшая ловятся на чтении. Умолчания здесь не «без предела», а величины на два-три порядка меньше нужного. Таймаут чтения запроса снят: шесть часов записи по медленному каналу переживают любой фиксированный, а стойкость к целенаправленной нагрузке объявлена вне модели угроз.
Чего среди настроек нет: срока хранения файлов и объектов нет вовсе. Таймаутов у обращений к S3 и SpeechKit тоже нет — ни одного.