Модуль 14

Dead Letter Exchanges (DLX)

Ось твій урок у стилі David Malan (CS50). Уяви, що я стою на сцені в Sanders Theatre, рукави закочені, і я готовий пояснити тобі, як не втратити найважливіше — твої дані.


🎓 Тема: Dead Letter Exchanges (DLX)

Або: "Кладовище загиблих повідомлень" (і як їх воскресити)


1. 🔥 Вступ: проблема та мотивація

Уявімо ситуацію. Ти пишеш бекенд для великого інтернет-магазину. Чорна п’ятниця. Тисячі замовлень летять у твою чергу повідомлень (RabbitMQ, Kafka, SQS — неважливо). Твій воркер (програма-обробник) бере замовлення, намагається списати гроші з картки клієнта... і БАМ! 💥

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

Риторичне питання: Що твій код має зробити з цим повідомленням? 1. Викинути його? А якщо це замовлення на $1000? Клієнт тебе з’їсть. 2. Повернути назад у чергу? Окей, воркер візьме його знову через мілісекунду, знову впаде, знову поверне... Ти отримаєш нескінченний цикл, процесор закипить, а логи заповнять увесь диск. Це називається Poison Message ("отруйне повідомлення").

Тож, як нам бути? Нам потрібне місце, куди ми можемо дбайливо відкласти "проблемні" повідомлення, щоб розібратися з ними пізніше, не зупиняючи роботу всього конвеєра.

Нам потрібен Dead Letter Exchange (DLX).

Аналогія з життя: Уяви звичайну пошту. Якщо на конверті розмита адреса, листоноша не викидає листа у смітник і не носить його з собою вічно. Він відправляє його у спеціальний відділ "незатребуваних листів" (Dead Letter Office). Там спеціальні люди (адміни) розбираються: повернути відправнику чи спробувати розшифрувати адресу.


2. 🧠 Теоретична база (без сухої академічності)

Отже, що таке DLX?

Насправді, тут немає ніякої магії. Dead Letter Exchange — це звичайнісінький Exchange (обмінник), просто ми використовуємо його для специфічної мети: приймати повідомлення, які "померли" в основній черзі.

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

Уяви чергу повідомлень як конвеєрну стрічку. В кінці стоїть робот (Consumer). Якщо робот каже: "Я не можу це обробити", стрічка має механізм "катапульти", який перекидає деталь на іншу, запасну стрічку.

Що треба запам’ятати залізною логікою: Повідомлення потрапляє в DLX, коли стається одна з трьох подій: 1. Rejected (відхилено): Консюмер каже basic.reject або basic.nack із прапорцем requeue=false. ("Я не буду це їсти, приберіть"). 2. Expired (прострочено): У повідомлення закінчився TTL (Time To Live). ("Термін придатності вийшов"). 3. Max Length (переповнення): Черга переповнена, і старі повідомлення витісняються, щоб звільнити місце новим.

Інтуїтивно: DLX — це "ізолятор" або "карантинна зона" для ваших даних.


3. 🧪 Приклади (від простого до реального)

Давайте подивимось на це в контексті RabbitMQ (стандарт де-факто для вивчення черг).

🐣 Приклад 1: Налаштування "Катапульти"

Як ми кажемо основній черзі: "Якщо щось піде не так, кидай сюди"? Ми використовуємо аргументи при створенні черги (policy).

Ти очікуєш побачити складний код? Ні, це просто конфігурація.

// Аргументи основної черги "orders_queue"
{
  "x-dead-letter-exchange": "orders_dlx", // Куди кидати?
  "x-dead-letter-routing-key": "failed_order" // З якою міткою?
}

Що тут відбулося? Ми зв'язали orders_queue з обмінником orders_dlx. Тепер будь-яке "мертве" повідомлення полетить туди.


🚀 Приклад 2: Реальний сценарій (Обробка помилок)

Уяви Python-скрипт, який обробляє платежі.

def callback(ch, method, properties, body):
    try:
        data = json.loads(body)
        process_payment(data) # Успіх!
        ch.basic_ack(delivery_tag=method.delivery_tag) # Підтверджуємо

    except json.JSONDecodeError:
        # Опа! Прийшло сміття замість JSON.
        print("Це повідомлення зламане! Відправляю в DLX.")

        # КЛЮЧОВИЙ МОМЕНТ:
        # requeue=False означає "не повертай у цю чергу, 
        # відправ туди, куди вказує x-dead-letter-exchange"
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

