Cooldown Lifecycle
otp_service · servers · cooldown

Cooldown: тривалість, вихід і шлях у Need Check

Як сендер потрапляє в 🧊, скільки там сидить, що має статися, щоб його випустили, і за яких умов замість випуску він отримує 🚨 Need Check. Окремо: що з цього реально керується групами.

гілка development · код станом на 2026-09-08 · цифри з prod за 7 днів (01–08.09)
Cooldown #1–#3
60–120 хв
за замовчуванням, без групи
Cooldown #4–#5
12–14 год
720–840 хв
Після #5
Need Check
наступна пара фейлів, без cooldown
Fail-open streak → NC
3 / 5
звичайний / preheating сендер

1. Як стартує cooldown

Єдиний живий тригер сьогодні — лічильник failed_in_row. Кожен фейл кастомерської відправки проходить через record_failed_send(): лічильник +1, рядок історії 📉 (EVENT 109), і одразу виклик check_deliverability_limits().

Два інші тригери, які видно в коді, вимкнені: check_blocking_servers (24h fail-rate) не стоїть у розкладі, __check_messages_limitation (ліміти 900/24h і OTP_LIMITS_1_HOUR) закоментований. У prod за 7 днів усі 10 648 стартів cooldown мають причину «failed in row».

otp_service/servers/deliverability_limits.py:40–153, otp_service/webhooks.py:268–345, otp_service/scheduler.py:386

2. Скільки триває

Тривалість визначає cooldown_times — скільки cooldown-ів сендер уже пройшов з моменту останнього скидання. Це драбина з двох сходинок і обривом:

Cooldown #1
60–120 хв
Cooldown #2
60–120 хв
Cooldown #3
60–120 хв
Cooldown #4
720–840 хв
Cooldown #5
720–840 хв
6-та пара фейлів
→ Need Check 🚨
07 год14 год

Світла частина смуги — мінімум діапазону, насичена — випадковий добір усередині діапазону (random.randint).

Що ще додається до тривалості

Коли cooldown_times повертається в 0

deliverability_limits.py:100–153 (драбина), 205–250 (start_cooldown), cooldown.py:36–160

3. Warm-up гейт і вихід

Cooldown не закінчується сам по себе в cooldown_end_date. У цей момент планувальник виносить вердикт: чи довів сендер, що вміє доставляти. Доказом є одна внутрішня server-to-server відправка (warm-up), запланована ще в start_cooldown().

startfailed_in_row ≥ поріг
warm-up слотend − 55…70 хв (≥100 хв)
end − 25…35 хв (<100 хв)
end_dateвердикт

Вердикт у момент end_date

Продовжити 🧊⏳ EVENT 106

Warm-up пішов, підтвердження нема, продовжень < 2. Cooldown +55–70 хв, новий warm-up через 20–30 хв. Якщо підтвердження прийде під час продовження — випуск одразу (early release).

sent ∧ ¬delivered ∧ ext < 2

Випустити ✅ streak = 0

Warm-up доставлено. Єдиний вердикт, що обнуляє fail-open streak.

sent ∧ delivered

Випустити fail-open ⚠️ streak +1

Продовження вичерпані, підтвердження так і нема. Тиша не доказ бану (приймач офлайн, Pusher), тому випускаємо, але рахуємо.

sent ∧ ¬delivered ∧ ext = 2

Випустити fail-open 🔇 streak +1

Після продовження повторний warm-up так і не диспатчився (девайс офлайн, нема пари).

¬sent ∧ ext > 0

Випустити fail-open 📵 streak +1

Warm-up жодного разу не пішов, хоча cooldown тривав ≥ 30 хв: слот дозрів, а девайс відмовив усім спробам. Це теж доказ.

¬sent ∧ ext = 0 ∧ тривалість ≥ 30 хв

Випустити нейтрально 💤 streak без змін

Cooldown коротший за 30 хв, warm-up не вмістився. Доказів нема ні за, ні проти.

¬sent ∧ ext = 0 ∧ тривалість < 30 хв

При будь-якому випуску: cooldown=False, failed_in_row=0, cooldown_start_date=None; історія отримує рядок «Cooldown finished 💧» (EVENT 100) з вердиктом у note.

Адмін-дія «Finish cooldown» не обходить вердикт

