mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-11 16:57:45 +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>
151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func newStoppableFilerServer() *FilerServer {
|
|
fs := &FilerServer{}
|
|
fs.subscriptionsStopped, fs.stopSubscriptions = context.WithCancel(context.Background())
|
|
return fs
|
|
}
|
|
|
|
func TestStopSubscriptionsEndsOpenSubscriptions(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
ctx, cancel := fs.subscriptionContext(context.Background())
|
|
defer cancel()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("subscription ended before StopSubscriptions")
|
|
default:
|
|
}
|
|
fs.StopSubscriptions()
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("subscription still open after StopSubscriptions")
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionStartedAfterStopEndsAtOnce(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
fs.StopSubscriptions()
|
|
ctx, cancel := fs.subscriptionContext(context.Background())
|
|
defer cancel()
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("a subscription started during shutdown stayed open")
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionStillEndsWithItsStream(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
stream, closeStream := context.WithCancel(context.WithValue(context.Background(), struct{}{}, "peer"))
|
|
ctx, cancel := fs.subscriptionContext(stream)
|
|
defer cancel()
|
|
if ctx.Value(struct{}{}) != "peer" {
|
|
t.Error("subscription context lost the stream's values")
|
|
}
|
|
closeStream()
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("subscription outlived its stream")
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionContextWithoutStopSignal(t *testing.T) {
|
|
fs := &FilerServer{}
|
|
fs.StopSubscriptions()
|
|
ctx, cancel := fs.subscriptionContext(context.Background())
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("subscription ended although nothing stopped it")
|
|
default:
|
|
}
|
|
cancel()
|
|
<-ctx.Done()
|
|
}
|
|
|
|
// A follower reads a clean end as "done" and stops for good (util.RetryUntil
|
|
// returns on nil), so a subscription that StopSubscriptions ended must not
|
|
// end cleanly.
|
|
func TestStoppedSubscriptionEndsUnavailable(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
fs.StopSubscriptions()
|
|
err := fs.endOfSubscription(context.Background(), nil)
|
|
if status.Code(err) != codes.Unavailable {
|
|
t.Fatalf("stopped subscription ended with %v, want Unavailable", err)
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionEndsCleanlyWithoutStop(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
if err := fs.endOfSubscription(context.Background(), nil); err != nil {
|
|
t.Fatalf("subscription without shutdown ended with %v, want nil", err)
|
|
}
|
|
if err := (&FilerServer{}).endOfSubscription(context.Background(), nil); err != nil {
|
|
t.Fatalf("server without stop signal ended with %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionErrorPassesThroughStop(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
fs.StopSubscriptions()
|
|
want := errors.New("reading from persisted logs")
|
|
if err := fs.endOfSubscription(context.Background(), want); !errors.Is(err, want) {
|
|
t.Fatalf("got %v, want the handler's own error", err)
|
|
}
|
|
}
|
|
|
|
func TestClosedStreamEndsCleanlyDuringStop(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
fs.StopSubscriptions()
|
|
stream, closeStream := context.WithCancel(context.Background())
|
|
closeStream()
|
|
if err := fs.endOfSubscription(stream, nil); err != nil {
|
|
t.Fatalf("stream the client closed ended with %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
// A read pass surfaces StopSubscriptions as a wrapped context.Canceled; on the
|
|
// wire that must still read as Unavailable so followers retry.
|
|
func TestCanceledReadEndsUnavailableDuringStop(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
fs.StopSubscriptions()
|
|
canceled := fmt.Errorf("reading from persisted logs: %w", context.Canceled)
|
|
if err := fs.endOfSubscription(context.Background(), canceled); status.Code(err) != codes.Unavailable {
|
|
t.Fatalf("stopped read ended with %v, want Unavailable", err)
|
|
}
|
|
}
|
|
|
|
func TestCanceledReadPassesThroughWithoutStop(t *testing.T) {
|
|
fs := newStoppableFilerServer()
|
|
canceled := fmt.Errorf("reading from persisted logs: %w", context.Canceled)
|
|
if err := fs.endOfSubscription(context.Background(), canceled); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("got %v, want the handler's own error", err)
|
|
}
|
|
}
|
|
|
|
// Neither read loop checks ctx per entry; the entry callback does.
|
|
func TestEachLogEntryFnEndsOnCancel(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
var filtered int64
|
|
fn := eachLogEntryFn(ctx, &filer_pb.SubscribeMetadataRequest{}, nil, nil, &filtered)
|
|
done, err := fn(&filer_pb.LogEntry{})
|
|
if !done || err != nil {
|
|
t.Fatalf("cancelled subscription returned done=%v err=%v, want a clean end", done, err)
|
|
}
|
|
}
|