Модуль 17

Delayed messages та retry patterns

Ось готовий урок, згенерований у стилі Девіда Малана (CS50), спеціально для тебе.


🎓 Урок CS50: Delayed Messages & Retry Patterns

(Або як навчити ваш код терпінню та наполегливості)


1. 🔥 Вступ: Коли все йде не за планом

Уявіть, що ви зайшли в улюблену кав’ярню. Ви замовляєте еспресо, простягаєте картку для оплати, а бариста каже: «Вибачте, термінал завис. Спробуйте ще раз».

Ви пробуєте знову. І знову.

А тепер уявіть, що ви — не людина, а програма. Якщо термінал банку не відповідає 1 секунду, що робить погано написаний код? 1. Він панікує і падає з помилкою 500 Internal Server Error. 2. Або (ще гірше) він починає "довбати" банк тисячу разів на секунду, поки банк не заблокує вас назавжди.

Питання до вас: Чи траплялося вам отримувати лист про підтвердження реєстрації через 10 хвилин після натискання кнопки? Чому він не прийшов миттєво, але все ж таки дійшов?

Сьогодні ми говоримо про Delayed Messages (відкладені повідомлення) та Retry Patterns (патерни повторних спроб).

Без цих механізмів будь-яка розподілена система (мікросервіси, інтеграції з API, платіжні шлюзи) — це картковий будиночок. Один легкий подих вітру (тимчасовий збій мережі), і все розвалюється. Ми ж хочемо будувати хмарочоси, які витримують землетруси.


2. 🧠 Теоретична база: Мистецтво чекати

Давайте розберемо це не як сухі терміни з підручника, а як життєві сценарії.

Що таке Retry (Повторна спроба)?

Це просто. Якщо щось не вдалося — спробуй ще раз. Але є нюанс. Якщо ви дзвоните другу, а він не бере слухавку, ви не дзвоните йому 50 разів за хвилину (сподіваюсь). Ви дзвоните зараз, потім через 5 хвилин, потім через годину.

У коді це називається Exponential Backoff (Експоненційна затримка). * Спроба 1: відразу. Помилка. * Спроба 2: чекаємо 1 с. * Спроба 3: чекаємо 2 с. * Спроба 4: чекаємо 4 с. * ...і так далі, поки не досягнемо ліміту.

Що таке Delayed Message (Відкладене повідомлення)?

Це повідомлення, яке ми відправляємо в чергу, але з поміткою: "Не чіпай мене наступні 15 хвилин".

Аналогія: Ви ставите таймер на духовці. Ви поклали страву (повідомлення) зараз, але процес "приготування" (обробка) має завершитися пізніше.

Як це працює "під капотом"?

Уявіть поштове відділення (Message Broker — RabbitMQ, Kafka, SQS).

  1. Стандартна черга: Лист прийшов — поштар одразу несе його вам.
  2. Delayed черга: Лист прийшов, але на ньому штамп "Відкрити після 18:00". Поштар кладе його на окрему поличку (окрему чергу або Exchange) і ігнорує до настання часу Х. Тільки коли час настав, він перекладає його у звичайну купу для доставки.

👉 Що треба запам’ятати залізобетонно: * Retry рятує від короткочасних збоїв (мерехтіння мережі). * Delayed потрібен для бізнес-логіки (нагадування, таймаути замовлень). * Ніколи не робіть нескінченні ретраї (infinite retries) — це вб’є вашу систему.


3. 🧪 Приклади: Від Hello World до Production

Приклад 1: Наївний підхід (Як не треба)

Уявіть, ми відправляємо SMS.

def send_sms(user_phone):
    try:
        sms_provider.send("Привіт!", user_phone)
    except Exception:
        # О ні! Що робити? 
        # Просто логуємо і забуваємо? Клієнт не отримає SMS :(
        print("Помилка відправки")

Питання: Що відчує користувач, якщо це був код для відновлення пароля? Відповідь: Лють. Він піде до конкурентів.


Приклад 2: Простий Retry (Вже краще)

def send_sms_with_retry(user_phone, retries=3):
    for i in range(retries):
        try:
            sms_provider.send("Привіт!", user_phone)
            return # Успіх! Виходимо.
        except Exception:
            print(f"Спроба {i+1} провалилася. Чекаємо...")
            sleep(2) # Чекаємо 2 секунди

    raise Exception("Ми намагалися, але SMS-шлюз мертвий.")

Аналіз: Це працює для дрібних скриптів. Але якщо у вас 1000 користувачів одночасно, і кожне повідомлення "спить" по 2 секунди у потоці — ваш сервер "зависне", чекаючи. Синхронне очікування (sleep) — зло у великих системах.


Приклад 3: Delayed Message (Архітектурний підхід)

