Какой контекст передать, чтобы просто подождать, пока что-то не будет завершено?

#go

#Вперед

Вопрос:

По разным причинам я чувствую, что http.ListenAndServe это не соответствует моим потребностям.

Мне нужно было иметь возможность определять связанный адрес и порт (т. Е. При использовании ":0" ), Поэтому я ввел net.Listener , прочитал listener.Addr() , а затем передал http.Serve(listener, nil) .

Затем мне нужно было иметь возможность запускать два HTTP-сервера с разными обработчиками URL, поэтому я ввел an http.NewServeMux() , добавил необходимые mux.HandleFunc("/path", fn) обработчики и передал как http.Serve(listener, mux) .

Затем мне нужно было иметь возможность чисто останавливать эти серверы и отключать любые соединения независимо от самой основной программы, так что теперь я представил amp;http.Server{Handler: mux} , что я могу go func() { server.Serve(listener) }() .

Теоретически я могу остановить это, позвонив server.Shutdown(ctx) , но теперь ни один из доступных контекстов, import "context" похоже, не предлагает того, что я хочу. Я хочу иметь возможность подождать, пока завершится чистое завершение работы, а затем продолжить работу с моим кодом.

Я понимаю, что я должен быть в состоянии <- ctx.Done() достичь этого, но я пробовал оба context.Background() , context.TODO() и ни один из них, похоже, не «срабатывает» ctx.Done() , и в итоге я блокирую навсегда. Другие context варианты, похоже, зависят от времени.

Если я чего-то не жду или не передаю nil , server.Shutdown(ctx) кажется, что все заканчивается слишком быстро, и я вижу, что на самом деле ничего не закрыто ( runtime.Numgoroutine() != 1 )

Я могу time.Sleep(duration) для некоторой произвольной продолжительности, но я не хочу произвольной продолжительности. Я хочу знать, что server.Shutdown все завершилось чисто.

 package main

import (
    "fmt"
    "net"
    "net/http"
    "runtime"
    "time"
)

func main() {
    var err error

    listener, err := net.Listen("tcp", "localhost:0")
    fmt.Printf("Listening on http://%vn", listener.Addr())

    mux := http.NewServeMux()
    mux.HandleFunc("/", handleIndex)

    stop, err := startHTTPServer(listener, mux)

    d, _ := time.ParseDuration("5s")

    time.Sleep(d)   // delay here just for example of "long-running" server
    close(stop)     // closing the channel returned by my helper should trigger shutdown
    time.Sleep(d)   // if this delay is here, I see the "Stopped" message

    if err != nil {
        panic(err)
    }

    fmt.Printf("End of program, active goroutines: %v", runtime.NumGoroutine())
}

// startHTTPServer is a helper function to start a server and return a channel that can trigger shutdown
func startHTTPServer(listener net.Listener, handler http.Handler) (stop chan struct{}, err error) {
    stop = make(chan struct{})
    server := amp;http.Server{Handler: handler}

    go func() {
        fmt.Println("Starting server...")
        err = server.Serve(listener)
    }()
    go func() {
        select {
        case <-stop:
            fmt.Println("Stop channel closed; stopping server...")
            err = server.Shutdown(nil)    // what is passed instead of nil here?
            fmt.Println("Stopped.")
            return
        }
    }()
    return
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello world")
}
  

Я пробовал оба context.Background() и context.TODO() . Я пробовал new(context.Context) , но это бросило SIGSEGV . Я пробовал nil , и это вообще не ждет.

Я попытался добавить a sync.WaitGroup и вызвать wg.Wait() вместо второго time.Sleep(d) , но мне все равно нужно дождаться server.Shutdown() завершения перед вызовом wg.Done() defer wg.Done() назвал его слишком рано).

Я чувствую, что с контекстами, группами ожидания и т. Д. Я Просто добавляю в код cruft, не понимая, зачем это нужно.

Каков правильный, чистый, идиоматический способ дождаться server.Shutdown завершения?

Ответ №1:

Чтобы дождаться завершения работы в основной подпрограмме, вызовите Shutdown из этой подпрограммы. Устраните канал и дополнительную подпрограмму.

 listener, _ := net.Listen("tcp", "localhost:0")
fmt.Printf("Listening on http://%vn", listener.Addr())

mux := http.NewServeMux()
mux.HandleFunc("/", handleIndex)

