Модуль 10

Message acknowledgements та delivery

Ось готовий урок, створений за твоїм майстер-промптом. Поїхали! 🚀


🎓 Урок: Message Acknowledgements та Delivery Semantics

(Або: Як не загубити мільйон доларів у мережі)


1. 🔥 Вступ: Чому це питання життя і смерті (вашого сервісу)

Уявіть ситуацію. Ви замовили піцу через додаток. Гроші з картки списалися. Але у піцерії зламався принтер чеків, і кухар навіть не дізнався про ваше замовлення. Ви чекаєте годину. Дві. Піци немає. Грошей немає. Ви розлючені.

А тепер уявіть, що це не піца, а банківський переказ на $10,000.

У світі розподілених систем (мікросервісів) ми постійно пересилаємо повідомлення: "Створи замовлення", "Надішли email", "Зніми гроші". Але мережа — це ненадійне місце. Кабель можуть перегризти щури, сервер може перезавантажитися, а процес — "впасти" через помилку в коді.

Питання до вас: Як відправник (Producer) може бути впевненим на 100%, що отримувач (Consumer) не просто отримав повідомлення, а й успішно його обробив?

Якщо ви просто "кидаєте" дані в мережу і сподіваєтеся на краще — ви будуєте картковий будинок. Сьогодні ми навчимося будувати фортеці. Ми поговоримо про Acknowledgements (Acks) — "рукостискання", які гарантують надійність.


2. 🧠 Теоретична база: Як це працює "під капотом"

У більшості систем обміну повідомленнями (RabbitMQ, Kafka, AWS SQS) є посередник — Брокер (Message Broker). Це як поштове відділення.

  1. Producer кладе лист у скриньку (Брокер).
  2. Consumer (наш робітник) забирає лист.

І ось тут виникає магія. Як Брокер дізнається, що лист можна видалити з черги?

