mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 12:34:29 +02:00
A handle opened while an older event sat in the invalidation queue started with a zero watermark, so the delayed worker rolled its freshly looked-up entry back. The open now fences the handle with the event cursor captured before the lookup: the entry it installs reflects every event applied by then, so anything queued at or before that position is old news. Captured before, never after, so an event arriving mid-open cannot inflate the fence past the state the lookup returned — and the cursor now advances only after the store write, so the pairing is sound. Handles reusing an existing entry are not fenced, since that entry did not come from the lookup. The delivered-event cursor also misses events committed on the filer but not yet delivered to the subscription, which let a late-arriving older event roll back a server-side copy or an already-cached remote download. Those paths now take a barrier from a filer self-ping issued before the operation: events are stamped with that same clock, so the returned state is at least as new as anything at or below it. The ping is a single bounded attempt with the cursor as fallback.
212 lines
7.2 KiB
Go
212 lines
7.2 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
)
|
|
|
|
func (fh *FileHandle) lockForRead(startOffset int64, size int) {
|
|
fh.dirtyPages.LockForRead(startOffset, startOffset+int64(size))
|
|
}
|
|
func (fh *FileHandle) unlockForRead(startOffset int64, size int) {
|
|
fh.dirtyPages.UnlockForRead(startOffset, startOffset+int64(size))
|
|
}
|
|
|
|
func (fh *FileHandle) readFromDirtyPages(buff []byte, startOffset int64, tsNs int64) (maxStop int64) {
|
|
maxStop = fh.dirtyPages.ReadDirtyDataAt(buff, startOffset, tsNs)
|
|
return
|
|
}
|
|
|
|
func (fh *FileHandle) readFromChunks(buff []byte, offset int64) (int64, int64, error) {
|
|
return fh.readFromChunksWithContext(context.Background(), buff, offset)
|
|
}
|
|
|
|
func (fh *FileHandle) readFromChunksWithContext(ctx context.Context, buff []byte, offset int64) (int64, int64, error) {
|
|
fh.entryLock.RLock()
|
|
defer fh.entryLock.RUnlock()
|
|
|
|
fileFullPath := fh.FullPath()
|
|
|
|
entry := fh.GetEntry()
|
|
|
|
// IsInRemoteOnly inspects entry.Chunks, so take the LockedEntry lock the
|
|
// async uploader appends under.
|
|
entry.RLock()
|
|
remoteOnly := entry.Entry.IsInRemoteOnly()
|
|
entry.RUnlock()
|
|
if remoteOnly {
|
|
glog.V(4).Infof("download remote entry %s", fileFullPath)
|
|
err := fh.downloadRemoteEntry(entry)
|
|
if err != nil {
|
|
glog.V(1).Infof("download remote entry %s: %v", fileFullPath, err)
|
|
return 0, 0, err
|
|
}
|
|
}
|
|
|
|
// Snapshot size, inline content, and the chunk list under the LockedEntry
|
|
// lock. Async upload workers append chunks under this lock (AddChunks), so
|
|
// reading entry.Chunks / FileSize without it races with the slice
|
|
// reallocation and can crash in filer.TotalSize. The captured slice headers
|
|
// stay valid afterwards: append never mutates the old backing array, and
|
|
// truncate is excluded by the fh.entryLock held for this whole read.
|
|
entry.RLock()
|
|
pbEntry := entry.Entry
|
|
fileSize := int64(pbEntry.Attributes.FileSize)
|
|
if fileSize == 0 {
|
|
fileSize = int64(filer.FileSize(pbEntry))
|
|
}
|
|
content := pbEntry.Content
|
|
chunks := pbEntry.Chunks
|
|
entry.RUnlock()
|
|
|
|
if fileSize == 0 {
|
|
glog.V(1).Infof("empty fh %v", fileFullPath)
|
|
return 0, 0, io.EOF
|
|
} else if offset == fileSize {
|
|
return 0, 0, io.EOF
|
|
} else if offset >= fileSize {
|
|
glog.V(1).Infof("invalid read, fileSize %d, offset %d for %s", fileSize, offset, fileFullPath)
|
|
return 0, 0, io.EOF
|
|
}
|
|
|
|
if offset < int64(len(content)) {
|
|
totalRead := copy(buff, content[offset:])
|
|
glog.V(4).Infof("file handle read cached %s [%d,%d] %d", fileFullPath, offset, offset+int64(totalRead), totalRead)
|
|
return int64(totalRead), 0, nil
|
|
}
|
|
|
|
// Try RDMA acceleration first if available
|
|
if fh.wfs.rdmaClient != nil && fh.wfs.option.RdmaEnabled {
|
|
totalRead, ts, err := fh.tryRDMARead(ctx, fileSize, buff, offset, chunks)
|
|
if err == nil {
|
|
glog.V(4).Infof("RDMA read successful for %s [%d,%d] %d", fileFullPath, offset, offset+int64(totalRead), totalRead)
|
|
return int64(totalRead), ts, nil
|
|
}
|
|
glog.V(4).Infof("RDMA read failed for %s, falling back to HTTP: %v", fileFullPath, err)
|
|
}
|
|
|
|
// Peer chunk sharing: try a peer mount's cache before the volume tier.
|
|
// Any failure falls through transparently. See design-weed-mount-
|
|
// peer-chunk-sharing.md §4.3.
|
|
if fh.wfs.option.PeerEnabled && fh.wfs.peerGrpcServer != nil {
|
|
totalRead, ts, err := fh.tryPeerRead(ctx, fileSize, buff, offset, chunks)
|
|
if err == nil {
|
|
glog.V(4).Infof("peer read successful for %s [%d,%d] %d", fileFullPath, offset, offset+int64(totalRead), totalRead)
|
|
return int64(totalRead), ts, nil
|
|
}
|
|
// Skip the "failed" log for benign skip reasons (local cache
|
|
// hit, no peer owner yet, etc.) — the cache/volume fallback is
|
|
// the expected outcome, not a failure.
|
|
if err != errPeerReadSkipped {
|
|
glog.V(4).Infof("peer read failed for %s, falling back to volume: %v", fileFullPath, err)
|
|
}
|
|
}
|
|
|
|
// Fall back to normal chunk reading
|
|
totalRead, ts, err := fh.entryChunkGroup.ReadDataAt(ctx, fileSize, buff, offset)
|
|
|
|
if err != nil && err != io.EOF {
|
|
glog.Errorf("file handle read %s: %v", fileFullPath, err)
|
|
}
|
|
|
|
// glog.V(4).Infof("file handle read %s [%d,%d] %d : %v", fileFullPath, offset, offset+int64(totalRead), totalRead, err)
|
|
|
|
return int64(totalRead), ts, err
|
|
}
|
|
|
|
// tryRDMARead attempts to read file data using RDMA acceleration. chunks is a
|
|
// snapshot captured under the LockedEntry lock by the caller.
|
|
func (fh *FileHandle) tryRDMARead(ctx context.Context, fileSize int64, buff []byte, offset int64, chunks []*filer_pb.FileChunk) (int64, int64, error) {
|
|
// For now, we'll try to read the chunks directly using RDMA
|
|
// This is a simplified approach - in a full implementation, we'd need to
|
|
// handle chunk boundaries, multiple chunks, etc.
|
|
|
|
if len(chunks) == 0 {
|
|
return 0, 0, fmt.Errorf("no chunks available for RDMA read")
|
|
}
|
|
|
|
// Find the chunk that contains our offset using binary search
|
|
var targetChunk *filer_pb.FileChunk
|
|
var chunkOffset int64
|
|
|
|
// Get cached cumulative offsets for efficient binary search
|
|
cumulativeOffsets := fh.getCumulativeOffsets(chunks)
|
|
|
|
// Use binary search to find the chunk containing the offset
|
|
chunkIndex := sort.Search(len(chunks), func(i int) bool {
|
|
return offset < cumulativeOffsets[i+1]
|
|
})
|
|
|
|
// Verify the chunk actually contains our offset
|
|
if chunkIndex < len(chunks) && offset >= cumulativeOffsets[chunkIndex] {
|
|
targetChunk = chunks[chunkIndex]
|
|
chunkOffset = offset - cumulativeOffsets[chunkIndex]
|
|
}
|
|
|
|
if targetChunk == nil {
|
|
return 0, 0, fmt.Errorf("no chunk found for offset %d", offset)
|
|
}
|
|
|
|
// Calculate how much to read from this chunk
|
|
remainingInChunk := int64(targetChunk.Size) - chunkOffset
|
|
readSize := min(int64(len(buff)), remainingInChunk)
|
|
|
|
glog.V(4).Infof("RDMA read attempt: chunk=%s (fileId=%s), chunkOffset=%d, readSize=%d",
|
|
targetChunk.FileId, targetChunk.FileId, chunkOffset, readSize)
|
|
|
|
// Try RDMA read using file ID directly (more efficient)
|
|
data, isRDMA, err := fh.wfs.rdmaClient.ReadNeedle(ctx, targetChunk.FileId, uint64(chunkOffset), uint64(readSize))
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("RDMA read failed: %w", err)
|
|
}
|
|
|
|
if !isRDMA {
|
|
return 0, 0, fmt.Errorf("RDMA not available for chunk")
|
|
}
|
|
|
|
// Copy data to buffer
|
|
copied := copy(buff, data)
|
|
return int64(copied), targetChunk.ModifiedTsNs, nil
|
|
}
|
|
|
|
func (fh *FileHandle) downloadRemoteEntry(entry *LockedEntry) error {
|
|
|
|
fileFullPath := fh.FullPath()
|
|
dir, _ := fileFullPath.DirAndName()
|
|
|
|
err := fh.wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
|
|
request := &filer_pb.CacheRemoteObjectToLocalClusterRequest{
|
|
Directory: string(dir),
|
|
Name: entry.Name,
|
|
}
|
|
|
|
glog.V(4).Infof("download entry: %v", request)
|
|
baselineTsNs := fh.wfs.filerBarrierTsNs()
|
|
resp, err := client.CacheRemoteObjectToLocalCluster(context.Background(), request)
|
|
if err != nil {
|
|
return fmt.Errorf("CacheRemoteObjectToLocalCluster file %s: %v", fileFullPath, err)
|
|
}
|
|
|
|
fh.SetEntry(resp.Entry)
|
|
fh.noteFilerAck(baselineTsNs, resp.GetMetadataEvent())
|
|
|
|
// Async: a sync apply deadlocks against the apply loop's invalidate, which needs this read's file-handle lock.
|
|
event := resp.GetMetadataEvent()
|
|
if event == nil {
|
|
event = metadataUpdateEvent(request.Directory, resp.Entry)
|
|
}
|
|
fh.wfs.applyLocalMetadataEventAsync(event)
|
|
|
|
return nil
|
|
})
|
|
|
|
return err
|
|
}
|