mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-06 14:31:57 +02:00
A draining filer stayed in the master's lock ring until its process exited, while gRPC GracefulStop was already refusing new connections. S3 gateways and peer filers kept routing object-write locks and owner-routed writes to it for the whole graceful-stop window. On shutdown the filer now sends leave_lock_ring on its open KeepConnected stream. The master removes it from the lock ring only; it stays a cluster member so peers keep following its metadata log through the drain. Once the ring update without it arrives, the filer has already transferred its locks to the new owners, and it keeps serving through the prior-owner window (plus a second for peers that apply the update later) before gRPC and HTTP begin draining. The whole leave is bounded at 10s so slow lock transfers or a stuck stream cannot hold up the drain. A lone filer, or a master that ignores the message, falls back to the previous behavior.
199 lines
5.2 KiB
Go
199 lines
5.2 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() {}, 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() {}, 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")
|
|
}
|
|
}
|
|
|
|
func TestFilerShutdownLeavesLockRingBeforeDraining(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}))
|
|
t.Cleanup(server.Close)
|
|
httpClosing := make(chan struct{})
|
|
server.Config.RegisterOnShutdown(func() { close(httpClosing) })
|
|
|
|
leaving := make(chan struct{})
|
|
releaseLeave := make(chan struct{})
|
|
finishLeave := sync.OnceFunc(func() { close(releaseLeave) })
|
|
t.Cleanup(finishLeave)
|
|
grpcStopping := make(chan struct{})
|
|
shutdown := newFilerShutdown(func() {
|
|
close(leaving)
|
|
<-releaseLeave
|
|
}, func() { close(grpcStopping) }, func() {}, server.Config)
|
|
joined := make(chan struct{})
|
|
go func() { shutdown(); close(joined) }()
|
|
|
|
select {
|
|
case <-leaving:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("shutdown did not leave the lock ring")
|
|
}
|
|
select {
|
|
case <-grpcStopping:
|
|
t.Fatal("gRPC stopped accepting while the filer was still leaving the lock ring")
|
|
case <-httpClosing:
|
|
t.Fatal("HTTP stopped accepting while the filer was still leaving the lock ring")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
finishLeave()
|
|
<-joined
|
|
select {
|
|
case <-grpcStopping:
|
|
default:
|
|
t.Error("gRPC was not stopped after leaving the lock ring")
|
|
}
|
|
}
|