Предположим, что я пишу обработчик http, который делает что-то еще, прежде чем вернуть ответ, нужно ли мне настроить прослушиватель, чтобы проверить, был ли отменен контекст запроса http? чтобы он мог немедленно вернуться, или есть ли другой способ выйти из обработчика при отмене контекста запроса?
func handleSomething(w http.ResponseWriter, r *http.Request) {
done := make(chan error)
go func() {
if err := doSomething(r.Context()); err != nil {
done <- err
return
}
done <- nil
}()
select {
case <-r.Context().Done():
http.Error(w, r.Context().Err().Error(), http.StatusInternalServerError)
return
case err := <-done:
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
}
func doSomething(ctx context.Context) error {
// simulate doing something for 1 second.
time.Sleep(time.Second)
return nil
}
Я попытался сделать для него тест, но после отмены контекста функция doSomething
не останавливалась и продолжала работать в фоновом режиме.
func TestHandler(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/something", handleSomething)
srv := http.Server{
Addr: ":8989",
Handler: mux,
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
if err := srv.ListenAndServe(); err != nil {
log.Println(err)
}
}()
time.Sleep(time.Second)
req, err := http.NewRequest(http.MethodGet, "http://localhost:8989/something", nil)
if err != nil {
t.Fatal(err)
}
cl := http.Client{
Timeout: 3 * time.Second,
}
res, err := cl.Do(req)
if err != nil {
t.Logf("error: %s", err.Error())
} else {
t.Logf("request is done with status code %d", res.StatusCode)
}
go func() {
<-time.After(10 * time.Second)
shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(shutdown)
}()
wg.Wait()
}
func handleSomething(w http.ResponseWriter, r *http.Request) {
done := make(chan error)
go func() {
if err := doSomething(r.Context()); err != nil {
log.Println(err)
done <- err
}
done <- nil
}()
select {
case <-r.Context().Done():
log.Println("context is done!")
return
case err := <-done:
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
}
func doSomething(ctx context.Context) error {
return runInContext(ctx, func() {
log.Println("doing something")
defer log.Println("done doing something")
time.Sleep(10 * time.Second)
})
}
func runInContext(ctx context.Context, fn func()) error {
ch := make(chan struct{})
go func() {
defer close(ch)
fn()
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-ch:
return nil
}
}
🤔 А знаете ли вы, что...
Go используется для создания контейнеров Docker.
Я только что немного отрефакторил предоставленное решение, и теперь оно должно работать. Позвольте мне провести вас через соответствующие изменения.
doSomething
func doSomething(ctx context.Context) error {
fmt.Printf("%v - doSomething: start\n", time.Now())
select {
case <-ctx.Done():
fmt.Printf("%v - doSomething: cancelled\n", time.Now())
return ctx.Err()
case <-time.After(3 * time.Second):
fmt.Printf("%v - doSomething: processed\n", time.Now())
return nil
}
}
Он ожидает ввода отмены или после задержки в 3
секунд возвращается к вызывающей стороне. Он принимает контекст для прослушивания.
handleSomething
func handleSomething(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
fmt.Printf("%v - handleRequestCtx: start\n", time.Now())
done := make(chan error)
go func() {
if err := doSomething(ctx); err != nil {
fmt.Printf("%v - handleRequestCtx: error %v\n", time.Now(), err)
done <- err
}
done <- nil
}()
select {
case <-ctx.Done():
fmt.Printf("%v - handleRequestCtx: cancelled\n", time.Now())
return
case err := <-done:
if err != nil {
fmt.Printf("%v - handleRequestCtx: error: %v\n", time.Now(), err)
w.WriteHeader(http.StatusInternalServerError)
return
}
fmt.Printf("%v - handleRequestCtx: processed\n", time.Now())
}
}
Здесь логика очень похожа на вашу. В выборке мы проверяем, является ли полученная ошибка nil
или нет, и на основании этого возвращаемся к правильному коду состояния HTTP вызывающей стороне. Если мы получаем ввод отмены, мы отменяем всю цепочку контекста.
TestHandler
func TestHandler(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/demo", handleSomething)
srv := http.Server{
Addr: ":8000",
Handler: r,
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
if err := srv.ListenAndServe(); err != nil {
fmt.Println(err.Error())
}
}()
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 1*time.Second) // request canceled
// ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // request processed
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8000/demo", nil)
client := http.Client{}
res, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Printf("res status code: %d\n", res.StatusCode)
}
srv.Shutdown(ctx)
wg.Wait()
}
Здесь мы запускаем HTTP-сервер и отправляем ему HTTP-запрос через http.Client
. Вы можете видеть, что есть два оператора для установки времени ожидания контекста. Если вы используете один с комментарием // request canceled
, все будет отменено, в противном случае, если вы используете другой, запрос будет обработан.
Я надеюсь, что это проясняет ваш вопрос!