【发布时间】:2019-07-03 16:38:09
【问题描述】:
总结:我在测试期间遇到了争用情况,我的服务器在发出客户端请求之前无法可靠地为请求提供服务。如何在侦听器准备好之前进行阻塞,并且仍然维护可组合的公共 API,而不需要用户 BYO net.Listener?
我们看到以下错误,因为在 TestRun 测试函数中调用 client.Do(req) 之前,在后台启动(阻塞)服务器的 goroutine 没有侦听。
--- 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,在使用StartTLS()启动它之后将其*tls.Config克隆到一个新的http.Server中,然后在调用*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 函数,它在 goroutine 中侦听,然后在 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包没有为此公开有用的类型错误。
【问题讨论】:
-
BYO 监听器或
for { c, err := dial(addr); if err != nil { c.Close(); break; }; time.Sleep(time.Second/10) } -
我制作了一个实用程序来帮助轮询服务(进程间)以检测它们何时准备就绪github.com/dm03514/wait-for,这类似于 Agis 建议的解决方案
标签: go