【问题标题】:Synchronizing a Test Server During Tests在测试期间同步测试服务器
【发布时间】: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


【解决方案1】:

您可以在启动测试套件之前尝试连接到服务器,作为初始化过程的一部分。

例如,我的测试中通常有这样的函数:

// waitForServer attempts to establish a TCP connection to localhost:<port>
// in a given amount of time. It returns upon a successful connection; 
// ptherwise exits with an error.
func waitForServer(port string) {
    backoff := 50 * time.Millisecond

    for i := 0; i < 10; i++ {
        conn, err := net.DialTimeout("tcp", ":"+port, 1*time.Second)
        if err != nil {
            time.Sleep(backoff)
            continue
        }
        err = conn.Close()
        if err != nil {
            log.Fatal(err)
        }
        return
    }
    log.Fatalf("Server on port %s not up after 10 attempts", port)
}

然后在我的TestMain() 我做:

func TestMain(m *testing.M) {
    go startServer()
    waitForServer(serverPort)

    // run the suite
    os.Exit(m.Run())
}

【讨论】:

  • 这就是我最终采用的方法:我正在努力避免非确定性睡眠(例如,睡眠 5 * time.Second),但我实现了一个简单的等待和指数回退在返回可用的测试服务器类型之前。谢谢!
猜你喜欢
  • 2021-11-04
  • 1970-01-01
  • 2014-01-29
  • 1970-01-01
  • 2019-02-07
  • 2011-01-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多