package reader import ( "context" "errors" "testing" "time" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) // stalledStream stays open and never delivers: keepalives answered, no // progress. type stalledStream struct { grpc.ClientStream ctx context.Context } func (s *stalledStream) Recv() (*filer_pb.SubscribeMetadataResponse, error) { <-s.ctx.Done() return nil, s.ctx.Err() } type stalledClient struct { filer_pb.SeaweedFilerClient req *filer_pb.SubscribeMetadataRequest } func (c *stalledClient) SubscribeMetadata(ctx context.Context, req *filer_pb.SubscribeMetadataRequest, _ ...grpc.CallOption) (filer_pb.SeaweedFiler_SubscribeMetadataClient, error) { c.req = req return &stalledStream{ctx: ctx}, nil } func TestRun_ReceiveTimeoutOnStalledStream(t *testing.T) { client := &stalledClient{} r := &Reader{ ShardPredicate: func(int) bool { return true }, BucketsPath: "/buckets", Events: make(chan *Event, 1), ReceiveTimeout: 50 * time.Millisecond, } done := make(chan error, 1) go func() { done <- r.Run(context.Background(), client, "test", 1) }() select { case err := <-done: require.ErrorIs(t, err, ErrReceiveTimeout) case <-time.After(5 * time.Second): t.Fatal("Run did not time out on a stalled stream") } assert.True(t, client.req.ClientSupportsIdleHeartbeat, "a reader with a receive timeout must ask for heartbeats, or a caught-up stream looks stalled") } // heartbeatStream delivers only heartbeats: a ts, no EventNotification. type heartbeatStream struct { grpc.ClientStream every time.Duration sent int max int } func (s *heartbeatStream) Recv() (*filer_pb.SubscribeMetadataResponse, error) { if s.sent >= s.max { return nil, context.Canceled } time.Sleep(s.every) s.sent++ return &filer_pb.SubscribeMetadataResponse{TsNs: int64(s.sent)}, nil } type heartbeatClient struct { filer_pb.SeaweedFilerClient stream *heartbeatStream } func (c *heartbeatClient) SubscribeMetadata(_ context.Context, _ *filer_pb.SubscribeMetadataRequest, _ ...grpc.CallOption) (filer_pb.SeaweedFiler_SubscribeMetadataClient, error) { return c.stream, nil } // Why the timeout is safe to set: a caught-up stream keeps arriving, so // the watchdog fires on real silence, not on an idle cluster. func TestRun_HeartbeatsHoldTheStreamOpen(t *testing.T) { stream := &heartbeatStream{every: 20 * time.Millisecond, max: 10} r := &Reader{ ShardPredicate: func(int) bool { return true }, BucketsPath: "/buckets", Events: make(chan *Event, 1), ReceiveTimeout: 200 * time.Millisecond, } done := make(chan error, 1) go func() { done <- r.Run(context.Background(), &heartbeatClient{stream: stream}, "test", 1) }() select { case err := <-done: assert.False(t, errors.Is(err, ErrReceiveTimeout), "heartbeats spanning more than one timeout window must keep the stream alive") case <-time.After(5 * time.Second): t.Fatal("Run never returned") } assert.Equal(t, 10, stream.sent, "every heartbeat should have been consumed") } func TestRun_RejectsNegativeReceiveTimeout(t *testing.T) { r := &Reader{ ShardPredicate: func(int) bool { return true }, BucketsPath: "/buckets", Events: make(chan *Event, 1), ReceiveTimeout: -time.Second, } err := r.Run(context.Background(), &stalledClient{}, "test", 1) require.Error(t, err) assert.Contains(t, err.Error(), "ReceiveTimeout") }