Files
seaweedfs/weed/mount/filehandle_read.go
Chris Lu ab490013e0 mount: stable-read open fence, causal log timestamp on the cache response
An event applied while the open's lookup was in flight could already be
reflected in the returned entry while sitting above the pre-lookup
cursor, so the fence under-covered and the queued invalidation replaced
the fresher entry. The open now re-reads until the cursor is stable
across the lookup; on sustained churn the last pre-lookup value stands,
which can only under-fence, never block a newer event.

A pre-RPC ping likewise cannot vouch for events committed during the
RPC itself. CacheRemoteObjectToLocalCluster now returns a log timestamp
stamped before the filer reads the entry — events are logged after
their store write on that same clock, so everything at or below it is
reflected in the returned entry, making the fence causal with the
response. The mount prefers the response event's timestamp, then this
log timestamp, and keeps the pre-RPC ping only for filers that return
neither.
2026-07-22 20:06:29 -07:00

220 lines
7.6 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)
// Barrier through this closure's client: on failover WithFilerClient
// retries against another filer while the current-filer index still
// points at the failed one. Only a fallback for old filers — the
// response's own log timestamp is causal with the returned entry,
// while a pre-RPC ping cannot cover events committed during the RPC.
baselineTsNs := fh.wfs.filerBarrierTsNsWith(client)
resp, err := client.CacheRemoteObjectToLocalCluster(context.Background(), request)
if err != nil {
return fmt.Errorf("CacheRemoteObjectToLocalCluster file %s: %v", fileFullPath, err)
}
fh.SetEntry(resp.Entry)
if resp.GetLogTsNs() > baselineTsNs {
baselineTsNs = resp.GetLogTsNs()
}
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
}