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
Ôn lại trước khi học
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ần | Yêu cầu |
|---|---|
| Hàng đợi | SQLite (bảng jobs: id, payload, attempts, next_run_at, status). Khởi động lại không mất việc |
| Worker | N worker lấy việc có next_run_at <= now |
| Retry | Backoff mũ có jitter: min(60, 2^attempt) * random(0.5, 1.5) giây |
| Dead-letter | Sau 5 lần lỗi chuyển status = dead. Có lệnh chạy lại việc chết |
| Idempotency | Mỗ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)
- 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.
- 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.
- 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).
- 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
WebhookChannelchỉ 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.