Модуль 11

Publishing та consuming повідомлень

Ось урок, створений спеціально для тебе в стилі CS50: енергійно, зрозуміло і з фокусом на архітектурне мислення.


🎓 CS50-Style: Publishing та Consuming Повідомлень

(Архітектура Pub/Sub та асинхронна комунікація)

Привіт, друзі! Радий бачити вас.

Сьогодні ми поговоримо про те, що змушує сучасний світ — від Uber до Instagram — крутитися. Ми розберемося, як різні частини програми спілкуються між собою, не знаючи про існування одна одної.


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

Уявіть, що ви зайшли в кав'ярню. Ви підходите до каси й замовляєте лате. Що відбувається далі?

Чи касир сам йде молоти каву, спінювати молоко, малювати сердечко на пінці, а ви та вся черга стоїте і чекаєте, поки він повернеться на касу?

Звісно, ні. Це було б катастрофою. Один клієнт заблокував би весь заклад.

Натомість касир просто кричить: "Одне лате!" (або клеїть стікер на стійку). Він публікує (Publish) замовлення. А бариста, коли звільниться, бачить стікер, споживає (Consume) це повідомлення і робить каву.

Касир не чекає. Касир вже обслуговує наступного клієнта.

Риторичне запитання: А що, якби в програмуванні ми писали код так, як працює погана кав'ярня?

user_clicked_buy()
send_email_confirmation() # Чекаємо 5 секунд... сервер висить...
notify_warehouse()        # Ще 2 секунди...
update_analytics()        # Ще 1 секунда...
return "Success"          # Клієнт чекав 8 секунд і вже пішов до конкурентів.

Чому без цього не обійтись? У сучасному світі (мікросервіси, хмара) системи не можуть бути жорстко зв’язаними. Якщо сервіс відправки пошти "впав", це не повинно зламати кнопку "Купити". Нам потрібен спосіб сказати: "Гей, хтось, зробіть це, коли буде час!" — і піти далі.

Це і є патерн Pub/Sub (Publisher/Subscriber).


2. 🧠 Теоретична база (А що там "під капотом"?)

Давайте розкладемо магію на три прості компоненти. Уявіть це як поштову систему.

1. Publisher (Паблішер / Видавець) 📢

Це той, хто створює подію. Приклад: Кнопка "Зареєструватися", датчик температури, касир у кав'ярні. Його задача проста: кинути повідомлення у "світ" і забути про нього (Fire and Forget).

2. Message Broker (Брокер) 📬

Це посередник. Це наша "поштова скринька" або "дошка оголошень". Найвідоміші брокери: RabbitMQ, Apache Kafka, Redis, Google Pub/Sub. Він приймає повідомлення від Паблішера і тримає його в себе, доки хтось його не забере. Важливо: Брокер гарантує, що повідомлення не загубиться, навіть якщо отримувач зараз вимкнений.

3. Consumer (Консьюмер / Споживач / Підписник) 👂

Це той, хто обробляє подію. Приклад: Сервіс, що шле SMS; скрипт, що записує дані в базу. Він постійно "слухає" брокера: "Є щось для мене? А зараз? А зараз?"

🧱 Що треба запам'ятати (Logic > Syntax):

  • Decoupling (Розчеплення): Паблішер не знає, хто (і чи взагалі хтось) прочитає повідомлення. Консьюмер не знає, хто його відправив.
  • Asynchronous (Асинхронність): Дії відбуваються не миттєво одна за одною, а в різний час.

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

Уявімо, що ми пишемо код на Python (псевдокод для ясності), використовуючи простого брокера (наприклад, Redis або RabbitMQ).

Приклад 1: "Hello World" (Основа)

Publisher (відправляє повідомлення):

# Підключаємось до брокера
broker.connect()

message = "Привіт, світ! Це моє перше повідомлення."
# Публікуємо в канал (чергу) 'greetings'
broker.publish(channel='greetings', body=message)

print("Повідомлення відправлено!")

Consumer (чекає на повідомлення):

def process_message(msg):
    print(f"Отримано: {msg}")

# Підписуємось на канал 'greetings'
broker.consume(channel='greetings', callback=process_message)

🤔 Запитання до вас: Що станеться, якщо я запущу Publisher, але забуду запустити Consumer? (Подумайте секунду...) Відповідь: Нічого страшного! Повідомлення лежатиме у Брокера (у черзі), доки ви не увімкнете Consumer. Це і є надійність.


Приклад 2: Реальний кейс (Реєстрація користувача)

