dtcuv8044 2019-07-03 01:06
浏览 32
已采纳

在测试过程中同步测试服务器

Summary: I'm running into a race condition during testing where my server is not reliably ready to serve requests before making client requests against it. How can I block only until the listener is ready, and still maintain composable public APIs without requiring users to BYO net.Listener?

We see the following error as the goroutine that spins up our (blocking) server in the background isn't listening before we call client.Do(req) in the TestRun test function.

--- 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
  • I'm not using httptest.Server directly as I'm attempting to test the blocking & cancellation characteristics of my own server componenent.
  • I create an httptest.NewUnstartedServer, clone its *tls.Config into a new http.Server after starting it with StartTLS(), and then close it, before calling *AdmissionServer.Run(). This also has the benefit of giving me a *http.Client with the matching RootCAs configured.
  • Testing TLS is important here as the daemon this exposes lives in a TLS-only environment.
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.

For posterity, this is our composable Run function, that listens in a goroutine and then blocks on our cancellation & error channels in a 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)
        }
    }
}

Notes:

  • There is a (simple) constructor for an *AdmissionServer: I've left it out for brevity. The AdmissionServer is composable and accepts a *http.Server so that it can be plugged into existing applications easily.
  • The wrapped http.Server type that we create a listener from doesn't itself expose any way to tell if its listening; at best we can try to listen again and catch the error (e.g. port already bound to another listener), which does not seem robust as the net package doesn't expose a useful typed error for this.
  • 写回答

1条回答 默认 最新

  • dongmen1860 2019-07-03 08:31
    关注

    You can just attempt to connect to the server before starting the test suite, as part of the initialization process.

    For example, I usually have a function like this in my tests:

    // 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)
    }
    

    Then in my TestMain() I do:

    func TestMain(m *testing.M) {
        go startServer()
        waitForServer(serverPort)
    
        // run the suite
        os.Exit(m.Run())
    }
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 对于这个问题的代码运行
  • ¥50 三种调度算法报错 有实例
  • ¥15 关于#python#的问题,请各位专家解答!
  • ¥200 询问:python实现大地主题正反算的程序设计,有偿
  • ¥15 smptlib使用465端口发送邮件失败
  • ¥200 总是报错,能帮助用python实现程序实现高斯正反算吗?有偿
  • ¥15 对于squad数据集的基于bert模型的微调
  • ¥15 为什么我运行这个网络会出现以下报错?CRNN神经网络
  • ¥20 steam下载游戏占用内存
  • ¥15 CST保存项目时失败