Сводка : я сталкиваюсь с состоянием гонки во время тестирования, когда мой сервер не готов к работе с запросами, прежде чем отправлять запросы клиентов на него. Как я могу блокировать только до тех пор, пока слушатель не будет готов, и при этом поддерживать составные публичные API, не требуя от пользователей BYO net.Listener
?
Мы видим следующую ошибку, поскольку программа, которая раскручивает наш (блокирующий) сервер в фоновом режиме, не слушает, прежде чем мы вызовем client.Do(req)
в тестовой функции TestRun
.
--- FAIL: TestRun/Server_accepts_HTTP_requests (0.00s)
/home/matt/repos/admission-control/server_test.go:64: failed to make a request: Get https://127.0.0.1:37877: dial tcp 127.0.0.1:37877: connect: connection refused
- Я не использую
httptest.Server
напрямую, поскольку пытаюсь проверить характеристики блокировки и отмены моего собственного серверного компонента.
- Я создаю
httptest.NewUnstartedServer
, клонирую его *tls.Config
в новый http.Server
после запуска его с StartTLS()
, а затем закрываю его перед вызовом *AdmissionServer.Run()
. Это также дает мне преимущество: *http.Client
с настроенными соответствующими RootCA.
- Тестирование TLS здесь важно, так как демон, который это выставляет, живет в среде только TLS.
func newTestServer(ctx context.Context, t *testing.T) *httptest.Server {
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
})
testSrv := httptest.NewUnstartedServer(testHandler)
admissionServer, err := NewServer(nil, &noopLogger{})
if err != nil {
t.Fatalf("admission server creation failed: %s", err)
return nil
}
// We start the test server, copy its config out, and close it down so we can
// start our own server. This is because httptest.Server only generates a
// self-signed TLS config after starting it.
testSrv.StartTLS()
admissionServer.srv = &http.Server{
Addr: testSrv.Listener.Addr().String(),
Handler: testHandler,
TLSConfig: testSrv.TLS.Clone(),
}
testSrv.Close()
// We need a better synchronization primitive here that doesn't block
// but allows the underlying listener to be ready before
// serving client requests.
go func() {
if err := admissionServer.Run(ctx); err != nil {
t.Fatalf("server returned unexpectedly: %s", err)
}
}()
return testSrv
}
// Test that we can start a minimal AdmissionServer and handle a request.
func TestRun(t *testing.T) {
testSrv := newTestServer(context.TODO(), t)
t.Run("Server accepts HTTP requests", func(t *testing.T) {
client := testSrv.Client()
req, err := http.NewRequest(http.MethodGet, testSrv.URL, nil)
if err != nil {
t.Fatalf("request creation failed: %s", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("failed to make a request: %s", err)
}
// Later sub-tests will test cancellation propagation, signal handling, etc.
Для потомков это наша составная функция Run
, которая слушает в программе и затем блокирует наши каналы отмены и ошибок в for-select
:
type AdmissionServer struct {
srv *http.Server
logger log.Logger
GracePeriod time.Duration
}
func (as *AdmissionServer) Run(ctx context.Context) error {
sigChan := make(chan os.Signal, 1)
defer close(sigChan)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// run in goroutine
errs := make(chan error)
defer close(errs)
go func() {
as.logger.Log(
"msg", fmt.Sprintf("admission control listening on '%s'", as.srv.Addr),
)
if err := as.srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
errs <- err
as.logger.Log(
"err", err.Error(),
"msg", "the server exited",
)
return
}
return
}()
// Block indefinitely until we receive an interrupt, cancellation or error
// signal.
for {
select {
case sig := <-sigChan:
as.logger.Log(
"msg", fmt.Sprintf("signal received: %s", sig),
)
return as.shutdown(ctx, as.GracePeriod)
case err := <-errs:
as.logger.Log(
"msg", fmt.Sprintf("listener error: %s", err),
)
// We don't need to explictly call shutdown here, as
// *http.Server.ListenAndServe closes the listener when returning an error.
return err
case <-ctx.Done():
as.logger.Log(
"msg", fmt.Sprintf("cancellation received: %s", ctx.Err()),
)
return as.shutdown(ctx, as.GracePeriod)
}
}
}
Примечания:
- Существует (простой) конструктор для
*AdmissionServer
: я оставил его для краткости. AdmissionServer
является составным и принимает *http.Server
, чтобы его можно было легко подключить к существующим приложениям.
- Обернутый
http.Server
тип, из которого мы создаем слушателя, сам по себе не предоставляет никакого способа определить, прослушивается ли он; в лучшем случае мы можем попытаться прослушать снова и поймать ошибку (например, порт уже связан с другим слушателем), который не выглядит надежным, поскольку пакет net
не предоставляет полезную типизированную ошибку для этого.