Два режими роботи (запам'ятайте це!):

А. Auto-Acknowledgement ("Вогонь і забув")

Брокер віддає повідомлення Консьюмеру і миттєво видаляє його зі своєї пам'яті. Йому байдуже, що станеться далі. * Аналогія: Поштар кидає вам лист у вікно поїзда, що рухається. Якщо ви його не спіймали — ваші проблеми.

Б. Manual Acknowledgement ("Під розпис")

Брокер віддає повідомлення, але НЕ видаляє його. Він помічає його як "в обробці" (Unacked). Повідомлення зникне з Брокера лише тоді, коли Консьюмер надішле спеціальний сигнал: "ACK" (Acknowledged — підтверджено). * Аналогія: Рекомендований лист. Поки ви не підпишете папірець, що отримали й прочитали, пошта вважає доставку незавершеною.

Гарантії доставки (Delivery Semantics)

Це три "кити", на яких тримається надійність:

  1. At-most-once (Не більше одного разу): Повідомлення може загубитися, але ніколи не прийде двічі. (Це режим Auto-Ack). Швидко, але ризиковано.
  2. At-least-once (Хоча б один раз): Повідомлення ніколи не загубиться, але може прийти двічі. (Це режим Manual Ack). Надійно, але треба думати про дублікати.
  3. Exactly-once (Рівно один раз): Святий Грааль. Дуже важко досягти в розподілених системах, дорого коштує по ресурсах. Зазвичай ми емулюємо це на рівні логіки (про це пізніше).

Інтуїтивне правило: Якщо вам важлива швидкість (стрімінг відео, збір логів) — обирайте At-most-once. Якщо важливі дані (гроші, замовлення) — тільки At-least-once.


3. 🧪 Приклади: Від простого до реального

Давайте подивимося на псевдокод (схожий на Python/RabbitMQ).

Приклад 1: Фатальна помилка (Auto-Ack)

def callback(message):
    print("Отримав повідомлення...")
    # ...тут відбувається складна робота...
    raise Exception("Ой, база даних впала!") 
    print("Роботу закінчено!")

# Налаштування консьюмера з автоматичним підтвердженням
channel.consume(queue='orders', on_message=callback, auto_ack=True)

Питання до студента: Що станеться з повідомленням, якщо на рядку з Exception програма впаде? Відповідь: Воно зникне назавжди. Брокер видалив його ще в момент передачі (auto_ack=True). Замовлення втрачено. Клієнт плаче.


Приклад 2: Надійний варіант (Manual Ack)

def callback(channel, delivery_tag, message):
    try:
        print("Отримав замовлення. Обробляю...")
        # ...зберігаємо в БД, надсилаємо чек...
        database.save(message)

        # Тільки коли ВСЕ добре — надсилаємо ACK
        channel.basic_ack(delivery_tag) 
        print("Успіх! Брокер може видаляти.")

    except Exception:
        # Якщо сталась помилка — кажемо брокеру "NACK" (Negative Ack)
        # або просто нічого не кажемо і закриваємо з'єднання.
        print("Помилка! Поверніть повідомлення в чергу!")
        channel.basic_nack(delivery_tag, requeue=True)

Що тут відбувається? Якщо функція впаде на етапі database.save, рядок basic_ack ніколи не виконається. Брокер побачить, що зв'язок розірвано, а ack не прийшов. Він скаже: "Ага, робітник помер, не доробивши роботу. Віддам це повідомлення іншому робітнику". Жодних втрат.


Приклад 3: Підступна пастка (Дублікати)

Уявіть, що database.save(message) пройшов успішно. Гроші списано. Але... саме в цю мілісекунду, перед відправкою channel.basic_ack, у дата-центрі вимкнули світло.

  1. Консьюмер впав.
  2. Брокер не отримав Ack.
  3. Брокер думає: "Повідомлення не оброблено!".
  4. Світло вмикають. Брокер віддає це саме повідомлення новому Консьюмеру.
  5. Новий Консьюмер знову робить database.save(message).

Результат: Гроші списано двічі. Це ціна гарантії At-least-once.


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

Час попрацювати головою. Уявіть, що ви Архітектор ПЗ.

Завдання 1: Симуляція Ви пишете сервіс розсилки SMS. * Брокер відправив воркеру завдання: "Надіслати SMS мамі". * Воркер відправив SMS. * Воркер "завис" і не відповів Брокеру. * Через 30 секунд Брокер віддав це завдання іншому Воркеру. * Питання: Що отримає мама? Як це виправити?

Завдання 2: Знайди помилку Джуніор написав код, де channel.basic_ack() стоїть на самому початку функції обробки (першим рядком), а далі йде довга логіка збереження в БД. * До якого типу гарантій (At-most-once чи At-least-once) це призвело фактично? Чому це погана ідея?

Завдання 3: "Отруйне повідомлення" (Poison Message) Є повідомлення, яке містить баг (наприклад, некоректний JSON), через який парсер воркера завжди падає з помилкою. * Воркер бере повідомлення -> Падає -> Брокер повертає повідомлення -> Воркер бере -> Падає... * Це нескінченний цикл. Як, використовуючи механізм Ack/Nack, вирішити цю проблему, щоб не забити чергу? (Підказка: Dead Letter Queue).

Завдання 4: Міні-кейс Вам треба спроектувати систему лайків у соцмережі. * Варіант А: Використовувати Manual Ack для кожного лайка. * Варіант Б: Використовувати Auto-ack. * Аргументуйте: Що ви оберете? Чи страшно, якщо 1 зі 1000 лайків загубиться? Як це вплине на навантаження системи?


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

Як відрізнити новачка від сеньйора у цій темі?

❌ Новачок: * Забуває робити Ack. Черга росте, пам'ять брокера забивається, система зупиняється (Unacked messages висять). * Робить Ack до реального завершення роботи. * Панікує, коли бачить дублікати в базі.

✅ Досвідчений інженер: * Розуміє: Мережа бреше. Відмови трапляються. * Будує Ідемпотентні системи. Це розумне слово означає: скільки б разів ти не виконав операцію з одними й тими ж даними, результат не зміниться. * Приклад: Замість UPDATE balance SET amount = amount - 100 (небезпечно при повторі!), він робить UPDATE transactions SET status = 'paid' WHERE id = 123 (безпечно, другий раз нічого не зміниться). * Використовує таймаути. Якщо повідомлення не підтверджено за 5 хвилин — повертаємо його в чергу.


6. 🧩 Підсумок

Отже, друзі, що ми сьогодні вивчили?

  1. Відправка повідомлення — це лише половина справи. Головне — знати, що його обробили.
  2. Auto-Ack — це швидко, але "діряво".
  3. Manual Ack — це надійно, але вимагає дисципліни.
  4. At-least-once гарантує доставку, але створює дублікати. Боротися з ними треба через ідемпотентність.

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

🚀 Тизер наступного уроку: Ми навчилися передавати повідомлення одному робітнику. А що, як ми хочемо, щоб одне повідомлення отримали всі сервіси одночасно? Або щоб повідомлення про "Платежі" йшли на один сервер, а про "Логи" — на інший? Наступного разу ми розберемо Exchanges, Routing Keys та патерн Pub/Sub. Готуйтеся, буде масштабно!


Маєте питання чи хочете розібрати рішення завдань? Я тут!