Go: วิธีเขียน http handler ให้รู้ว่า request ถูก cancel ไปแล้ว
net/http (หรือ gin) handler จะมีจะรับ request type เข้ามาคือ type *http.Request ซึ่งในนี้จะมีตัวแปร context ของ request ที่เราสามารถใช้เช็คได้ว่า request ถูก cancel ไปแล้วหรือยัง
ตัวอย่างเช่นถ้าเรามี handler แบบนี้
package main
import (
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("start")
defer log.Println("done")
// simulate long running process
time.Sleep(60 * time.Second)
w.Write([]byte("Hello World"))
})
http.ListenAndServe(":8000", nil)
}
ถ้าเราเรียกไปที่ endpoint นี้ด้วย curl http://localhost:8000
จะต้องรอ 60 วินาทีถึงจะได้ response กลับมาเป็น "Hello World"
และเราจะเห็น log แบบนี้
2023/08/18 14:14:20 start
2023/08/18 14:15:20 done
และถ้าเราเรียกอีกครั้ง แต่กด ctrl+c ก่อนเพื่อยกเลิกการทำงานของ curl และปิด connection ไปแล้วนั้น จะเห็นว่า time.Sleep
ก็ยังทำงานต่อไปจนครบ 60 วินาทีแล้วค่อยจบการทำงาน ยังเห็น log pattern เดิมแบบนี้
2023/08/18 14:14:20 start
2023/08/18 14:15:20 done
คือทำจนครบ 60 วินาที
ถ้าเราอยากจะเช็คได้ว่า request ถูก cancel แล้วหรือยังให้เราเขียนโค้ดเช็คจาก request.Context() ได้โดยใช้เมธอด Done() ของ request.Context() เพื่อดักจับ signal จาก channel นี้ว่า request cancel แล้วหรือยัง นอกจากนั้นเราจะเปลี่ยน time.Sleep เป็น time.NewTimer เพื่อที่จะสามารถยกเลิกการ sleep ได้ผ่าน เมธอด timer.Stop
package main
import (
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("start")
defer log.Println("done")
// simulate long running process with aware of context
tm := time.NewTimer(60 * time.Second)
select {
case <-r.Context().Done():
tm.Stop()
case <-tm.C:
w.Write([]byte("Hello World"))
}
})
http.ListenAndServe(":8000", nil)
}
อย่างไรก็ตาม ต่อให้เราใช้ r.Context.Done() เพื่อเช็คแล้ว แต่พอ request ถูก disconnect จากฝั่ง client แล้วแต่กลับยังไม่มี signal ส่งมาที่ Done channel นั้นเป็นเพราะว่า client อาจจะส่ง request body มาด้วย แต่ handler ไม่ยอม read request body ที่ส่งมาให้หมดก่อน ตัวอย่างเช่นถ้าเราเรียก curl ด้วย POST และมี body ด้วย แบบนี้
curl -XPOST http://localhost:8000 -d '{}'
แล้วกด ctrl-c ตัว handler ก็ยังจะรอ 60 วิอยู่ดี
วิธีที่จะทำให้เช็คได้คือ ต้อง flush request body ออกให้หมดก่อนด้วย ซึ่งการเขียน REST หรือ HTTP API ปกติ เราก็จะ unmarshaling request body แล้วทำให้อ่านจนหมดกันอยู่แล้ว ถ้าจะจำลองก็ใช้ io.Copy ได้แบบนี้
package main
import (
"io"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("start")
defer log.Println("done")
// simulate unmarshaling request body
io.Copy(io.Discard, r.Body)
// simulate long running process with aware of context
tm := time.NewTimer(60 * time.Second)
select {
case <-r.Context().Done():
tm.Stop()
case <-tm.C:
w.Write([]byte("Hello World"))
}
})
http.ListenAndServe(":8000", nil)
}
กลไกการเช็คแบบนี้เป็น pattern ที่ package context ใช้งาน และ pattern ที่เราเห็นได้เป็นปกติใน Go คือส่ง ctx contex.Context เป็น parameter แรกต่อๆกันไป เพื่อให้ process ที่ทำงานเช็คได้ว่าควรจะยกเลิกการทำงานที่เหลือเวลา request ถูกยกเลิกไปแล้วนั่นเอง