mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 17:00:48 +01:00
* mount: keep the shared directory build out of the requester's context The shared directory build behind EnsureVisited is a singleflight call: the first requester for a path owns the build, and every later requester for the same path waits on that same build. The build ran on the owner's request context, so a FUSE request that returned -- or that the kernel interrupted -- canceled the build for everybody waiting on it. A waiter whose own context was still alive then received somebody else's cancellation. weed/mount/weedfs_dir_read.go classified a canceled read as EINTR only when the local context was done, so such a waiter fell through to fuse.EIO: the kernel handed EIO to readdirent, kubelet's applyFSGroup failed, and the volume never mounted. Build the shared directory on a context that keeps the request's values but not its cancellation, and report a canceled read as EINTR even when the local context is still alive. Waiters still abandon on their own context, so a canceled request stops waiting immediately. This follows up #11717, which made the wait abandonable and mapped the local cancellation to EINTR, but left the shared build itself on the owner's context. The surrounding build lifecycle calls already use context.Background() for the same reason. Test: TestEnsureVisitedSharedBuildSurvivesOwnerRequestCancel reproduces the cancellation with a fake filer and fails without this change. * mount: classify gRPC-canceled directory listings as EINTR * mount: log only real directory read failures, not interruptions * mount: keep shared section refresh out of the requester's context * mount: let section-refresh waiters leave on their own context The shared refresh flight now outlives the requester that started it, but callers still blocked in singleflight.Do until it finished — an interrupted directory read could not stop waiting, and the caller's own deadline never applied to the wait. DoChan + select on the caller's context restores early abandon while the detached refresh still completes for the remaining waiters. * ci: re-run FUSE integration after a flaky writeback-cache failure * mount: refresh a stale entry once before reporting a read error A read resolves chunk views from the handle's entry. A concurrent overwrite commits a new entry whose replaced file ids the filer queues for deletion; the open handle only learns the new chunk list when the metadata invalidation lands. A fetch that lands between the volume delete and the invalidation names data that is already gone and the read reports EIO. On a chunk read error, wait out any in-flight async flush, re-read the entry from the filer, install it when it outranks the handle, and retry once. An unchanged version means the failure is genuine. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: fence the read-error entry refresh by file identity Two guards on refreshEntryForRead's install path: the unlocked dirtyMetadata read raced with Write under the handle lock — drop it, installAckedEntry already checks under that lock, and it now fences isDeleted handles the same way. And a saved path can name a different file after unlink/reuse, so refuse the refresh when the fetched inode disagrees with the one the handle holds. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
type directoryListStream struct {
|
|
responses []*filer_pb.ListEntriesResponse
|
|
index int
|
|
}
|
|
|
|
func (s *directoryListStream) Recv() (*filer_pb.ListEntriesResponse, error) {
|
|
if s.index >= len(s.responses) {
|
|
return nil, io.EOF
|
|
}
|
|
resp := s.responses[s.index]
|
|
s.index++
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *directoryListStream) Header() (metadata.MD, error) { return metadata.MD{}, nil }
|
|
func (s *directoryListStream) Trailer() metadata.MD { return metadata.MD{} }
|
|
func (s *directoryListStream) CloseSend() error { return nil }
|
|
func (s *directoryListStream) Context() context.Context { return context.Background() }
|
|
func (s *directoryListStream) SendMsg(any) error { return nil }
|
|
func (s *directoryListStream) RecvMsg(any) error { return nil }
|
|
|
|
type directoryListClient struct {
|
|
filer_pb.SeaweedFilerClient
|
|
responses []*filer_pb.ListEntriesResponse
|
|
}
|
|
|
|
func (c *directoryListClient) ListEntries(ctx context.Context, in *filer_pb.ListEntriesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[filer_pb.ListEntriesResponse], error) {
|
|
return &directoryListStream{responses: c.responses}, nil
|
|
}
|
|
|
|
type directoryFilerAccessor struct {
|
|
client filer_pb.SeaweedFilerClient
|
|
}
|
|
|
|
func (a *directoryFilerAccessor) WithFilerClient(_ bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
|
return fn(a.client)
|
|
}
|
|
|
|
func (a *directoryFilerAccessor) AdjustedUrl(*filer_pb.Location) string { return "" }
|
|
func (a *directoryFilerAccessor) GetDataCenter() string { return "" }
|
|
|
|
func TestLoadDirectoryEntriesDirectFiltersHiddenEntriesAndMapsIds(t *testing.T) {
|
|
mapper, err := meta_cache.NewUidGidMapper("10:1000", "20:2000")
|
|
if err != nil {
|
|
t.Fatalf("uid/gid mapper: %v", err)
|
|
}
|
|
|
|
client := &directoryFilerAccessor{
|
|
client: &directoryListClient{
|
|
responses: []*filer_pb.ListEntriesResponse{
|
|
{
|
|
Entry: &filer_pb.Entry{
|
|
Name: "topics",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Uid: 1000,
|
|
Gid: 2000,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Entry: &filer_pb.Entry{
|
|
Name: "visible",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Uid: 1000,
|
|
Gid: 2000,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
entries, _, err := loadDirectoryEntriesDirect(context.Background(), client, mapper, util.FullPath("/"), "", false, 10, 0, false)
|
|
if err != nil {
|
|
t.Fatalf("loadDirectoryEntriesDirect: %v", err)
|
|
}
|
|
if got := len(entries); got != 1 {
|
|
t.Fatalf("entry count = %d, want 1", got)
|
|
}
|
|
if entries[0].Name() != "visible" {
|
|
t.Fatalf("entry name = %q, want visible", entries[0].Name())
|
|
}
|
|
if entries[0].Attr.Uid != 10 || entries[0].Attr.Gid != 20 {
|
|
t.Fatalf("mapped uid/gid = %d/%d, want 10/20", entries[0].Attr.Uid, entries[0].Attr.Gid)
|
|
}
|
|
}
|
|
|
|
func TestLoadDirectoryEntriesDirectShowsSystemEntriesWhenEnabled(t *testing.T) {
|
|
client := &directoryFilerAccessor{
|
|
client: &directoryListClient{
|
|
responses: []*filer_pb.ListEntriesResponse{
|
|
{Entry: &filer_pb.Entry{Name: "topics"}},
|
|
{Entry: &filer_pb.Entry{Name: "visible"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
entries, _, err := loadDirectoryEntriesDirect(context.Background(), client, nil, util.FullPath("/"), "", false, 10, 0, true)
|
|
if err != nil {
|
|
t.Fatalf("loadDirectoryEntriesDirect: %v", err)
|
|
}
|
|
if got := len(entries); got != 2 {
|
|
t.Fatalf("entry count = %d, want 2", got)
|
|
}
|
|
if entries[0].Name() != "topics" || entries[1].Name() != "visible" {
|
|
t.Fatalf("entry names = %q, %q, want topics, visible", entries[0].Name(), entries[1].Name())
|
|
}
|
|
}
|
|
|
|
func TestDirReadStatus(t *testing.T) {
|
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
err error
|
|
want fuse.Status
|
|
}{
|
|
{"local context canceled", canceledCtx, io.EOF, fuse.EINTR},
|
|
{"context canceled error", context.Background(), context.Canceled, fuse.EINTR},
|
|
{"context deadline error", context.Background(), context.DeadlineExceeded, fuse.EINTR},
|
|
{"grpc canceled", context.Background(), status.Error(codes.Canceled, "stream canceled"), fuse.EINTR},
|
|
{"grpc deadline exceeded", context.Background(), status.Error(codes.DeadlineExceeded, "deadline exceeded"), fuse.EINTR},
|
|
{"wrapped grpc canceled", context.Background(), fmt.Errorf("list: %w", status.Error(codes.Canceled, "stream canceled")), fuse.EINTR},
|
|
{"ordinary failure", context.Background(), errors.New("filer broke"), fuse.EIO},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := dirReadStatus(tt.ctx, tt.err, "dir read: %v", tt.err); got != tt.want {
|
|
t.Errorf("%s: dirReadStatus = %v, want %v", tt.name, got, tt.want)
|
|
}
|
|
}
|
|
}
|