Cái cron job chết lặng — ai báo cho bạn?
Scheduled workflow âm thầm ngừng chạy, bug gzip lọt vào production SSE. Bài học từ asgi-gzip 0.3 cho mọi team đang "set and forget".
Bụi WireAutomation đáng sợ nhất không phải automation chạy sai
Mình từng nghĩ thứ kinh khủng nhất trong CI/CD là pipeline đỏ lè lúc 5 giờ chiều thứ Sáu. Nhưng không. Thứ kinh khủng nhất là pipeline không chạy gì cả — và tuyệt đối không ai hay biết.
Chuyện vừa xảy ra với Simon Willison — tác giả Datasette. Anh deploy một feature mới dùng SSE (Server-Sent Events) lên production. Local chạy mượt. Production: client không nhận được event nào. Đào xuống tận đáy dependency chain mới phát hiện: thư viện asgi-gzip đang nén cả response text/event-stream. SSE cần stream từng chunk liên tục, gzip lại giữ data chờ đủ buffer mới nhả — hai thứ đó đá nhau.
Plot twist: Starlette — framework gốc mà asgi-gzip được extract ra — đã fix bug này từ lâu. Bản thân asgi-gzip có hẳn một scheduled GitHub Actions workflow để tự đồng bộ fix từ upstream. Nhưng workflow đó đã âm thầm ngừng chạy. Không alert. Không log. Im re.
Đèn giao thông hỏng mà không ai biết
Hình dung thế này: ngã tư có đèn tín hiệu tự động. Nửa đêm đèn hỏng, đường vắng chẳng ai để ý. Sáng ra giờ cao điểm — va chạm liên hoàn. Cả khu phố ngạc nhiên: "Ủa, đèn này không phải tự động hả?"
Scheduled workflow cũng y chang. Bạn set cron 0 0 1 (chạy mỗi thứ Hai), nó chạy ngon lành vài tháng, rồi GitHub thay đổi policy với inactive repo, token hết hạn, hay đơn giản là permissions thay đổi — workflow dừng. Nhưng vì nó không block PR hay deployment nào, hoàn toàn vô hình.
Mình từng chia sẻ trong bài về dependency đổi lúc 3 giờ sáng — chuyện upstream thay đổi mà downstream không biết. Lần này còn ironic hơn: cơ chế tự động theo dõi upstream... chính nó lại chết mà không ai canh.
Chuyện thường thấy ở hai dự án thật
Dự án A: Bot Slack "canh gác" release notes
Giả sử team bạn 5 người, có một GitHub Action chạy hàng tuần, tự pull release notes từ dependency chính rồi post vào channel Slack. Tháng đầu mọi người đọc chăm chỉ. Tháng thứ ba channel bị mute vì "nhiều tin quá". Tháng thứ sáu workflow lỗi do token expire — nhưng channel đã mute nên không ai thấy nó ngừng. Khi một CVE nghiêm trọng được patch ở upstream, team vẫn chạy bản có lỗ hổng, hoàn toàn vô tư.
Dự án B: Middleware gzip "best practice" phản chủ
Team build API bằng FastAPI, thêm GZipMiddleware vì ai cũng bảo "best practice — giảm bandwidth mà". Vài tháng sau, team thêm endpoint SSE cho real-time notification. Local không bật middleware nên chạy ngon. Lên staging có middleware: SSE bị buffer, client cứ chờ mãi rồi timeout. Mất nửa ngày debug mới ra — gzip đang ôm data chờ đủ lượng mới chịu nhả.
Kiểu bug này hiểm vì nó không throw error, không crash server. Nó chỉ khiến mọi thứ im lặng hoặc chậm đi — như đèn giao thông hỏng mà xe vẫn qua được, chỉ là va quẹt nhiều hơn bình thường thôi.
Chiều nay: audit "zombie workflows" trong repo
Bốn bước, mất chưa tới 30 phút:
Bước 1 — Tìm tất cả workflow có trigger schedule:
grep -r "schedule:" .github/workflows/ --include="*.yml" -l
Bước 2 — Kiểm tra lần chạy cuối:
gh run list --workflow=sync-upstream.yml --limit=5
Nếu lần chạy cuối cách đây vài tháng, hoặc status toàn cancelled / failure — xin chúc mừng, bạn có zombie.
Bước 3 — Thêm "heartbeat" cho scheduled workflow. Cách đơn giản nhất: step cuối mỗi lần chạy gửi một ping qua webhook Slack hoặc ghi timestamp vào một file. Nếu không nhận ping đúng lịch, bạn biết ngay có vấn đề. Một số team dùng Healthchecks.io (open-source, self-host được) — tạo một check, workflow ping khi chạy xong, nếu miss thì gửi alert.
Bước 4 — Review dependency chain. Với các library extracted từ framework lớn (kiểu asgi-gzip từ Starlette), đặt lịch review upstream changelog mỗi quý. Automation giỏi, nhưng automation có người kiểm tra vẫn giỏi hơn.
Cái giá của "extract rồi quên"
Pattern "tách module từ framework lớn thành library riêng" rất phổ biến trong open-source. Lợi ích rõ ràng: dùng nhẹ nhàng mà không cần kéo cả framework. Cái giá ẩn: bạn fork logic mà không fork quy trình bảo trì.
Starlette có CI liên tục, contributor đông, test suite dày. asgi-gzip cũng cẩn thận set scheduled sync — nhưng chỉ cần một mắt xích đứt, toàn bộ cơ chế đồng bộ sụp đổ trong im lặng.
Nếu team bạn đang maintain internal library extract từ nguồn khác, hãy tự hỏi: "Nếu upstream fix critical bug ngày mai, bao lâu thì fix đó chạy tới production của mình?" Nếu câu trả lời phụ thuộc hoàn toàn vào một cron job, bạn đang đi trên dây mà không có lưới.
Với case cụ thể: asgi-gzip 0.3 đã fix đúng — skip compression cho text/event-stream. Nếu bạn đang dùng GZipMiddleware qua Starlette gốc thì yên tâm, fix đã có từ trước. Nhưng nếu dùng qua wrapper library bên thứ ba — kiểm tra lại version đi, đừng giả định.
Cron job không phải "set and forget"
CI/CD pipeline giống hệ thống phòng cháy tòa nhà — lắp xong không có nghĩa là quên được. Bình cứu hỏa vẫn cần kiểm tra định kỳ, đèn báo khói vẫn cần thay pin.
Bài release asgi-gzip 0.3 chỉ là một bản vá nhỏ. Nhưng câu chuyện đằng sau — một automation âm thầm chết, một fix upstream không bao giờ đến được downstream — là thứ có thể xảy ra với bất kỳ ai đang vận hành hệ thống đủ phức tạp. Chiều nay, thử chạy gh run list xem mấy con scheduled workflow trong repo bạn còn thở không. Biết đâu có zombie nào đang nằm chờ gây họa đấy.
---
Bụi Wire — nghiện đọc release notes lúc 2 giờ sáng