Java delayedExecutor: หน่วงการส่งงานโดยไม่สร้าง scheduler ใหม่ทุกจุด
บาง pipeline ต้องรอก่อนส่งงาน เช่น backoff หลังบริการตอบชั่วคราวไม่พร้อม การสร้าง ScheduledExecutorService ใหม่ตามทุก call ทำให้ lifecycle กระจัดกระจาย CompletableFuture.delayedExecutor คืน Executor wrapper ที่รอตามกำหนดแล้วค่อยส่งงานให้ executor ปลายทางครับ
WHY: แยก “เมื่อไรเริ่ม” ออกจาก “ที่ไหนรัน”
เหมือนบัตรคิวหน้าร้าน: เครื่องกดบัตรกำหนดว่าจะเรียกเมื่อไร แต่พนักงานหลังเคาน์เตอร์เป็นคนทำงานจริง delayedExecutor รับ delay กับ TimeUnit และอาจรับ base executor จึงแยกนโยบายเวลาจาก pool ที่รันงานได้
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
public static void main(String[] args) {
AtomicBoolean ran = new AtomicBoolean(false);
Executor direct = Runnable::run;
Executor delayed = CompletableFuture.delayedExecutor(
0, TimeUnit.MILLISECONDS, direct
);
CompletableFuture<Void> task = CompletableFuture.runAsync(
() -> ran.set(true),
delayed
);
task.join();
assert ran.get();
assert task.isDone();
assert !task.isCompletedExceptionally();
}
}
test ใช้ delay ศูนย์เพื่อไม่ให้ flaky แต่ยังตรวจ composition จริง ใน production การหน่วงไม่ใช่ timeout: delayedExecutor บอกว่า “ยังไม่เริ่มก่อนเวลา” ส่วน orTimeout บอกว่า “ถ้ายังไม่เสร็จในเวลาให้ล้ม” สองอย่างใช้ร่วมกันได้แต่ตอบคนละคำถาม
อย่าใช้เป็น retry policy ทั้งก้อน
retry ต้องตัดสินชนิด error, จำนวนครั้ง, exponential backoff, jitter, idempotency และ cancellation delayedExecutor ให้เพียงกลไกส่งงานช้า หากวน retry แบบไม่จำกัดก็ยังถล่มปลายทางช้าลงเท่านั้น เหมือนเลื่อนรถติดออกไปห้านาทีแต่ไม่จำกัดจำนวนรถ
default overload ใช้ executor ภายในของ CompletableFuture ขณะที่ overload สาม argument ส่งต่อไป base executor ที่เราเลือก งาน blocking ควรใช้ pool ที่ออกแบบสำหรับ blocking ไม่ใช่โยนลง common pool โดยไม่ดู capacity และควร propagate cancellation/trace context ตามระบบจริง
Recap
- ใช้ delayedExecutor เพื่อกำหนดเวลาเริ่ม
- ใช้ timeout แยกสำหรับเส้นตายของงาน
- ใส่ retry limit, jitter และ idempotency เอง
- เลือก base executor ให้ตรงกับงาน CPU หรือ blocking