Тут ми використовуємо чергу повідомлень (наприклад, RabbitMQ або AWS SQS).

Задача: Користувач створив замовлення, але не оплатив. Якщо через 15 хвилин оплати немає — скасувати замовлення.

Логіка: 1. Користувач тисне "Замовити". 2. Створюємо замовлення в базі (Status: PENDING). 3. Відправляємо повідомлення в чергу з затримкою 15 хвилин: { "order_id": 123, "action": "check_payment" }. 4. ...проходить 15 хвилин... 5. Воркер (обробник) отримує повідомлення. 6. Він перевіряє базу: "Чи оплачено замовлення 123?" * Так: Ігноруємо повідомлення. * Ні: Скасовуємо замовлення і шлемо лист "Ви забули оплатити".

Чому це круто? Вашому серверу не треба тримати відкрите з'єднання 15 хвилин. Ви "вистрелили і забули" (fire and forget). Таймер цокає на стороні інфраструктури.


4. 🛠 Практична частина

Час розім'яти мозок! 🧠

Завдання 1: Симулятор ретраю У вас є функція roll_dice(), яка випадково повертає число від 1 до 6. Якщо випадає 1 — це "помилка сервера". Напишіть псевдокод циклу, який намагається отримати результат, відмінний від 1, але не більше 5 спроб.

Завдання 2: Експоненціальний ріст Розрахуйте, скільки сумарно часу пройде перед 5-ю спробою, якщо базова затримка 1 секунда, а множник — 2 (тобто: 1с, 2с, 4с...). Підказка: просто додайте числа.

Завдання 3: Кейс "Чорна п'ятниця" Ви розробляєте систему розсилки промокодів. API поштового сервісу дозволяє відправляти лише 50 листів на секунду. А вам треба відправити 10 000. Якщо ви отримали помилку 429 Too Many Requests, який патерн ви застосуєте: А) Негайний повтор? Б) Delayed Message з випадковою затримкою (Jitter)? В) Просто запишете помилку в лог? Поясніть вибір.

Завдання 4: Міні-кейс Користувач завантажує відео на сайт. Обробка відео займає час. Спроектуйте флоу: 1. Завантаження завершено. 2. ?? (що робимо тут?) 3. Відео готове до перегляду. Використайте поняття черги.


5. 💡 Мислення як у розробника

Ось де ми відокремлюємо новачків від сеньйорів.

🚫 Типова помилка новачка: "Thundering Herd" (Ефект натовпу)

Уявіть, що ваш сервіс упав. 10 000 клієнтів одночасно отримали помилку. Якщо всі вони запрограмовані зробити Retry рівно через 5 секунд... Рівно через 5 секунд ваш сервер отримає удар у 10 000 запитів і впаде знову. Рішення профі: Додайте Jitter (випадковість). Нехай один клієнт чекає 5.1 с, інший 5.8 с, третій 4.9 с. Це "розмаже" навантаження.

🚫 Помилка: Нескінченні ретраї (Poison Pill)

Є повідомлення, яке завжди викликає помилку (наприклад, у ньому кривий JSON). Якщо ви будете ретраїти його вічно, ваш воркер застрягне на одному повідомленні назавжди, ігноруючи інші. Рішення профі: Dead Letter Queue (DLQ). Після 5 невдалих спроб перемістіть повідомлення в окрему чергу "кладовище помилок". Потім розробник розбереться з ними вручну.

🔑 Ключове поняття: Ідемпотентність

Це слово звучить страшно, але суть проста. Якщо я повторю запит "Зніми $10 з рахунку" двічі (через Retry), чи зніметься сумарно $20? Хороша система має бути ідемпотентною: скільки б разів я не повторював той самий запит, результат має бути таким, ніби я зробив його один раз. Порада: Використовуйте унікальні ID транзакцій (idempotency-key), щоб перевіряти, чи ми вже виконували цю операцію.


6. 🧩 Підсумок

Отже, що ми сьогодні поклали у свій "рюкзак розробника"?

  1. Ми навчилися не здаватися при першій помилці (Retry).
  2. Ми зрозуміли, як не бути настирливими (Exponential Backoff).
  3. Ми навчилися планувати події на майбутнє без зависання коду (Delayed Messages).
  4. Ми дізналися про DLQ — місце, де вмирають безнадійні повідомлення, щоб не заважати живим.

Тепер ви можете писати системи, які не падають від того, що хтось випадково висмикнув кабель в дата-центрі.

Наступного разу: Ви навчилися обробляти повідомлення по одному. А що, якщо їх мільйон на секунду? Як розділити цю роботу між сотнею серверів, щоб вони не побилися за задачі? Готуйтеся, наступна тема — Шардінг та Партиціювання даних.

А поки що — це був CS50! 🏛️