mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 13:00:45 +02:00
Serve can return when its listener closes while HTTP requests are still draining. The main path could then close the metadata store before those requests finish. Make signal, context, and Serve-exit paths join one shutdown sequence. Drain gRPC and HTTP concurrently with 15-second default limits, then close the store. Test both completion orders.
160 lines
4.0 KiB
Go
160 lines
4.0 KiB
Go
package command
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFilerShutdownJoinsServeDuringParallelDrain(t *testing.T) {
|
|
var servers []*http.Server
|
|
var releases []func()
|
|
var completed []chan struct{}
|
|
var listenersClosing []chan struct{}
|
|
for range 2 {
|
|
entered := make(chan struct{})
|
|
release := make(chan struct{})
|
|
finish := sync.OnceFunc(func() { close(release) })
|
|
done := make(chan struct{})
|
|
closing := make(chan struct{})
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
close(entered)
|
|
<-release
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
t.Cleanup(finish)
|
|
server.Config.RegisterOnShutdown(func() { close(closing) })
|
|
servers = append(servers, server.Config)
|
|
releases = append(releases, finish)
|
|
completed = append(completed, done)
|
|
listenersClosing = append(listenersClosing, closing)
|
|
go func() {
|
|
defer close(done)
|
|
response, err := server.Client().Get(server.URL)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
defer response.Body.Close()
|
|
io.Copy(io.Discard, response.Body)
|
|
if response.StatusCode != http.StatusNoContent {
|
|
t.Errorf("active request returned %s", response.Status)
|
|
}
|
|
}()
|
|
<-entered
|
|
}
|
|
|
|
grpcStarted := make(chan struct{})
|
|
grpcRelease := make(chan struct{})
|
|
releaseGrpc := sync.OnceFunc(func() { close(grpcRelease) })
|
|
t.Cleanup(releaseGrpc)
|
|
grpcStopped := make(chan struct{})
|
|
filerClosed := make(chan struct{})
|
|
var closes atomic.Int32
|
|
shutdown := newFilerShutdown(func() {
|
|
close(grpcStarted)
|
|
<-grpcRelease
|
|
close(grpcStopped)
|
|
}, func() {
|
|
closes.Add(1)
|
|
close(filerClosed)
|
|
}, servers...)
|
|
joined := make(chan struct{})
|
|
go func() { shutdown(); close(joined) }()
|
|
deadline := time.After(5 * time.Second)
|
|
select {
|
|
case <-grpcStarted:
|
|
case <-deadline:
|
|
t.Fatal("gRPC shutdown did not start")
|
|
}
|
|
for _, closing := range listenersClosing {
|
|
select {
|
|
case <-closing:
|
|
case <-deadline:
|
|
t.Fatal("HTTP shutdown did not start while gRPC was draining")
|
|
}
|
|
}
|
|
// Model the main Serve path joining shutdown as soon as listeners close.
|
|
serveReturned := make(chan struct{})
|
|
go func() { shutdown(); close(serveReturned) }()
|
|
|
|
releases[0]()
|
|
<-completed[0]
|
|
select {
|
|
case <-filerClosed:
|
|
t.Error("filer closed while another server still had an active request")
|
|
default:
|
|
}
|
|
select {
|
|
case <-serveReturned:
|
|
t.Error("Serve exit path returned before request draining completed")
|
|
default:
|
|
}
|
|
releases[1]()
|
|
<-completed[1]
|
|
select {
|
|
case <-filerClosed:
|
|
t.Error("filer closed while gRPC was still draining")
|
|
default:
|
|
}
|
|
releaseGrpc()
|
|
<-grpcStopped
|
|
<-joined
|
|
<-serveReturned
|
|
if closes.Load() != 1 {
|
|
t.Errorf("filer closed %d times, want once", closes.Load())
|
|
}
|
|
}
|
|
|
|
func TestFilerShutdownWaitsForHTTPAfterGrpcStops(t *testing.T) {
|
|
entered := make(chan struct{})
|
|
release := make(chan struct{})
|
|
finish := sync.OnceFunc(func() { close(release) })
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
close(entered)
|
|
<-release
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
t.Cleanup(finish)
|
|
requestDone := make(chan struct{})
|
|
go func() {
|
|
defer close(requestDone)
|
|
response, err := server.Client().Get(server.URL)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
response.Body.Close()
|
|
}()
|
|
<-entered
|
|
|
|
httpClosing := make(chan struct{})
|
|
server.Config.RegisterOnShutdown(func() { close(httpClosing) })
|
|
grpcStopped := make(chan struct{})
|
|
filerClosed := make(chan struct{})
|
|
shutdown := newFilerShutdown(func() { close(grpcStopped) }, func() { close(filerClosed) }, server.Config)
|
|
joined := make(chan struct{})
|
|
go func() { shutdown(); close(joined) }()
|
|
<-httpClosing
|
|
<-grpcStopped
|
|
select {
|
|
case <-filerClosed:
|
|
t.Error("filer closed while HTTP request was active")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
finish()
|
|
<-requestDone
|
|
<-joined
|
|
select {
|
|
case <-filerClosed:
|
|
default:
|
|
t.Error("filer was not closed after HTTP request completed")
|
|
}
|
|
}
|