server := amp;http.Server{Handler: mux}
go func() {
    fmt.Println("Starting server...")
    err := server.Serve(listener)
    if err != nil amp;amp; err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

time.Sleep(5 * time.Second) // delay here just for example of "long-running" server

// Shutdown and wait for server to complete.
server.Shutdown(context.Background())
  

Если вы хотите ограничить время ожидания завершения работы сервера, context.Background() замените контекст, созданный с указанием крайнего срока.

Комментарии:

1. Анонимная подпрограмма должна обрабатывать http. Ошибка Serverclosed изящно для чистого завершения работы.

2. Для чего-то без тайм-аута context.Background() правильно, нет context.TODO() . Согласно документам: «Код должен использовать контекст. ЗАДАЧА, когда неясно, какой контекст использовать, или он еще недоступен (поскольку окружающая функция еще не была расширена для принятия параметра контекста). »

3. @Peter I обновил ответ, чтобы аккуратно обработать ErrServerClosed. Остановка канала и получение подпрограммы на этом канале не требуется.

Ответ №2:

Теоретически я могу остановить это, вызвав server .Завершение работы (ctx), но теперь ни один из доступных контекстов в import «context», похоже, не предлагает того, что я хочу. Я хочу иметь возможность подождать, пока завершится чистое завершение работы, а затем продолжить работу с моим кодом.

Это означает, что вы должны передать контекст, у которого нет тайм-аута. Ни context.Background() один, ни context.TODO() тайм-аут и, следовательно, не подходят (действительно, см. Ниже). Используете ли вы тот или иной вариант, зависит от того, планируете ли вы тайм-аут завершения работы (вы должны предотвратить то, чтобы rough client не мешал вам завершить работу вашего сервера) или нет.

Я понимаю, что я должен иметь возможность <- ctx.Done() для достижения этой цели, но я пробовал как context.Background(), так и context .TODO() и ни один из них, похоже, не «запускает» ctx.Done(), и я в конечном итоге блокирую навсегда. Другие параметры контекста, похоже, зависят от времени.

Ну, это неправильно. Ни context.Background() nor context.TODO() не закроют их Done , поскольку у них нет тайм-аута. Но нет необходимости ждать завершения: server.Shutdown это обычная функция, которая возвращается после того, как сервер действительно выключен должным образом (это то, что вы, кажется, хотите) или время ожидания контекста истекло. В любом случае server.Shutdown просто возвращает.

Простой

 server.Shutdown(context.TODO())
  

это то, что вы хотите. (На данный момент, в долгосрочной перспективе: передайте контекст, время ожидания которого истекает через долгое, но конечное время.)

Но ваш код в любом случае выглядит подозрительно: ваш функциональный startHTTPServer неправильно обрабатывает ошибки: не ошибка при запуске, а не ошибка при остановке. Если ваш сервер не запустился, вы не можете его остановить, и ваш код просто проглатывает ошибку. Это также пикантно при ошибке. Ваши проблемы, вероятно, не связаны с контекстом, переданным на сервер.Завершение работы, но откуда-то еще.

Проблема в том, что ваш код не ожидает сервер.Завершение работы для возврата, поскольку эта функция запускается в подпрограмме без какой-либо синхронизации с вызывающим startHTTPServer: не делайте этого. Действительно: контекст сервера.Завершение работы не проблема.

Ниже приведен непроверенный код, который немного устраняет проблемы. Он не готов к производству, как вы можете видеть при всех настроениях.

 // startHTTPServer is a helper function to start a server and returns
// a channel to stop the server and a channel reporting errors during
// starting/stopping the server or nil if the server was shut down
// properly
func startHTTPServer(listener net.Listener, handler http.Handler) (stop chan bool, problems chan error) {
    stop, problems = make(chan bool), make(chan error)
    server := amp;http.Server{Handler: handler} // TODO: set timeouts

    go func() {
        fmt.Println("Starting server...")
        err := server.Serve(listener)
        if err != http.ErrServerClosed {
            problems <- err // TODO: tag/classify as startup error
        }
    }()
    go func() {
        select {
        case <-stop:
            fmt.Println("Stop channel closed; stopping server...")
            err := server.Shutdown(context.TODO())
            fmt.Println("Stopped.")
            problems <- err // TODO: tag/classify as shutdown error
        case e := <-problems:
            problems <- e  // resend, this error  is not for us
            return // stop waiting for stop as server did not start anyway.
        }
    }()
    return stop, problems
  

}

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

Комментарии:

1. Спасибо за это — я уверен, что вы правы. Но в этом случае проблема в том, что я действительно не понимаю, куда я должен поместить синхронизацию и как я должен ее вызывать. Можете ли вы предоставить исправленную startHTTPServer версию, которая демонстрирует вашу позицию? Я уверен, что есть много ошибок, которые я не обрабатываю, но, похоже, я не могу найти способ изящно обработать их и получить ListenAndServe эквивалент, который возвращает что-то неопределенно закрываемое, сохраняя при этом простую подпись вызова.

2. Для чего-то без тайм-аута context.Background() правильно, нет context.TODO() . Согласно документам: «Код должен использовать контекст. ЗАДАЧА, когда неясно, какой контекст использовать, или он еще недоступен (поскольку окружающая функция еще не была расширена для принятия параметра контекста). »

3. @Adrian Моим аргументом было: вам, вероятно, не следует использовать контекст, у которого нет тайм-аута, особенно если на вашем сервере нет тайм-аутов. Я предложил TODO, потому что считаю, что бесконечное ожидание опасно : следует добавить — возможно, очень длинный, но конечный — тайм-аут.