WebSocket 优雅关闭

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

websocket gracefully shutdown

问题

我有一个 WebSocket 服务器。我为它编写了一个测试,测试它是否能够优雅地关闭。我创建了5个连接,每个连接发送5个请求。过了一段时间后,开始关闭服务器。所有的25个请求必须得到满足。如果我关闭 exit 通道,那么测试就不能按照预期工作。

	time.AfterFunc(50*time.Millisecond, func() {
		close(exit)
		close(done)
	})

如果我只是调用 s.shutdown 函数,那么一切都正常。

	time.AfterFunc(50*time.Millisecond, func() {
		require.Nil(t, s.Shutdown())
		close(done)
	})

我的测试代码如下:

func TestServer_GracefulShutdown(t *testing.T) {
	done := make(chan struct{})
	exit := make(chan struct{})
	ctx := context.Background()

	finishedRequestCount := atomic.NewInt32(0)
	ln, err := net.Listen("tcp", "localhost:")
	require.Nil(t, err)

	handler := HandlerFunc(func(conn *websocket.Conn) {
		for {
			_, _, err := conn.ReadMessage()
			if err != nil {
				return
			}
			time.Sleep(100 * time.Millisecond)
			finishedRequestCount.Inc()
		}
	})
	s, err := makeServer(ctx, handler) // 创建服务器
	require.Nil(t, err)
	time.AfterFunc(50*time.Millisecond, func() {
		close(exit)
		close(done)
	})
	go func() {
		fmt.Printf("Running...")
		require.Nil(t, s.Run(ctx, exit, ln))
	}()
	for i := 0; i < connCount; i++ {
		go func() {
			err := clientRun(ln)
			require.Nil(t, err)
		}()
	}

	<-done

	assert.Equal(t, int32(totalCount), finishedRequestCount.Load())
}

我的 Run 函数如下:

func (s *Server) Run(ctx context.Context, exit <-chan struct{}, ln net.Listener) error {
	errs := make(chan error, 1)

	go func() {
		err := s.httpServer.Run(ctx, exit, ln)
		if err != nil {
			errs <- err
		}
	}()

	select {
	case <-ctx.Done():
		return s.Close()
	case <-exit:
		return s.Shutdown()
	case err := <-errs:
		return err
	}
}

我的 Shutdown 函数如下:

func (s *Server) Shutdown() error {
	err := s.httpServer.Shutdown() // 关闭所有连接的可能性
	s.wg.Wait()
	return err
}
英文:

I have a websocket server. I wrote a test for him that tests his ability to shutdown gracefully. 5 connections are created and each sends 5 requests. After a while, shutdown starts. All 25 requests must be fulfilled. If I close the exit channel, then the test does not work as it should.

	time.AfterFunc(50*time.Millisecond, func() {
		close(exit)
		close(done)
	})

And if I just call the s.shutdown function, then everything is ok.

	time.AfterFunc(50*time.Millisecond, func() {
		require.Nil(t, s.Shutdown())
		close(done)
	})

My test

func TestServer_GracefulShutdown(t *testing.T) {
	done := make(chan struct{})
	exit := make(chan struct{})
	ctx := context.Background()

	finishedRequestCount := atomic.NewInt32(0)
	ln, err := net.Listen(&quot;tcp&quot;, &quot;localhost:&quot;)
	require.Nil(t, err)

	handler := HandlerFunc(func(conn *websocket.Conn) {
		for {
			_, _, err := conn.ReadMessage()
			if err != nil {
				return
			}
			time.Sleep(100 * time.Millisecond)
			finishedRequestCount.Inc()
		}
	})
	s, err := makeServer(ctx, handler) // server create
	require.Nil(t, err)
	time.AfterFunc(50*time.Millisecond, func() {
		close(exit)
		close(done)
	})
	go func() {
		fmt.Printf(&quot;Running...&quot;)
		require.Nil(t, s.Run(ctx, exit, ln))
	}()
	for i := 0; i &lt; connCount; i++ {
		go func() {
			err := clientRun(ln)
			require.Nil(t, err)
		}()
	}

	&lt;-done

	assert.Equal(t, int32(totalCount), finishedRequestCount.Load())
}

My run func

func (s *Server) Run(ctx context.Context, exit &lt;-chan struct{}, ln net.Listener) error {
	errs := make(chan error, 1)

	go func() {
		err := s.httpServer.Run(ctx, exit, ln)
		if err != nil {
			errs &lt;- err
		}
	}()

	select {
	case &lt;-ctx.Done():
		return s.Close()
	case &lt;-exit:
		return s.Shutdown()
	case err := &lt;-errs:
		return err
	}
}

My shutdown

func (s *Server) Shutdown() error {
	err := s.httpServer.Shutdown() // we close the possibility to connect to any conn
	s.wg.Wait()
	return err
}

答案1

得分: 1

如果执行以下代码会发生什么情况?

close(exit)
close(done)

这两个通道几乎同时关闭。第一个通道触发了Shutdown函数,该函数等待优雅关闭。但是第二个通道触发了以下代码的评估:

assert.Equal(t, int32(totalCount), finishedRequestCount.Load())

这个断言在优雅关闭仍在运行或尚未开始时触发。


如果直接执行Shutdown函数,它将阻塞直到完成,然后才会开始close(done)断言。这就是为什么这样可以工作的原因:

require.Nil(t, s.Shutdown())
close(done)

您可以将close(done)移动到以下位置,以便在使用exit通道关闭时使测试工作:

go func() {
    fmt.Printf("Running...")
    require.Nil(t, s.Run(ctx, exit, ln))
    close(done)
}()

这样,在执行Shutdown函数后,done将被关闭。


如评论中所讨论的,我强烈建议使用上下文而不是通道来进行关闭。它们将关闭通道的复杂性隐藏起来。

英文:

What is happening if the following code is executed?

close(exit)
close(done)

Both channels are closed almost at the same time. The first triggers the Shutdown function which waits for a graceful shutdown. But the second triggers the evaluation of

assert.Equal(t, int32(totalCount), finishedRequestCount.Load())

It is triggered while the graceful shutdown is still running or hasn't even started yet.


If you execute the Shutdown function directly it will block until finished and only then close(done) will start the assertion. That is why this works:

require.Nil(t, s.Shutdown())
close(done)

You can move the close(done) to the following location to make the test work while using the exit channel to close:

go func() {
    fmt.Printf(&quot;Running...&quot;)
    require.Nil(t, s.Run(ctx, exit, ln))
    close(done)
}()

This way done will be closed after the Shutdown function was executed.


As discussed in the comments I strongly suggest to use contexts instead of channels to close. They have the complexity of close channels hidden away.

huangapple
  • 本文由 发表于 2021年9月30日 17:33:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/69389783.html
匿名

发表评论

匿名网友

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

确定