mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-12 01:07:35 +02:00
* fix(filer): end metadata subscriptions before gRPC GracefulStop on shutdown GracefulStop waits for every open stream. The filer's own MetaAggregator subscription and, under -s3, the S3 gateway's IAM subscription live in the same process and only end when the filer shuts down, which happens after GracefulStop - so every shutdown sat out the full 15s timeout. StopSubscriptions ends them (and any started later) right after leaving the lock ring; the handlers derive their context from the stream and this signal. Measured on weed server -s3: stop 25.6s -> 10.7s, and 0.6s with -volume.preStopSeconds=0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTq6bDpgdQfqQagQsUUvdw * fix(filer): end stopped subscriptions with Unavailable, not cleanly A subscription that StopSubscriptions cut short returned nil. The client reads that as io.EOF, "caught up, done", and util.RetryUntil - which the S3 gateway and mount follow with - stops on nil. A separate S3 gateway therefore never resubscribed after its filer restarted (measured: 0 subscriptions in 25s after the restart; upstream master and this fix: it is back within a second). endOfSubscription now turns that clean end into codes.Unavailable, which is what a dropped connection looks like, so followers reconnect. Errors pass through, and so does the end of a stream the client closed. A subscriber that has stopped reading is still left to GracefulStop's timeout: its handler sits in stream.Send, which only the end of the stream releases, and grpc-go does not allow Send after the handler returns. Documented at StopSubscriptions. Shutdown under weed server -s3 is unchanged at 10.7s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTq6bDpgdQfqQagQsUUvdw * fix(filer): interrupt in-flight subscription reads and end them Unavailable StopSubscriptions only reached the wait points: a handler replaying the ring or the persisted log kept streaming until the pass ended, and a pass cut short at a ctx.Err() boundary (the replay semaphore, chunk ref collection, ref sends) returned a wrapped context.Canceled, which reads as codes.Canceled on the wire - a code transient-error classifiers treat as non-retryable. eachLogEntryFn and sendRefsBatched now check the subscription context per entry/batch, and endOfSubscription reports a context.Canceled cut short by the stop signal as codes.Unavailable, same as a clean end it interrupted. * filer: honor the subscription context in the pipelined sender A subscriber that stops reading wedges sendLoop in stream.Send; Send and Close then blocked past StopSubscriptions, so GracefulStop still waited out its timeout. Both now return when the subscription context ends, letting the handler return and gRPC tear the stream down. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: stop sendLoop from starting sends after the subscription ends With Send and Close released by the subscription context, sendLoop could still issue a stream.Send after the handler returned. Gate every send on the context so at most the in-flight Send overlaps teardown - and the stream's own teardown is what unblocks it. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: assert sendLoop exits in the cancel test Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Chris Lu <chris.lu@gmail.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
227 lines
6.1 KiB
Go
227 lines
6.1 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() {}, 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() {}, 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() {}, 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")
|
|
}
|
|
}
|
|
|
|
// The filer's own MetaAggregator and an in-process S3 gateway keep metadata
|
|
// subscription streams open until the filer is shut down, which happens after
|
|
// gRPC GracefulStop - so the subscriptions have to end before it, or every
|
|
// shutdown sits out the full graceful-stop timeout.
|
|
func TestFilerShutdownStopsSubscriptionsBeforeGrpc(t *testing.T) {
|
|
var order []string
|
|
var mu sync.Mutex
|
|
record := func(step string) func() {
|
|
return func() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
order = append(order, step)
|
|
}
|
|
}
|
|
shutdown := newFilerShutdown(record("leave"), record("subscriptions"), record("grpc"), record("filer"))
|
|
shutdown()
|
|
|
|
want := []string{"leave", "subscriptions", "grpc", "filer"}
|
|
if len(order) != len(want) {
|
|
t.Fatalf("shutdown steps %v, want %v", order, want)
|
|
}
|
|
for i := range want {
|
|
if order[i] != want[i] {
|
|
t.Fatalf("shutdown steps %v, want %v", order, want)
|
|
}
|
|
}
|
|
}
|