http.Server的Serve方法在立即调用Shutdown时挂起

huangapple go评论132阅读模式
英文:

http.Server Serve method hangs when calling Shutdown immediately

问题

我在Go的http.Server中遇到了一个问题,我将其嵌入在一个结构体中,该结构体应该控制服务器的启动和关闭。该结构体的定义如下:

type HTTPListen struct {
    Consumers []pipeline.Consumer
    Cfg       HTTPListenConfig
    Srv       *http.Server
    Logger    log.Logger
    wg        *sync.WaitGroup
    mu        sync.Mutex
    state     State
}

问题是,在我的测试代码中,我调用了结构体的Start()方法(该方法在http.Server上运行Serve()方法),检查了一些变量,然后调用Stop()方法,该方法关闭服务器,然后等待http.Server退出(从Serve()方法返回err)。

现在,由于某种原因,当我尝试在启动后立即关闭服务器时,Serve()方法似乎在WaitGroup.Wait()上挂起。当我添加一个短暂的延迟(尝试了100毫秒),或者在运行带有竞争检测器的测试时,它就正常工作。

不确定是否重要,但在调用Serve()Shutdown()之间没有传入的请求。

编辑:[最小示例的Playground链接][1]。如果注释掉time.Sleep调用,程序将挂起。

下面是这两个方法的相关代码:

func (h *HTTPListen) Start() error {

    h.Logger.Log("msg", "starting HTTPListen input")

    addr := h.Cfg.ListenAddr
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        h.Logger.Log("msg", "failed to create listener on tcp/"+addr+": "+err.Error())
        h.setState(StateFailed)
        return err
    }

    h.wg.Add(1)
    go func() {

        defer h.wg.Done()
        err := h.Srv.Serve(ln)
        h.Logger.Log("msg", "HTTP server stopped: "+err.Error())

    }()

    h.setState(StateStarted)
    h.Logger.Log("msg", "HTTPListen input started")

    return nil

}

Stop方法

```go
func (h *HTTPListen) Stop() error {

    h.Logger.Log("msg", "stopping HTTPListen input")

    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    defer cancel()

    if err := h.Srv.Shutdown(ctx); err != nil {
        h.Logger.Log("msg", "HTTP server shutdown deadline expired")
    }

    h.wg.Wait()
    
    h.setState(StateStopped)
    h.Logger.Log("msg", "HTTPListen input stopped")

    return nil
}

运行日志输出:

kwz@cyclone ~/s/stblogd> go test -v ./pkg/pipeline/input/ -run TestHTTPListen_StartStop
=== RUN   TestHTTPListen_StartStop
msg="starting HTTPListen input"
msg="HTTPListen input started"
msg="stopping HTTPListen input"
... 无限期挂起

使用竞争检测器运行测试时的日志输出:

```shell
kwz@cyclone ~/s/stblogd> go test -race -v ./pkg/pipeline/input/ -run TestHTTPListen_StartStop
=== RUN   TestHTTPListen_StartStop
msg="starting HTTPListen input"
msg="HTTPListen input started"
msg="stopping HTTPListen input"
msg="HTTP server stopped: http: Server closed"
msg="HTTPListen input stopped"
--- PASS: TestHTTPListen_StartStop (0.00s)
PASS
ok      stblogd/pkg/pipeline/input      1.007s

我倾向于在测试中加入一个短暂的延迟,然后就可以了结了,但我想知道为什么会出现这种情况。


  [1]: https://play.golang.org/p/s1jCfE44c3


<details>
<summary>英文:</summary>

I have an issue with Go&#39;s http.Server, which I&#39;m embedding in a struct that is supposed to control the server startup and shutdown. The struct looks like this:

    type HTTPListen struct {
	    Consumers []pipeline.Consumer
	    Cfg       HTTPListenConfig
	    Srv       *http.Server
    	Logger    log.Logger
    	wg        *sync.WaitGroup
    	mu        sync.Mutex
    	state     State
    }

The issue is that in my test code, I call my struct&#39;s `Start()` method (which in turn runs the `Serve()` method on the http.Server), check a few vars, and then call `Stop()`, whitch `Shutdown()`s the server and then waits for the http.Server to exit (return `err` from the `Serve()` method).

