При написании обработчика http должны ли мы прослушивать отмену контекста запроса?

Предположим, что я пишу обработчик 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.


1
76
1

Ответ:

Решено

Я только что немного отрефакторил предоставленное решение, и теперь оно должно работать. Позвольте мне провести вас через соответствующие изменения.

Функция 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, все будет отменено, в противном случае, если вы используете другой, запрос будет обработан.
Я надеюсь, что это проясняет ваш вопрос!