Чому так? Якщо ми тут поставимо requeue=True, повідомлення повернеться в початок черги, ми знову спробуємо зробити json.loads, знову впадемо... і так до кінця часів. requeue=False + налаштований DLX рятує систему від зациклення.


🧙‍♂️ Приклад 3: Магія відкладених повторів (Delayed Retry)

А що, якщо помилка тимчасова? Наприклад, база даних перезавантажується. Ми хочемо спробувати обробити повідомлення ще раз, але через 10 хвилин.

RabbitMQ не має вбудованої функції "delayed message" з коробки (без плагінів). Але ми можемо схитрувати, використовуючи DLX!

Схема: 1. Створюємо чергу wait_queue з TTL = 600000 (10 хв) і без консюмера. 2. Вказуємо їй DLX, який веде назад у... main_queue! 3. Коли повідомлення в wait_queue "протухає" (спливає час), воно стає "мертвим" і летить у DLX -> main_queue.

Вуаля! Ми зробили відкладену доставку, використовуючи механізм "смерті" повідомлень. Геніально, правда?


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

Час забруднити руки. Уяви, що ти маєш доступ до RabbitMQ Management UI або пишеш код.

Завдання 1: Створити "Морг" Створи Exchange з назвою dlx_exchange (тип direct) і прив'яжи до нього чергу dead_letters_queue. Це буде наш морг для повідомлень.

Завдання 2: Зв'язати живих і мертвих Створи основну чергу main_work_queue. У її аргументах (Arguments) додай: * x-dead-letter-exchange: dlx_exchange * x-message-ttl: 5000 (5 секунд).

Завдання 3: Експеримент з часом Відправ повідомлення в main_work_queue і... нічого не роби. Не запускай консюмера. Питання: Що станеться через 5 секунд? Перевір dead_letters_queue. Воно має з'явитися там. Чому?

Завдання 4: Ручний реджект Напиши скрипт (або використай інтерфейс), який читає повідомлення з main_work_queue і одразу робить Nack (Negative Acknowledgement) з requeue=False. Переконайся, що воно миттєво телепортується в DLX.

Завдання 5: Міні-кейс Придумай архітектуру для системи розсилки Email. Умова: Якщо поштовий сервер не відповідає, ми не маємо втратити лист. Ми маємо спробувати відправити його 3 рази з інтервалом у 1 хвилину. Якщо і тоді не вийде — покласти в "чергу помилок" для ручного аналізу адміністратором. Намалюй схему руху повідомлення.


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

Ти запитаєш: "Девіде, а де тут пастка?"

Типові помилки новачків (Junior mistakes):

  1. Ігнорування DLQ: Ви налаштували DLX, повідомлення туди падають... і лежать там вічно. Черга росте, пам'ять сервера закінчується, сервер падає.
  2. Як думає профі: "Мені потрібен моніторинг! Якщо в DLQ більше 10 повідомлень — надсилай мені SMS, сирену, викликай поліцію!"
  3. Infinite Loop: Основна черга кидає в DLX, а DLX (помилково) налаштований кидати назад в основну чергу без затримки. Це пін-понг, який вб'є ваш CPU.

Поради з "полів":

  • Завжди аналізуйте причину смерті. RabbitMQ додає в заголовок повідомлення поле x-death. Там написано, чому воно померло (rejected, expired тощо). Читайте це у вашому коді обробника помилок!
  • Не бійтеся ручного втручання. Іноді кращий спосіб обробити DLQ — це написати скрипт, який адмін запускає раз на тиждень, щоб розібрати "сміття".

6. 🧩 Підсумок

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

  1. Повідомлення не повинні зникати безслідно. Це гріх розробника.
  2. DLX — це ваша страхівка. Це механізм автоматичного перенаправлення повідомлень, які не вдалося обробити.
  3. Ми використовуємо це для обробки помилок, відкладених повторів (retries) та діагностики системи.

Тепер ти вмієш будувати надійні системи (resilient systems), які не "ковтають" помилки, а акуратно складають їх для аналізу.

Що далі? Тепер, коли ми вміємо зберігати "мертві" повідомлення, виникає питання: а що, якщо ми випадково обробимо одне й те саме замовлення двічі? Як зробити так, щоб клієнт не заплатив двічі?

На наступному уроці ми поговоримо про Ідемпотентність.

А поки що... це був CS50! 👋