Вона лише ставить cooldown_end_date = now. Наступний тік планувальника винесе вердикт за загальними правилами: якщо cooldown уже тривав ≥ 30 хв і warm-up не пішов, це зарахується як 📵 fail-open і збільшить streak.

cooldown.py:36–160 (вердикти), 163–203 (продовження), 206–245 (підтвердження); preheat.py:211–320; webhooks → confirm_cooldown_warmup_delivery

4. Два шляхи в Need Check

Cooldown переводить сендер у 🚨 двома незалежними механізмами. Вони рахують різне і мають різний почерк в історії.

A. Вичерпана драбина

441 з 569 NC за 7 днів
  1. Сендер проходить cooldown #1…#5 без скидання cooldown_times (менше 11 доставок за 14 год між фейлами).
  2. Після виходу з #5 приходить чергова пара фейлів.
  3. check_deliverability_limits бачить cooldown_times ≥ 5 і викликає flag_cooldown_server(): статус NEED_CHECK одразу, без cooldown.

Telegram: 📵 «Need to check SIM card… due to cooldown many times», топік SIM card. Пропускається, якщо сервер у STATUS_REBOOT (фейли рахуються далі, флаг спрацює на наступному).

Час від cooldown #1 до NC: медіана 34 год (p25 33, p75 37). Це і є сума драбини: 3 × ~1,5 год + 2 × ~13 год плюс паузи на фейли.

Рядок історії без note

