App: hệ thống thông báo với Observer và queue

Xây hệ thống thông báo đa kênh: Observer pub-sub, hàng đợi retry, template Strategy, đo tỉ lệ gửi và độ trễ.
KhóDesign pattern ~120 phút

Giới thiệu

Bài này xây hệ thống gửi thông báo đa kênh. 1 sự kiện bắn ra là email SMS push cùng chạy.

Kiến thức cần nhớ

Observer pub-sub là gì

  • Publisher bắn event vào bus, không biết ai nghe.
  • Subscriber đăng ký theo chủ đề như đơn hàng hay khuyến mãi.
  • Hủy đăng ký thì ngừng nhận ngay.
  • Ví dụ đời thường: loa phường phát, ai mở cửa mới nghe.

Hàng đợi và retry

  • Event vào queue bền nên burst 5000 tin không mất.
  • Worker lấy tin gửi dần, lỗi thì retry với backoff mũ.
  • Hỏng N lần thì chuyển dead-letter chờ xử lý tay.
  • Digest gom 10 tin cùng user thành 1 tin tổng hợp.

Kênh gửi dạng Strategy

  • 3 kênh email SMS push cùng 1 interface send.
  • Mỗi kênh có giới hạn tốc độ và tỉ lệ lỗi riêng.
  • Thêm kênh webhook mới không sửa bus cũ.
  • Đo tỉ lệ thành công và p95 độ trễ mỗi kênh.

Ví dụ

  • Input: subscriber A đăng ký cảnh báo, B đăng ký khuyến mãi. Output: mỗi người chỉ nhận đúng chủ đề mình.
  • Giải thích: bus lọc theo chủ đề trước khi đẩy vào queue.
  • Input: kênh lỗi 30%, gửi 200 tin có retry. Output: thành công cuối trên 95%, còn lại vào dead-letter.
  • Giải thích: retry vớt được lỗi tạm thời, lỗi vĩnh viễn gom riêng.

💻 Bài tập

Ngôn ngữ: Python 3 với asyncio (hoặc Go). Dùng Observer ngày 9, rate limiter ngày 128. Nộp vào day-149-notify/.

Bài 1: Event bus

class EventBus:
    def subscribe(self, topic: str, handler: Callable[[Event], Awaitable[None]]) -> str: ...  # trả subscription id
    def unsubscribe(self, sub_id: str): ...
    async def publish(self, event: Event): ...    # topic hỗ trợ ký tự đại diện: "order.*"
subscribe("order.*"), publish("order.paid") -> nhận
subscribe("order.paid"), publish("order.shipped") -> không nhận
unsubscribe rồi publish -> không nhận
1 handler lỗi -> các handler khác vẫn nhận

Bài 2: Hàng đợi bền, retry và dead-letter

Thành phầnYêu cầu
Hàng đợiSQLite (bảng jobs: id, payload, attempts, next_run_at, status). Khởi động lại không mất việc
WorkerN worker lấy việc có next_run_at <= now
RetryBackoff mũ có jitter: min(60, 2^attempt) * random(0.5, 1.5) giây
Dead-letterSau 5 lần lỗi chuyển status = dead. Có lệnh chạy lại việc chết
IdempotencyMỗi thông báo có idempotency_key, gửi trùng thì bỏ qua

3 kênh cùng interface Channel.send(user, message): EmailChannel (lỗi ngẫu nhiên 5%), SmsChannel (giới hạn 10 tin/giây), PushChannel (lỗi 20%). Thêm WebhookChannel không sửa bus hay worker.

Bài 3: Bùng nổ và gom tin (nâng cao)

  1. Bắn 5000 sự kiện trong 1 giây. Không mất tin nào: số tin gửi thành công + dead-letter = 5000.
  2. Digest: 1 user nhận trên 3 thông báo khuyến mãi trong 10 phút thì gom thành 1 tin tổng hợp.
  3. Tùy chọn người dùng: mỗi user chọn kênh cho từng loại thông báo và giờ yên lặng (22h-7h, trừ thông báo bảo mật).
  4. Dashboard dòng lệnh: số việc chờ, đang chạy, thành công, chết, theo kênh.

✅ Tiêu chí hoàn thành

  • Test event bus 4 kịch bản pass.
  • Giết tiến trình giữa chừng, khởi động lại không mất việc.
  • Bài 3: 5000 sự kiện đếm đủ, có digest và giờ yên lặng.
  • Thêm WebhookChannel chỉ tạo file mới.

❓ Câu hỏi ôn tập

  • Publisher và subscriber tách nhau qua bus thế nào?
  • Queue giúp burst lớn không mất tin ra sao?
  • Retry backoff và dead-letter mỗi cái xử lý lỗi nào?
  • Thêm kênh mới vì sao không cần sửa bus?

🌕 Bạn đã hoàn thành ngày 149. Làm hết bài tập phía trên rồi sang ngày tiếp theo.

Bớt paste mù, hiểu sâu từng dòng mình viết.

180 Days Challenge. 180 ngày cai nghiện vibe code.