Now, for some reason, the `Serve()` method seems to just hang on the `WaitGroup.Wait()`, when I try to shutdown the server immediately after starting. When I add a short pause (tried 100ms), or when running the tests with the race detector, It works just fine.

Not sure if it matters, but there are no incoming requests between calling `Serve()` and `Shutdown()`.

**EDIT:** [link to a playground minimal example][1]. If you comment out the `time.Sleep` call, the program hangs.

Here is the relevant code for the two methods:

    func (h *HTTPListen) Start() error {
    
	    h.Logger.Log(&quot;msg&quot;, &quot;starting HTTPListen input&quot;)
    
	    addr := h.Cfg.ListenAddr
	    ln, err := net.Listen(&quot;tcp&quot;, addr)
	    if err != nil {
		    h.Logger.Log(&quot;msg&quot;, &quot;failed to create listener on tcp/&quot;+addr+&quot;: &quot;+err.Error())
		    h.setState(StateFailed)
		    return err
	    }
    
	    h.wg.Add(1)
	    go func() {
    
		    defer h.wg.Done()
		    err := h.Srv.Serve(ln)
		    h.Logger.Log(&quot;msg&quot;, &quot;HTTP server stopped: &quot;+err.Error())
    
	    }()
    
	    h.setState(StateStarted)
	    h.Logger.Log(&quot;msg&quot;, &quot;HTTPListen input started&quot;)
    
	    return nil
    
    }

Stop method:

    func (h *HTTPListen) Stop() error {
    
	    h.Logger.Log(&quot;msg&quot;, &quot;stopping HTTPListen input&quot;)
    
	    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	    defer cancel()
    
	    if err := h.Srv.Shutdown(ctx); err != nil {
		    h.Logger.Log(&quot;msg&quot;, &quot;HTTP server shutdown deadline expired&quot;)
	    }
    
	    h.wg.Wait()
    	
	    h.setState(StateStopped)
	    h.Logger.Log(&quot;msg&quot;, &quot;HTTPListen input stopped&quot;)
    
	    return nil
    }

Log output:

    kwz@cyclone ~/s/stblogd&gt; go test -v ./pkg/pipeline/input/ -run TestHTTPListen_StartStop
    === RUN   TestHTTPListen_StartStop
    msg=&quot;starting HTTPListen input&quot;
    msg=&quot;HTTPListen input started&quot;
    msg=&quot;stopping HTTPListen input&quot;
    ... hangs indefinitely

Log output when running tests with the race detector:

    kwz@cyclone ~/s/stblogd&gt; go test -race -v ./pkg/pipeline/input/ -run TestHTTPListen_StartStop
    === RUN   TestHTTPListen_StartStop
    msg=&quot;starting HTTPListen input&quot;
    msg=&quot;HTTPListen input started&quot;
    msg=&quot;stopping HTTPListen input&quot;
    msg=&quot;HTTP server stopped: http: Server closed&quot;
    msg=&quot;HTTPListen input stopped&quot;
    --- PASS: TestHTTPListen_StartStop (0.00s)
    PASS
    ok      stblogd/pkg/pipeline/input      1.007s

I&#39;m tempted to just slap a short delay on the test and call it a day, but I would like to know why it behaves like this.


  [1]: https://play.golang.org/p/s1jCfE44c3

</details>


# 答案1
**得分**: 2

这是一个已知的问题,可以参考这个帖子:

https://github.com/golang/go/issues/20239

希望他们能尽快修复,但目前似乎在你的测试中添加一个短暂的延迟是最简单的解决方案——在实际使用中可能不会经常出现这个问题,因为你不会在启动后很快触发关闭。

<details>
<summary>英文:</summary>

This is a known issue, see this thread:

https://github.com/golang/go/issues/20239

Hopefully they will fix it soon but for now it sounds like adding a short delay in your test is the simplest solution - it probably doesn&#39;t come up in real world use much because you won&#39;t trigger a shutdown so soon after starting. 

</details>



huangapple
  • 本文由 发表于 2017年8月18日 16:18:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/45751869.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定