0076. Планировщик скачиваний: сессии, очередь, приоритеты и окна
Статус: proposed (этапы 1, 2а, 2б и часть этапа 3 реализованы). Owner: Ian Panaev. Дата: 2026-10-14.
Контекст
Массовое обновление: 1000 клиентов одновременно скачивают по 4–20 ГБ через канал 1 Гбит/с. Это 4–20 ТБ, то есть 9–45 часов передачи при любом коде. Задача планировщика — не ускорить канал, а сделать так, чтобы ни один клиент не отвалился, каждый знал, когда начнётся его скачивание, и первые закончили через минуты, а не все разом в конце.
Что сейчас (ADR 0004, 0007, 0008, 0009, 0022):
- Допуск выдаётся на HTTP-запрос. SDK и
arkvoryctlчитают файл диапазонами по 8 МиБ, и каждый диапазон заново проходитAdmissionQueue: 20 ГБ — около 2500 допусков. 1000 клиентов делят канал поровну, и все заканчивают одновременно. - Очередь сервера в памяти: 16 активных, 64 ждущих (8 на principal), ожидание до 20 с. Переполнение — сразу 503
busyс фиксированнымRetry-After: 2. Ни места в очереди, ни срока клиент не узнаёт. - Бюджет повторов SDK — 20 на всю операцию и 5 на запрос, ожидание дольше 60 с считается отказом. При тысяче клиентов бюджет кончается за минуты: обновление падает.
arkvoryctlсохраняет префикс, но продолжит только при повторном запуске. - Под одним ключом (частый случай для парка агентов) активны не больше 4 скачиваний, остальные получают отказ.
- Сторонние клиенты (docker/oras, git-lfs, npm/Unity, curl/wget по ссылкам и путям) видят только статус и
Retry-After; их повторами мы не управляем.
При одинаковых файлах среднее время завершения минимально, когда файлы обслуживаются по очереди целиком, и максимально при делении поровну. Поэтому единица планирования — сессия скачивания, а не запрос.
Решение
1. Клиент никогда не сдаётся из-за занятости
Ответ 503 busy или 429 с Retry-After — это «приходи позже», а не сбой. В SDK такие ожидания не расходуют бюджет повторов (maxRetries, maxAttempts) и не ограничены maxDelayMs: клиент ждёт указанное время плюс случайную добавку, пока не истечёт общий срок операции, заданный вызывающим (waitLimitMs, по умолчанию без срока), или не сработает отмена. Бюджет повторов остаётся для настоящих сбоев: обрыв сети, 502/504, таймаут попытки. arkvoryctl download по умолчанию ждёт без срока (--wait-limit задаёт срок) и печатает, сколько ещё ждать.
2. Сессия скачивания
Сессия — допуск на всё скачивание одного объекта (артефакт, путь, пакет, слой OCI, объект LFS, tarball npm) одним клиентом. Пока сессия активна, её диапазоны идут без повторного допуска; полоса по-прежнему делится BandwidthGovernor (ADR 0008).
- Явная сессия. SDK, CLI и консоль просят талон:
POST /api/v1/downloadsсIdempotency-Keyи объектом. Ответ — состояниеqueuedилиactive, место в очереди, оценка началаstartsAtс интервалом,checkAt(когда спросить снова) и срок жизни. Диапазоны сессии несут заголовокArkvory-Download-Id. - Неявная сессия. Запрос без талона встаёт в ту же очередь под ключом «principal + объект +
Arkvory-Download-Id», который клиент создаёт сам на одно скачивание (SDK иarkvoryctl— с этапа 2а). Без id (сторонние инструменты, ссылки, старые клиенты) ключ — адрес клиента, и клиенты за одним NAT с одним ключом делят одну сессию. Пока сессия ждёт, клиент с id получает 503busyсRetry-AfterдоcheckAtи заголовкамиArkvory-Queue-PositionиArkvory-Starts-At; запрос без id сервер сначала держит до очереди ограниченное время. - Активная сессия держит место, пока идут байты. Пауза между диапазонами дольше
idleSeconds(30 с), выдача всех байтов объекта или отмена освобождают место. - Ждущая сессия, которую клиент не подтвердил вовремя (не пришёл к
checkAtплюс запас), удаляется — «неявка». Неявка не наказывается: новый запрос встаёт в конец очереди своего класса.
3. Стратегии
Оператор выбирает стратегию во время работы, без перезапуска:
ordered(по умолчанию) — классы приоритета с весами, внутри класса FIFO, между principal класса — по кругу. Ждущий дольшеagingMinutesподнимается на класс выше: никто не голодает.smallest-first— внутри класса первыми идут меньшие объекты (минимум среднего времени при разных размерах), со старением, чтобы большие не ждали вечно.shared— прежнее поведение: допуск на запрос, канал делится поровну. Для тех, кому важнее равномерность, чем раннее завершение.
Смена стратегии действует на следующие решения о допуске. Активные сессии не прерываются.
4. Классы приоритета
Встроенные классы: interactive (люди в консоли и CLI), release (CI и развёртывания), bulk (массовые обновления), background (синхронизация зеркал и прочее фоновое).
- Безопасные умолчания. Люди —
interactive, сервисные аккаунты и ссылки —release; ссылка — отдельный принципал, не выпустивший её. Веса 8/4/2/1. Дляinteractiveрезервируется одно место (от четырёх мест): консоль работает даже при полной очереди обновлений. - Настройка. Оператор меняет веса, резерв и прочие параметры (этап 2а — переменными окружения, этап 3 — в консоли) и класс каждого сервисного аккаунта (этап 3; класс входит в сервисную политику с её ревизией и аудитом). Клиент может только понизить свой класс заголовком
Arkvory-Download-Classили в запросе талона (например, агент обновления проситbulk), но не повысить; неизвестное значение — 400, применённый класс возвращается в ответе.
5. Окна
- Окна клиента. Талон принимает
notBeforeиnotAfter. Планировщик не начинает сессию раньшеnotBeforeи сразу говорит, если с текущей очередью кnotAfterне успеть (startsAtпозже срока; клиент решает сам). - Окна оператора. У класса могут быть окна по расписанию (дни недели, часы, часовой пояс): например,
bulk— только 22:00–06:00. Вне окна класс получаетoutsideShareполосы (по умолчанию 0: только в окне). Начатая в окне сессия доводится до конца. Оценки начала учитывают окна.
6. Оценка начала
Планировщик знает размер каждого объекта, остаток активных сессий и измеренную скорость выдачи (скользящее среднее за последнюю минуту, не меньше настроенной полосы, если она задана). Начало ждущей сессии — момент, когда перед ней освободится место, с учётом классов, резерва и окон; клиенту отдаются точка и интервал. Оценка пересчитывается при каждом обращении. checkAt — середина до начала, но не позже 5 минут и не раньше 5 секунд, с разбросом по талону: опросы не приходят толпой. Точность оценок видна в метриках.
7. Ресурсы и утечки
- Все структуры ограничены: сессий всего (по умолчанию 65 536), на principal (4096 — парк агентов под одним ключом), длина неявного ключа. Сверх лимита — 503
busyсRetry-After, а не рост памяти. - Нет таймера на сессию: один такт планировщика (500 мс) пересчитывает очередь и удаляет просроченное; поиск — по индексам (класс → principal → FIFO), без сканирования всей очереди на каждый запрос.
- Сессия освобождается при закрытии ответа, отмене, ошибке и на такте при простое: каждый путь проверяется тестом на счётчики (
active,waitingвозвращаются к нулю). - Остановка сервиса: ждущие получают 503
busyсRetry-After, активные доводятся по ADR 0048.
8. Хранение и несколько процессов
Этап 2 держит очередь в памяти процесса записи за портом DownloadScheduler (application). Талон — подписанный токен с id, классом и временем постановки: после перезапуска клиент приходит с тем же талоном и встаёт на своё прежнее место, а не в конец. Этап 4 добавляет реализацию порта на PostgreSQL (выдача мест с арендой и fencing, как arkvory_jobs), общую для процесса записи и шлюзов чтения (ADR 0009); её таблицы исключаются из резервной копии.
9. Видимость
arkvoryctl download: «в очереди, 312-й, начало ≈ 14:05 (через 47 мин)», затем прогресс. Консоль показывает это же на странице «Скачивания».- Оператор: страница настроек скачиваний (стратегия, классы, веса, резерв, окна) и состояние очереди — длина по классам, ожидание, прогноз опустошения.
- Метрики: сессии по классам и состояниям, время ожидания, неявки, отклонение оценки от факта. Readiness — агрегаты без id.
10. Этапы (каждый — отдельный PR)
- Устойчивый клиент (п. 1) и
Retry-Afterпо фактической очереди вместо фиксированных 2 с. - Планировщик в памяти, в двух PR:
- 2а — неявные сессии на сервере: стратегии, классы и резерв, все параметры очереди переменными окружения, оценки и заголовки очереди,
Arkvory-Download-IdиArkvory-Download-Class, вытеснение, удержание запросов сторонних клиентов, место и начало в SDK иarkvoryctl, метрики и readiness; - 2б — подписанный талон очереди, переживающий перезапуск, окна клиента в SDK и
arkvoryctl, id скачивания в контрольной точкеarkvoryctl.
- 2а — неявные сессии на сервере: стратегии, классы и резерв, все параметры очереди переменными окружения, оценки и заголовки очереди,
- Настройки оператора: стратегия, веса, резерв, классы сервисных аккаунтов, окна классов; страница консоли.
- Очередь в PostgreSQL для шлюзов чтения и HA.
- Нагрузочный гейт: масштабированная модель (например, 500 клиентов по 40 МБ на канале 40 МБ/с) проверяет, что все докачали без ошибок, оценка начала отклоняется не больше заданного, первые заканчивают за время одного файла.
Уточнения реализации (этап 2а)
Проверены разбором кода и детерминированной симуляцией массового обновления (tests/download-simulation.test.mjs).
- Порог. Сессией скачиваются объекты больше
ARKVORY_DOWNLOAD_SESSION_MIN_BYTES(16 МиБ). Меньшие (пара диапазонов) допускаются на запрос прежней очередью: они не должны ждать за многогигабайтными скачиваниями. - Порядок проверок. Очередь встаёт после авторизации и метаданных: отказ в доступе, HEAD и 304 места не занимают.
- Завершение. Сессия хранит отданные целиком диапазоны (слитый список интервалов, не больше 64; сверх него забывается самый короткий — это только откладывает завершение до
idleSeconds). Начало скачивания — наименьшее смещение запросов, кроме чтений хвоста: суффиксный диапазон или ограниченный диапазон до последнего байта (каталог ZIP) начала не задают, открытыйbytes=X-и запрос без Range — задают. Сессия завершена, когда диапазоны покрывают байты от начала до последнего и других ответов нет: возобновление с середины (curl -C -,wget -c, SDK с сохранённым префиксом) завершается без байтов до смещения, чтение конца файла первым сессию не закрывает. Прерванный ответ байтов не добавляет: в покрытие идёт только ответ, отданный до конца. Пауза дольшеidleSecondsосвобождает место, но 10 минут скачивание хранит время прихода и отданные диапазоны; вернувшись, оно встаёт в очередь по прежнему времени прихода, а не получает место раньше ждущих. - Только хвост. SDK, возобновивший скачивание внутри последнего диапазона, просит ограниченный диапазон до конца: это неотличимо от чтения хвоста, и сессия не завершается сразу. Поэтому сессия, которая читала только хвост, держит место без ответов 5 секунд (не дольше
idleSeconds), а неidleSeconds: следующее чтение клиента ZIP идёт сразу, а место после, возможно, законченного скачивания не простаивает 30 секунд. Ушедший так клиент сохраняет время прихода и отданный хвост, как после паузы. - Место без резервирования. За отсутствующим клиентом место не держится: такт отмечает первых в очереди, вдвое больше свободных мест (с местами сессий, заканчивающих в 5 с), и место берёт первый пришедший. Иначе места простаивали, пока клиент шёл к своему
checkAt: симуляция показала до 10–20 % потерь на коротких скачиваниях, после правки — 0. - Оценка. Модель освобождения мест по остаткам активных сессий при пиковой секундной скорости за минуту, поделённой между идущими скачиваниями. Средняя скорость падала при простое и растягивала оценки, клиенты приходили позже, и простой рос.
checkAt— середина до начала, от 2 с до 5 мин; первым в очереди — от 1 с. Новичку первый ответ — через 2 с без места: его место станет известно на такте, а конец очереди может оказаться следующим по кругу. - Доля принципала.
ARKVORY_MAX_DOWNLOADS_PER_PRINCIPALограничивает принципала, пока первым в очереди стоит другой; без претендентов свободное место отдаётся и сверх доли. - Вытеснение. Без свободных мест голова очереди более высокого класса или принципал ниже доли получает место, которое на границе диапазонов уступает клиент протокола (с
Arkvory-Download-Id), держащий его не меньше минуты, ниже классом или выше доли. Не больше одного за такт и только пока голове не освободилось место иначе; уступивший начинает ожидание заново, без накопленного старения, и не берёт место, пока ждёт голова, которой он уступил: иначе он, спрашивая раньше головы, забирал бы место обратно ещё на минуту. Инструменты без id не вытесняются: многие не переживают 503 посреди файла.ARKVORY_DOWNLOAD_PREEMPT=falseотключает. - Сторонние клиенты. Запрос без id сервер держит до очереди до
ARKVORY_DOWNLOAD_HOLD_SECONDS(по умолчанию срок прежней очереди, 20 с), не большеARKVORY_TRANSFER_QUEUE_LIMITодновременно: так docker, npm и браузер по ссылке ведут себя не хуже, чем до очереди. - Сессия за NAT. Одновременно идут не больше
rangesPerSession(4) ответов сессии: клиенты без id за одним адресом не обходят очередь целиком. - Старение поднимает не выше
release: полоса людей остаётся их.agingMinutes = 0отключает. - Резерв по умолчанию — одно место при четырёх и больше местах: при двух местах резерв отдавал бы людям половину канала.
- Шлюзы чтения до общей очереди этапа 4 по умолчанию в
shared: очередь в памяти процесса за балансировщиком без привязки клиента теряла бы позиции. - Часы монотонные: сдвиг системного времени не сбрасывает очередь и не держит места.
- Стоимость такта — сортировка O(n log n); 65 536 ждущих — около 30–40 мс на такт. Длительность последнего такта видна в readiness. Следующий такт — через 500 мс или через десять длительностей последнего, если это дольше: очередь у предела
ARKVORY_DOWNLOAD_QUEUE_LIMIT(до 1 048 576) занимает не больше десятой доли цикла событий ценой более редкой передачи мест и пересчёта оценок. - Заголовки без префикса
X-(RFC 6648); прежниеX-Arkvory-*не переименовываются. - До этапа 2б талонов нет: после перезапуска процесса сессии встают в очередь заново.
Уточнения реализации (этап 2б)
- Талон вместо ресурса. Отдельный ресурс
POST /api/v1/downloadsне нужен: всё, что он давал, уже идёт заголовками ответа на сам запрос байтов (место, начало, срок проверки). Позиция, переживающая перезапуск, — заголовокArkvory-Queue-Ticket:2.<время прихода>.<время выдачи>.<HMAC-SHA256>(время — миллисекунды по стене в base 36) над хешем ключа сессии и обоими временами. Клиент возвращает последний полученный талон; новый процесс ставит скачивание на прежнее время прихода. Порядок очереди — по времени прихода, затем по номеру. Талон нельзя перенести на другое скачивание или сдвинуть раньше. Он принимается 15 минут после выдачи (10 минут хранения паузы и запас) и выдаётся заново с каждым ответом с тем же временем прихода: ожидание дольше суток место не теряет, а старый талон нельзя повторить, чтобы вернуть раннее время. Время выдачи до 2 минут впереди часов узла (расхождение часов HA) считается текущим; приход позже выдачи и выдача дальше в будущем отвергаются. Неверный талон игнорируется, скачивание встаёт заново. Талоны прежнего формата1.не читаются: талон — подсказка, его потеря стоит места один раз при обновлении. - Ключ подписи — одна строка
arkvory_download_ticket_key(миграция 38). Её создаёт первый процесс записи, читают все процессы установки, поэтому талон верен после перезапуска и на другом узле HA. В резервную копию не входит: восстановленная установка делает свой. Шлюз чтения без ключа (до миграции при поэтапном обновлении или до первого старта процесса записи) работает без талонов и ищет ключ снова каждые 30 секунд, пока не найдёт. - Окна клиента — на стороне клиента, сервер для них не нужен.
notBefore: SDK иarkvoryctl --not-beforeне спрашивают раньше.notAfter: еслиArkvory-Starts-Atпозже срока или срок прошёл в очереди, SDK завершает скачивание сqueue_deadline,arkvoryctl --not-after— с кодом 4, а не ждёт впустую. Время — RFC 3339 илиЧЧ:ММ(ближайшее наступление). Ожидание любой длины SDK ждёт частями: таймер платформы держит не больше 2^31−1 мс и больший срок срабатывает сразу. ДоnotBeforeSDK ждёт шагами не больше минуты и каждый раз заново сверяет остаток с часами: таймер не считает сон ноутбука и паузу ВМ, а часы могут перевести, и одно долгое ожидание сдвинуло бы начало на часы.Retry-Afterбольше часа (ошибка прокси) SDK ждёт час и спрашивает снова. Окна оператора по классам остаются этапом 3. arkvoryctlхранитArkvory-Download-Idв контрольной точке скачивания: новый процесс продолжает то же скачивание в очереди. Старые контрольные точки получают id при первом использовании, без несовместимости.- Часы. Талон и
Arkvory-Starts-At— во времени по стене, очередь — в монотонном; планировщик переводит одно в другое.
Уточнения реализации (этап 3, первая часть)
- Настройки в БД. Одна запись
arkvory_download_settings(миграция 40) с ревизией:strategy,sessions,reserve,perPrincipal,weights,sessionMinBytes,idleSeconds,agingMinutes,preempt,rangesPerSession. Переменные окружения остаются значениями по умолчанию; запись хранит только изменённые поля,{}— возврат к окружению. Длина очереди и удержание сторонних клиентов размеряют память при старте и остаются только в окружении. - Проверка до сохранения. Изменение проверяется политикой этого сервера (
overrideDownloadPolicy): настройки, которые не могут работать, не сохраняются (400 с полем). Еслиsessionsменяется безreserve/perPrincipal, они понижаются, чтобы поместиться, и никогда не повышаются молча. Веса задаются только все четыре сразу. - Применение без перезапуска.
DownloadScheduler.configureдействует со следующего решения; активные сессии сохраняют места. Процесс записи читает ревизию раз в 10 секунд и при старте; изменение через API применяется сразу. Сохранённое, но не подходящее этому серверу после смены окружения: очередь работает на окружении,problemобъясняет почему. - Шлюзы чтения не применяют запись до общей очереди этапа 4: их очереди — на процесс.
GETна шлюзе показывает его окружение с пояснением вproblem,PUTна шлюзе —read_only. В установке со шлюзами процесс записи не уходит со стратегииshared(400 на/overrides/strategy): иначе диапазоны одного скачивания встречали бы два вида очереди. Запись с другой стратегией, сохранённая до появления шлюзов, тоже не применяется: работает окружение,problemобъясняет почему. - Устойчивое чтение записи. Запись читается мягко: поля новой версии (сохранённые до отката) пропускаются, известное поле вне диапазона даёт
problem, и работает окружение. Ошибка БД при старте не останавливает процесс: фоновая задача повторяет чтение каждые 10 секунд. - Порядок ревизий. Применяется только более новая ревизия, чем работающая: поздний ответ старого чтения не откатывает свежее изменение.
GETприменяет запись, если она новее. admission. При стратегииsharedдопуск идёт по запросу через ограничения окружения, и настройки сессий не действуют: ответ говоритadmission: per-request, консоль объясняет это. Удерживаемый запрос перепроверяет стратегию и порог: после переключения наsharedили подъёма порога он уходит к допуску по запросу, а не ждёт места в очереди.- Конкурентность.
PUT /api/v1/system/downloads/settingsсexpectedRevision— CAS одним SQL-оператором; 409revision_mismatchпри гонке. Записываютсяupdated_byи имя администратора управляющей площадки (ADR 0077) для ключа управления. - Запись не входит в резервную копию: настройки подобраны под оборудование и канал источника.
- Классы сервисных аккаунтов и окна классов — следующая часть этапа 3.
Последствия
- Правило ADR 0007 «Retry-After дольше
maxDelayMs— отказ» заменено п. 1: для ответовbusyиrate_limitedклиент ждёт, сколько просит сервер, в пределахwaitLimitMs. Остальные повторы ADR 0007 не меняются. - Публичный контракт расширяется аддитивно: ресурс
/api/v1/downloads, заголовкиArkvory-Download-Id,Arkvory-Download-Class,Arkvory-Queue-Ticket,Arkvory-Queue-Position,Arkvory-Starts-At, причинаdownload_queued, поле класса в сервисной политике. Существующие клиенты работают без изменений: без талона они получают неявную сессию. ARKVORY_MAX_DOWNLOADSостаётся пределом одновременных запросов для объектов до порога и дляshared; места сессий задаётARKVORY_MAX_DOWNLOAD_SESSIONS(по умолчанию то же число).- Сторонние клиенты сначала ждут на сервере, затем получают честный
Retry-After; их дальнейшие повторы остаются на их совести. Для массовых раздач рекомендуются SDK илиarkvoryctl. - Очередь не увеличивает пропускную способность канала. Для 20 ТБ через 1 Гбит/с по-прежнему нужны зеркала и шлюзы на площадках, волны через хаб и, позже, дельты между версиями.