Vert.x không có async/await, không có CompletableFuture ở tầng API. Nó có FuturePromise — hai kiểu đơn giản tới mức dễ dùng sai. Bài này đo bốn hành vi mà bạn sẽ gặp trong tuần đầu tiên.

Promise là đầu ghi, Future là đầu đọc

Promise<String> p = Promise.promise();       // bạn hoàn tất nó
vertx.setTimer(20, id -> p.complete("xong")); // hoặc p.fail(...)
return p.future();                            // người gọi chỉ đọc

Đây là cách bọc mọi API kiểu callback thành thứ ghép nối được. Quy tắc: hàm của bạn trả Future, và chỉ nơi tạo ra công việc mới giữ Promise. Đưa Promise cho người gọi là mời họ hoàn tất nhầm nó.

compose: hỏng một bước là dừng cả chuỗi

buoc1()
  .compose(v -> buoc2())     // bước này hỏng
  .compose(v -> buoc3())
  .compose(v -> buoc4())
  .onFailure(t -> ...);
chuỗi thất bại: buoc-2 hong
các bước ĐÃ chạy: [buoc-1, buoc-2]

Bước 3 và 4 không bao giờ chạy. Đây là hành vi bạn muốn — nhưng nhớ rằng nó cũng có nghĩa là mọi việc dọn dẹp đặt ở cuối chuỗi cũng bị bỏ qua. Cần chắc chắn chạy thì dùng eventually, đừng nối vào compose.

recover khác otherwise ở chỗ nào

Hai cái tên gần giống nhau, và khác biệt là kiểu trả về:

.recover(t -> Future.succeededFuture("dự phòng"))   // trả về FUTURE
.otherwise(t -> "dự phòng")                          // trả về GIÁ TRỊ

Cả hai đều biến thất bại thành thành công, nhưng recover cho bạn chạy tiếp một việc bất đồng bộ — gọi sang dịch vụ dự phòng, đọc cache — còn otherwise chỉ thay bằng một giá trị có sẵn ngay.

Quy tắc chọn: cần I/O để cứu thì recover, có sẵn giá trị thì otherwise.

Ngoại lệ đi về đâu

Đây là phần đáng nhớ nhất của bài, vì hai trường hợp trông giống nhau lại xử sự ngược nhau:

ném trong map()       -> map bắt lại và biến thành thất bại: IllegalStateException
ném trong onSuccess() -> onFailure có chạy không? KHÔNG — ngoại lệ bị nuốt
map, compose, flatMap là các toán tử biến đổi: ngoại lệ ném ra trong đó được bắt lại và chuyển thành thất bại của Future, nên onFailure phía sau bắt được. onSuccessonFailure là các toán tử tiêu thụ — chúng đứng ở cuối chuỗi, và ngoại lệ ném ra ở đó không đi đâu cả.

Hệ quả thực tế: nếu logic nghiệp vụ của bạn nằm trong onSuccess và nó ném, bạn mất luôn lỗi đó — không log, không 500, request treo cho tới khi hết giờ. Hãy đặt việc thật trong map/compose, và để onSuccess chỉ làm đúng một việc là trả lời.

Chi phí thật của một chuỗi Future

Câu hỏi hay gặp: bọc mọi thứ vào Future có tốn không? Đo 5 triệu lần, kết quả hai vế được đối chiếu để chắc chắn JIT không xoá mất vòng lặp:

Mỗi thao tác Số thao tác mỗi giây
Gọi hàm trực tiếp (3 bước) 0,29 ns 3 399 385 663
Chuỗi 3 Future 25,03 ns 39 952 383

Đắt hơn 85 lần — và con số đó hoàn toàn không quan trọng. Chênh lệch tuyệt đối là 25 nano giây, trong khi một lời gọi mạng nội bộ tốn khoảng 200 000 nano giây. Chuỗi Future đắt gấp 85 lần một phép nhân, và rẻ hơn tám nghìn lần so với việc mà nó đang điều phối.

Bốn mươi triệu chuỗi mỗi giây cũng cao hơn nhiều lần thông lượng tối đa mà phần 1 đo được cho chính máy chủ HTTP (86 804 req/s).

Nhưng đừng bọc vòng lặp tính toán vào Future. Nếu bạn đang map từng phần tử của một mảng một triệu phần tử, 25 ns nhân lên thành 25 mili giây — và phần 3 đã đo 50 ms chặn tốn bao nhiêu.

Thử ba mươi giây

// tim moi cho ban dat logic nghiep vu vao onSuccess
grep -rn "onSuccess(.*->.*{" src/ | head -20

Mỗi khối onSuccess nhiều hơn một dòng là một chỗ ngoại lệ có thể biến mất. Chuyển phần xử lý sang map hoặc compose, giữ onSuccess cho đúng việc trả lời.

Bài sau: ghép nhiều Future — all, any, join — và đo khác biệt giữa chạy song song với chạy tuần tự.