Как прервать обработчик HTTP? - PullRequest
0 голосов
/ 30 октября 2019

Скажем, у меня есть обработчик http, подобный этому:

func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
        // run code that takes a long time here
        // Executing dd command with cmd.Exec..., etc.

})

Есть ли способ, которым я могу прервать эту функцию, если пользователь обновляет страницу или убивает запрос другим способом, не выполняя последующий код и какя бы сделал это?

Я пытался сделать это:

notify := r.Context().Done()
go func() {
    <-notify
     println("Client closed the connection")
     s.downloadCleanup()
     return
}()

, но код после каждого прерывания все равно запускается.

Ответы [ 2 ]

8 голосов
/ 30 октября 2019

Нет способа принудительно вырвать goroutine из любого кода external в эту goroutine.

Следовательно, единственный способ фактически прервать обработку - это периодически проверять, пропал ли клиент (или есть ли другой сигнал для остановки обработки).

По сути, это будет означать структурирование вашего обработчика примерно так:

func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")

    done := r.Context().Done()

    // Check wheteher we're done

    // do some small piece of stuff

    // check whether we're done

    // do another small piece of stuff

    // …rinse, repeat
})

Теперь способ проверить, было ли что-то записано в канал,но без блокировки операция должна использовать идиому «выбрать по умолчанию»:

select {
    case <- done:
        // We're done
    default:
}

Этот statemept выполняет код в блоке "// We done", если и только если записано doneдля или был закрыт (что имеет место с контекстами), а в остальном выполняется пустой блок в ветви default.

Таким образом, мы можем рефакторировать это как что-то вроде

func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")

    done := r.Context().Done()
    closed := func () bool {
        select {
            case <- done:
                return true
            default:
                return false
        }
    }

    if closed() {
        return
    }

    // do some small piece of stuff

    if closed() {
        return
    }

    // do another small piece of stuff

    // …rinse, repeat
})

Остановка внешнего процесса, запущенного в обработчике HTTP

Для обращения к комментарию ОП…

Тип os/exec.Cmd имеет поле Process, котороеимеет тип os.Process, и этот тип поддерживает метод Kill, который принудительно останавливает запущенный процесс.

Единственная проблема состоит в том, что exec.Cmd.Run блокирует, пока процесс не завершится, поэтому программа, выполняющая егоне может выполнить другой код, и если в обработчике HTTP вызывается exec.Cmd.Run, отменить его невозможно.

Как лучше справиться с запуском программы в такой асинхронной манере, во многом зависит от того, как сам процессорганизовано, но я бы катился так:

  1. В обработчике подготовьте процесс и затем запустите его, используя exec.Cmd.Start (в отличие от Run).

    Проверьте значение ошибки Start, возвращенное: если это nil, процесс удалось запустить ОК. В противном случае каким-либо образом сообщите об ошибке клиенту и выйдите из обработчика.

    Как только процесс, как известно, начался, значение exec.Cmd содержит некоторые из его полей, заполненные информацией, связанной с процессом;Особый интерес представляет поле Process, которое имеет тип os.Process: этот тип имеет метод Kill, который можно использовать для принудительного останова процесса.

  2. Началои передайте ему значение exec.Cmd и канал какого-либо подходящего типа (см. ниже).

    Эта программа должна вызвать Wait для нее, и как только она вернется, она должна сообщить об этом факте исходящемупереработка этого каналаактивность процесса.

    После отправки данных эта программа завершается.

  3. Основная программа (выполняющая обработчик) должна просто вызвать exec.Cmd.Process.Kill, когда обнаружитобработчик должен завершиться.

    При уничтожении процесса в конечном итоге разблокируется процедура, выполняющая Wait для того же значения exec.Cmd, когда завершается процесс.

    После уничтожения процесса, программа-обработчик ожидает на канале ответа от программы, наблюдающего за процессом. Обработчик что-то делает с этими данными (может быть, регистрирует их или что-то еще) и завершает работу.

2 голосов
/ 30 октября 2019

Вы должны отменить выполнение программы изнутри, поэтому для длительной задачи расчета вы можете предоставить контрольные точки, чтобы остановить и проверить отмену:

Вот проверенный код для сервера, который имеет, например, длинный расчетЗадача и контрольные точки для отмены:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc(`/`, func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()

        log.Println("wait a couple of seconds ...")
        for i := 0; i < 10; i++ { // long calculation
            select {
            case <-ctx.Done():
                log.Println("Client closed the connection:", ctx.Err())
                return
            default:
                fmt.Print(".")
                time.Sleep(200 * time.Millisecond) // long calculation
            }
        }
        io.WriteString(w, `Hi`)
        log.Println("Done.")
    })
    log.Println(http.ListenAndServe(":8081", nil))
}

Вот код клиента, время ожидания которого:

package main

import (
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

func main() {
    log.Println("HTTP GET")
    client := &http.Client{
        Timeout: 1 * time.Second,
    }
    r, err := client.Get(`http://127.0.0.1:8081/`)
    if err != nil {
        log.Fatal(err)
    }
    defer r.Body.Close()
    bs, err := ioutil.ReadAll(r.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("HTTP Done.")
    log.Println(string(bs))
}

Вы можете использовать обычный браузер проверить, нет ли отмены, или закрыть, обновить, отключить или ... для отмены.

...