flag_cooldown_server не виставляє status_note, тож усі 441 рядок NC цього шляху в Load History порожні. Шлях B підписує себе явно. Варто дописати note на кшталт «cooldown ladder exhausted (#5); failed in row: N».

B. Fail-open streak

106 з 569 NC за 7 днів
  1. Кожен вихід із cooldown з вердиктом ⚠️ / 🔇 / 📵 робить cooldown_failopen_streak +1.
  2. ✅ обнуляє streak; 💤 його не чіпає.
  3. Коли streak досягає ліміту, замість випуску в Active сендер отримує NEED_CHECK, streak обнуляється.

Ліміт: 3 поспіль для звичайного сендера, 5 для preheating=True (preheat-сендери підтверджують warm-up у ~53% проти 84%, тому їм довша повідь).

Telegram: 🧊🚩 «flagged NEED CHECK: N consecutive cooldowns released without warm-up delivery confirmation», топік Need Check. Note в історії: «… (streak 3/3) → NEED CHECK 🚩».

За 7 днів: 74 через ⚠️, 29 через 📵, 3 через 🔇. 13 із 106 — preheating з лімітом 5.

Решта NC за тиждень: 10 з адмінки і 12 від chat-preheat (внутрішня відправка впала в SMS).

deliverability_limits.py:100–101, 252–275 (шлях A); cooldown.py:127–143 (шлях B)

5. Картина в prod за 7 днів

Старти cooldown за номером у драбині

CooldownСтартівМедіана плану, хвМінМакс
#18 4046710120
#27788511120
#35568510120
#445977312840
#545177515840

Мінімуми 10–15 хв на кожній сходинці — це сендери в групах: групова затримка заміняє драбину повністю (див. розділ 6). Різкий спад 8 404 → 778 означає, що більшість сендерів після #1 встигають назбирати 11 доставок і скинути лічильник.

Планова тривалість, усі старти

ДіапазонСтартівЗвідки
< 15 хв443групи 10–30 / 15–25; warm-up не вміщується → завжди 💤
15–30 хв2 626групи «Shortest», «Short» (15–45)
30–50 хв747групи 15–45 / 30–90
50–100 хв4 178дефолт #1–#3, група «Medium»
1,5–3 год1 837дефолт #1–#3 (верх діапазону) + продовження
> 10 год817дефолт #4–#5

Вердикти при виході (EVENT 100)

ВердиктВиходівЗ них → NC
✅ підтверджено9 1080
⚠️ не підтверджено після 2 продовжень57274
📵 без warm-up, ≥ 30 хв41529
💤 без warm-up, < 30 хв3950
🔇 повторний warm-up не пішов713

Warm-up відправок 13 060, підтверджень 9 230, продовжень 3 252. Тобто ~86% виходів проходять з доказом доставки, ~10% fail-open, ~4% нейтральних.

6. Керування cooldown через групи

Група (otp_service.models.Group) — це набір параметрів з часовим вікном. Сендер може бути в кількох групах; у момент фейлу береться та, чиє вікно накриває поточний час, з найвищим priority. Якщо груп нема або жодна не активна зараз — дефолти з розділу 2.

Поля групи і що вони змінюють

failed_in_row
Поріг фейлів підряд для входу в cooldown. Дефолт 2. Значення 1 = кожен фейл одразу cooldown. 0 ігнорується (діє дефолт 2).
cooldown_delay_from / _to
Тривалість cooldown у хвилинах, випадково з діапазону. Заміняє драбину цілком: і 60–120 для #1–#3, і 12–14 год для #4–#5. Обидва значення мають бути > 0, інакше діє драбина.
send_from / send_to_message_interval
Пауза між кастомерськими відправками сендера (next_send_date), хвилини. Не про cooldown, але саме це визначає, як швидко сендер назбирає 11 доставок для скидання cooldown_times.
from_time / to_time
Вікно дії групи за часом сервера (UTC). Може переходити через північ (22:00 → 08:00). Дефолт 00:00–23:59:59, тобто завжди.
priority
Хто виграє при перетині вікон. Більше = важливіше. Унікальне серед усіх груп (валідація в Group.clean).

Чого група не змінює

Продовження (55–70 хв × 2), пороги streak (3 / 5), правило 30 хв для 📵, розклад warm-up слота, скидання cooldown_times через 11 доставок і сам обрив на cooldown_times ≥ 5. Це константи в коді.

Як затримка групи взаємодіє з гейтом

cooldown_delayWarm-upТиповий вердиктНаслідок
< 15 хвне вміщується💤Гейт фактично вимкнено: сендер виходить без доказу, streak не росте. Мертвий девайс іде в NC лише шляхом A.
15–30 хвслот на +5–10 хв, впритул або 💤Часто встигає. Якщо не пішов — усе одно нейтрально, бо тривалість < 30 хв.
30–100 хвend − 25…35 хв / ⚠️📵Повний гейт: і продовження, і streak. Оптимум за співвідношенням простою до доказовості.
≥ 100 хвend − 55…70 хвяк дефолтПоводиться як штатна драбина #1–#3, лише без стрибка до 12–14 год на #4–#5.

Рецепти

Що є в prod зараз

ГрупаprioВікноfailscooldown, хвsend, хвActiveNewінші
High frequency10008:00–17:002480–7204–6000
Short cooldown after 1 fail95завжди115–458–12000
Short cooldown90завжди215–458–121918228
Shortest cooldown85завжди210–308–123122301
Medium cooldown80завжди230–908–121221235
Shortest cooldown #275завжди215–258–123750445
Long cooldown70завжди260–1208–12000
Short cooldown #250завжди215–458–123331389
Short cooldown, longer send intervals40завжди215–4512–16000

З 660 Active сендерів у групах лише 132; решта 528 живуть на дефолтній драбині. «Інші» — переважно Deleted / Lost / Need Check, що лишились у групах як хвіст від старих батчів. Жодна група з вікном не використовується: «High frequency» порожня.

Де керувати

# групи
GET   groups/                      список
POST  groups/                      створити: name, priority, failed_in_row,
                                   cooldown_delay_from, cooldown_delay_to,
                                   send_from_message_interval, send_to_message_interval,
                                   from_time, to_time ("HH:MM[:SS]" або null)
GET   groups/<uuid>/               деталі
PATCH groups/<uuid>/               змінити будь-яке з полів вище
DELETE groups/<uuid>/

# прив'язки, серверо-центрично (до 500 items за запит)
POST  groups/assignments/          set:    items[{server:{region,unit} | uuid, groups:[uuid…]}]
POST  groups/assignments/add/      add
POST  groups/assignments/remove/   remove

# прив'язки, групо-центрично
POST  groups/<uuid>/servers/        set список серверів групи
POST  groups/<uuid>/servers/add/
POST  groups/<uuid>/servers/remove/

Зміна параметрів групи діє з наступного фейлу: уже запущений cooldown свою cooldown_end_date не перераховує.

otp_service/models.py:1177–1215 (Group), 632–642 (get_group_for_current_time); otp_service/servers/groups.py; otp_service/urls_statistics.py:26–35