Как работает claim¶
Claim выбирает из одной очереди до slots заданий по порядку, соблюдая лимиты и, в режиме fair, черёд каждой группы limit_key. Каждая очередь забирается в своей транзакции.
Чтение очереди окнами¶
Claim читает готовые строки через индекс (queue, priority DESC, run_at, id) WHERE state = 'queued' окнами: первое — slots × 4 строк, каждое следующее вдвое больше. После каждого окна планировщик в Python решает, может ли ещё какая-то непрочитанная строка изменить результат. Если нет — чтение останавливается.
Из следующего окна исключаются строки двух видов:
- Ключи на лимите. Ключ, у которого осталось
nстартов, берёт свои первыеnстрок; остальные строки этого ключа пропускаются до конца claim. - Группы fair, которым слот не достанется. Следующий черёд группы позже последнего, который нужен claim, или в плане у группы уже
slotsстрок.
flowchart TD
W[Прочитать следующее окно без исключённых ключей] --> K{Новые ключи?}
K -- да --> L[Заблокировать их строки limits, затем посчитать running и старты]
L --> P
K -- нет --> P[План: лимиты, черёд групп, исключения]
P --> S{План устоялся?}
S -- нет --> W
S -- да --> T[Взять план через FOR UPDATE SKIP LOCKED]
T --> H{Взяли меньше запланированного?}
H -- да, строки держит другой claim --> W
H -- нет --> E[Хуки переходов]
План совпадает с ранжированием всей очереди сразу. Это проверено против старого запроса с полным сканом на 3 520 случайных очередях: расхождений нет.
Блокировки¶
Claim блокирует строки limits рассматриваемых ключей, а затем считает running и старты новым запросом, поэтому два воркера не могут оба увидеть свободный слот одного ключа. Ждать может только первая блокировка: когда claim уже держит строку limits или задание, следующие ключи блокируются через SKIP LOCKED, и ключ, занятый другим claim, ждёт следующего claim. Поэтому два claim, встречающие ключи в разном порядке, не попадают в deadlock.
Скорость¶
Note
Claim по окнам есть в akadze 0.6.0 и новее.
PostgreSQL 16, slots = 20, 100 000 готовых заданий, медиана 15 claim. «0.5.0» — версия, которая ранжировала всю очередь одним запросом, когда голова не заполняла claim, и всегда в режиме fair.
| Очередь | Режим | 0.5.0 | 0.6.0 |
|---|---|---|---|
| 20 000 ключей × 5 заданий | fair | 313 мс | 2,6 мс |
| 200 из 400 групп заняты | fair | 276 мс | 7,3 мс |
| 5 000 срочных над 95 000 | fair | 255 мс | 4,6 мс |
| 1 000 ключей на лимите, затем свободные | fair | 252 мс | ~15 мс |
| 1 000 ключей на лимите, затем свободные | обычный | 201 мс | ~14 мс |
| 50 ключей, у одного глубокий бэклог | fair | 190 мс | 4,3 мс |
| первые 50 000: один ключ на лимите | обычный | 187 мс | 5,7 мс |
| первые 50 000: один ключ на лимите | fair | 159 мс | 4,2 мс |
| без ключей, одна группа | fair | 120 мс | 17,9 мс |
| вся очередь: один ключ на лимите | обычный | 93 мс | 16,1 мс |
| вся очередь: один ключ на лимите | fair | 92 мс | 17,2 мс |
Под нагрузкой 10 параллельных claim в течение 20 с на 20 000 заданиях с 20 ключами под лимитом забрали 8 610 заданий в обычном режиме (было 2 796) и 5 387 в fair (было 3 340), без ошибок и без превышения max_running ни в один момент.
Худший случай по-прежнему линейный: когда claim должен прочитать всю очередь (одна группа в fair или ключ на лимите во всю очередь), он читает индекс до конца — около 17 мс на 100 000 заданий.
Заметки о дизайне, отвергнутые варианты и доказательство точности: akadze-harness, заметка 15.