Selective Test Execution at Stripe: Fast CI for a 50M-line Ruby monorepo
Bài tóm tắt Stripe 2026 3. Quy mô lớn Độ khó: Khó
Stripe · 2026 · 3. Quy mô lớn · Độ khó: Khó — bài gốc
Bối cảnh
- Monorepo Ruby của Stripe khoảng 50 triệu dòng, khoảng 100.000 file test, 1,2 triệu test unit (2026). Chạy tuần tự hết một lần mất bốn tháng; build chạy trên một dàn worker song song khổng lồ, khoảng 50.000 build mỗi tuần, và tăng thêm khi AI làm tốc độ commit tăng.
- Phân tích phụ thuộc tĩnh (theo import, theo module) dùng tốt ở các codebase khác của Stripe, nhưng Ruby có metaprogramming, dispatch động, nhánh theo cấu hình, và test phụ thuộc file không phải Ruby (YAML, JSON, template, fixture, file sinh ra). Đoán tĩnh hoặc chọn thiếu (bỏ test cần chạy, nguy hiểm) hoặc chọn thừa (gần như chạy hết, chậm).
Thay đổi chính
- Ghi lại thay vì đoán: đo xem mỗi test đã mở file nào, rồi với thay đổi mới, chạy lại test nào từng mở file bị đổi. Đồ thị phụ thuộc mức file tự bắt được phụ thuộc không phải Ruby, và rẻ vì chỉ theo dõi việc mở file, không theo dõi từng dòng chạy.
file_access_interceptor: thư viện C++ nạp bằngLD_PRELOAD, chặn syscallopen(và các biến thể), ghi "file → scope đang chạy". Test runner đặt scope (thường là file test đang chạy); thư viện lan sang mọi tiến trình con. Trong đường chặnopenchỉ append một bản ghi vào file log riêng của tiến trình; mọi việc đắt (gộp, index, đảo chiều) làm ngoài đường nóng.- Scope phân cấp: file mở ở root scope là phụ thuộc toàn cục; đổi nó là chạy hết test. Họ coi đó là "hệ thống trung thực về phụ thuộc chung" (ví dụ các primitive được preload cho mọi test).
- Selection index: scheduler của build (sidecar điều phối test trên worker) gom log, đảo thành bản đồ "file → các test cần chạy", lưu bằng roaring bitmap (khoảng 3 tỉ điểm dữ liệu) để hợp nhiều file thay đổi nhanh. Kèm file inventory: danh sách file và hash (cả file sinh ra, nên không dùng git được), tính bằng
hashdeep; build mới so hash với inventory của build nền để biết file nào đổi. - Ngoại lệ có chủ đích: luôn chạy lại test đang hỏng; luôn chạy các test duyệt thư mục hay glob (thêm file mới đổi hành vi mà không có tín hiệu
open; đa số là test kiểm sức khoẻ codebase); linter nhận danh sách file đổi thay vì quét cả repo. - Build nền nhanh và tái lập được: trạng thái hoàn thành và metadata của build lưu trong MongoDB, sắp theo Monotonic Revision ID (commit A là tổ tiên của B thì
MRI(A) < MRI(B)), nên đầu build chỉ một query là tìm được bộ dữ liệu STE gần nhất, không phải duyệt lịch sử git. "Build nền nào đã được chọn" thành một đầu vào gỡ lỗi được, chạy lại cho ra cùng lựa chọn.
Kết quả (theo Stripe)
- Trung bình chạy khoảng 5% bộ test (trung vị 0,5%), tốn dưới 10% compute so với chạy hết; thay đổi thường gặp chỉ cần vài trăm test thay vì 100.000 file test, mà theo Stripe không giảm độ an toàn.
Vì sao
- Ngôn ngữ càng động thì đồ thị phụ thuộc tĩnh càng sai; tín hiệu đo được lúc chạy (mở file) đúng với mọi loại phụ thuộc (CI và build ở quy mô lớn).
- Hạ tầng CI là một nền tảng nội bộ: dữ liệu của build trước được phục vụ cho build sau với độ tin cậy như một dịch vụ (Nền tảng nội bộ cho service (đường ray chung)).
Bài học cho SWE
- Chọn tín hiệu phụ thuộc chịu được độ động của ngôn ngữ, và đo nó ở ranh giới rẻ (syscall) thay vì ranh giới ồn (từng dòng code).
- Đường đo phải gần như không làm gì; xử lý nặng đẩy ra sau.
- Phụ thuộc toàn cục làm chạy hết test là tín hiệu thật, không phải lỗi; muốn chạy ít hơn thì giảm thứ ở root scope.
- Ghi rõ các loại test mà tín hiệu không bắt được và luôn chạy chúng.
- Làm cho lựa chọn tái lập được: cùng build nền → cùng tập test.
Liên quan
#CI#Monorepo#Ruby#Selective test execution#LD_PRELOAD#Dependency graph#Roaring bitmap#hashdeep#Build scheduler#MongoDB
Được nhắc tới trong
Công ty (1)