Deadlines & Cancellation
deadline ไม่ใช่ timeout
หัวข้อที่มีชื่อว่า “deadline ไม่ใช่ timeout”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"]
ตั้งเสมอ
หัวข้อที่มีชื่อว่า “ตั้งเสมอ”กฎเชิงปฏิบัติที่สำคัญที่สุดใน 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")}try: # timeout is converted to an absolute deadline internally user = client.GetUser(user_pb2.GetUserRequest(id=42), timeout=0.3)except grpc.RpcError as e: if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED: print("call ran out of time")const deadline = new Date(Date.now() + 300); // absolute timeclient.getUser({ id: 42 }, { deadline }, (err, user) => { if (err?.code === status.DEADLINE_EXCEEDED) { console.log('call ran out of time'); }});cancellation หยุดงานที่เสียเปล่า
หัวข้อที่มีชื่อว่า “cancellation หยุดงานที่เสียเปล่า”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)}def GetUser(self, request, context): if not context.is_active(): # client cancelled / deadline passed return user_pb2.User() # pass context to downstream calls so cancellation propagates return lookup(request.id, context)getUser(call, callback) { if (call.cancelled) return; // client already gave up call.on('cancelled', () => stopWork()); doLookup(call.request.id, callback);}กฎทอง: ส่งต่อ context ส่ง context ของ request ที่เข้ามาไปยังทุก downstream gRPC call และทุก database query แล้ว deadline หรือ cancellation ตัวเดียวจะคลี่ต้นไม้ของงานทั้งหมดออกอย่างสะอาด