ข้ามไปยังเนื้อหา

Deadlines & Cancellation

deadline คือจุดเวลาแบบสัมบูรณ์ — “ยอมแพ้ถ้ายังไม่เสร็จภายใน 10:00:03.500” ส่วน timeout คือช่วงเวลา — “ยอมแพ้หลัง 300ms” — ซึ่ง library ส่วนใหญ่แปลงเป็น deadline ข้างในอยู่ดี

ความต่างนี้สำคัญ เพราะ gRPC ส่งต่อ deadline ไม่ใช่ timeout เมื่อ service A เรียก B แล้ว B เรียก C, deadline สัมบูรณ์ ตัวเดียวกัน วิ่งลงไปตาม chain แต่ละ hop จึงรู้ว่าเหลือเวลาจริง ๆ เท่าไร ไม่มีใครทำงานต่อกับ request ที่ผู้เรียกยอมแพ้ไปแล้ว

flowchart LR
  a["Service A
deadline = now + 300ms"] -->|deadline propagated| b["Service B
sees ~280ms left"]
  b -->|deadline propagated| c["Service C
sees ~250ms left"]
  c -. "if deadline passes,
all hops abort" .-> x["DEADLINE_EXCEEDED"]
deadline สัมบูรณ์ตัวเดียววิ่งลงไปทั้ง call chain

กฎเชิงปฏิบัติที่สำคัญที่สุดใน gRPC: ทุก call ต้องมี deadline call ที่ไม่มี deadline จะรอตลอดกาลโดย default dependency ที่ช้าหรือค้างตัวเดียวจะผูก request ไว้ ซึ่งผูก goroutine/thread/connection ไว้ และตอน load สูงจะลามเป็น stall ทั้งระบบ — เป็นวิธีคลาสสิกที่ service ช้าตัวเดียวลาก service ที่อยู่เหนือขึ้นไปล้มตามกันหมด

นี่คือวิธีตั้ง deadline บน call:

ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel() // always release the context
user, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 42})
if status.Code(err) == codes.DeadlineExceeded {
log.Println("call ran out of time")
}

deadline เป็นทางหนึ่งที่ทำให้ call จบก่อนเวลา; cancellation แบบชัดเจนเป็นอีกทาง ถ้า client หายไป — user ปิดแท็บ, parent request เองถูก cancel — gRPC จะส่งสัญญาณให้ server รู้ว่า call ถูก cancel แล้ว server ที่เขียนดี ๆ จะ เช็ค การ cancel แล้วหยุด: ยกเลิก database query, break loop และคืน resource แทนที่จะคำนวณผลที่ไม่มีใครอ่าน

ฝั่ง server คุณเฝ้าดู context ของ request:

func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
select {
case <-ctx.Done(): // client cancelled or deadline exceeded
return nil, status.FromContextError(ctx.Err()).Err()
default:
}
// ... do the work, ideally passing ctx to the DB call too
return lookup(ctx, req.Id)
}

กฎทอง: ส่งต่อ context ส่ง context ของ request ที่เข้ามาไปยังทุก downstream gRPC call และทุก database query แล้ว deadline หรือ cancellation ตัวเดียวจะคลี่ต้นไม้ของงานทั้งหมดออกอย่างสะอาด

gRPC ส่งต่ออะไรลงไปตาม call chain?
gRPC call ที่ไม่ได้ตั้ง deadline จะเป็นอย่างไร?
status code ไหนบอกว่า call หมดเวลา?
server หลีกเลี่ยงการคำนวณผลที่ไม่มีใครอ่านอย่างไร?