Уявіть, що користувач зареєструвався. Нам треба: 1. Відправити "Welcome Email". 2. Нарахувати бонусні бали.

Замість одного великого шматка коду, ми робимо так:

Publisher (Сервіс реєстрації):

def register_user(user):
    save_to_db(user)
    # Ми просто кидаємо подію: "Новий юзер з'явився!"
    event = {"event": "USER_CREATED", "user_id": 123, "email": "dave@cs50.harvard.edu"}
    broker.publish(channel='user_events', body=json.dumps(event))
    return "Ви зареєстровані!" # Миттєва відповідь юзеру

Consumer A (Email Service):

# Слухає 'user_events', бачить USER_CREATED -> шле листа.

Consumer B (Rewards Service):

# Слухає ТУ САМУ чергу 'user_events' -> нараховує бонуси.

В чому тут магія? Якщо завтра менеджер скаже: "А давайте ще створювати профіль у CRM?", ви не чіпаєте код реєстрації. Ви просто створюєте Consumer C, який теж слухає ці події. Система розширюється без болю.


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

Прийшов час забруднити руки кодом (або хоча б логікою). Ось твій Challenge Set:

🔹 Завдання 1: Рольова гра Візьми два листки паперу. На одному напиши "Publisher", на іншому "Consumer". Поклади коробку посередині ("Broker"). Напиши на стікері "Замовлення #1" і поклади в коробку. Тепер "зіграй" за Consumer-а — дістань і "оброби". Відчуй цей фізичний процес передачі.

🔹 Завдання 2: Зміна умов У коді Прикладу 2 у нас два консьюмери (Email і Бонуси). Що станеться, якщо сервіс Email впаде з помилкою? * Чи отримає користувач бонуси? (Так, бо сервіси незалежні!) * Чи дізнається про це сервіс реєстрації? (Ні, йому байдуже, він свою роботу зробив).

🔹 Завдання 3: Виправити помилку Новачок написав такий код для Consumer-а:

def callback(msg):
    try:
        process_data(msg)
    except Exception:
        pass # Ігноруємо помилку
    # Повідомляємо брокера, що повідомлення оброблено, видаляй його!
    broker.ack(msg) 

Питання: Що поганого в try...except pass тут? Підказка: Якщо обробка впала (наприклад, база даних недоступна), а ми сказали брокеру "все ок, видаляй", то повідомлення зникне назавжди, а юзер не отримає послугу. Як це виправити? (Не робити ack, щоб брокер віддав повідомлення іншому консьюмеру або спробував пізніше).

🔹 Завдання 4: Міні-кейс Спроєктуй систему для Uber. * Подія: "Водій знайшовся". * Хто має підписатися на цю подію? (Назви мінімум 3 сервіси, яким цікаво знати про це).


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

Як відрізнити новачка від профі в цій темі?

❌ Новачок думає: "Я відправив повідомлення, значить воно точно обробиться прямо зараз". ✅ Профі думає: "Мережа ненадійна. Брокер може впасти. Консьюмер може бути зайнятий. Що буде з моїм повідомленням?"

Три золоті правила:

  1. Idempotency (Ідемпотентність): Страшне слово, проста суть. Якщо ваш консьюмер отримає одне й те саме повідомлення двічі (випадково), він не повинен списати гроші з клієнта двічі. Він має перевірити: "Я це вже обробляв?"
  2. Dead Letter Queue (DLQ): Куди діваються повідомлення, які неможливо обробити (наприклад, "кривий" формат JSON)? Не дозволяйте їм блокувати чергу. Викидайте їх в окрему "сміттєву" чергу (DLQ), щоб потім розібратися вручну.
  3. Моніторинг: Завжди слідкуйте за довжиною черги. Якщо в черзі 1 мільйон повідомлень — ваші консьюмери не справляються. Потрібно додавати нових.

6. 🧩 Підсумок

Отже, що ми сьогодні зробили? Ми розрізали невидимі дроти, які зв’язували наші програми, і поставили між ними надійного листоношу — Брокера.

Тепер ти вмієш: * Розуміти, як будувати системи, що не падають від одного збою. * Мислити подіями ("щось сталось"), а не командами ("зроби це"). * Масштабувати програми, просто додаючи нових "слухачів".

🔮 Тизер наступного уроку: Ми навчилися передавати повідомлення. Але що, як нам треба обробляти 100 000 подій за секунду? Що, як повідомлення повинні зберігатися вічно? Наступного разу ми заглянемо в очі звірові на ім'я Apache Kafka і дізнаємося, як обробляє дані Netflix.

А поки що — це був CS50. Кодимо далі!