mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-07 20:47:46 +01:00
* volume: check a needle's CRC the first time a range of it is read A ranged read never checked the CRC, so a replica with a damaged needle served the damaged bytes with 206 and the reader never tried another copy. The first ranged read of a needle now streams the whole needle through the existing CRC check, with the usual pooled page buffers, before writing any byte. A mismatch fails the read, so the reader falls back to another replica. Needles that pass go into a bounded LRU, keyed by volume, compaction revision, id, append time and checksum, so later ranges of the same needle stream as before. Fixes #5959 for ranged reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume: key verified ranges by volume load, share concurrent checks From review: the cache key read v.SuperBlock.CompactionRevision without the data file lock, racing a compaction commit that reloads the super block, and a remounted copy of a volume inherited the old copy's verified entries. Key entries by a load generation instead, an atomic counter that changes on every load, including the reload in CommitCompact, and cache a result only if no reload happened during the check. Concurrent first ranges of a needle now share one check through singleflight instead of each reading the whole needle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume: keep hot needles verified, leave the io error streak to the range read From review: Contains does not refresh an entry's place in the LRU, so even a constantly read needle was evicted after 64k other needles were verified; look it up with Get instead. The verification pass also cleared the io error streak before the range read ran, so a range read failing with EIO after a passing check never built up a streak; the pass into io.Discard now leaves the streak to the range read, like the meta read before a streamed GET. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume (Rust): check a needle's CRC the first time a range of it is read Mirror of the Go change: a range served from the data file now checks the whole needle's CRC before any byte of it is sent, so a damaged replica answers 500 and the reader tries another copy. Needles that pass go into a bounded LRU keyed by the volume's load generation, needle id, append time and checksum; concurrent first ranges of a needle share one check, and a check that passes leaves the io error streak to the range read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume (Rust): a range over the whole needle checks its CRC Go reads a range that covers the whole needle through the checked full-read path. The Rust volume server read it from the data file unchecked, so "bytes=0-" served a damaged needle with 206. Check the slice's CRC before answering, as the full read does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume: check the covered blocks of every range against CRCs recorded at the first check The first ranged read of a needle now records the CRC of each block (64 KiB to 1 MiB, at most 64 blocks below 64 MiB) while it checks the whole needle. Later ranges read the block-aligned span around them and write a block's bytes only once it matches, so damage that appears after the first check is not served. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * volume (Rust): check the covered blocks of every range against CRCs recorded at the first check verify_checksum now records a CRC per block (same sizes as Go's rangeCheckBlockSize) and returns them from the cache; read_checked_range reads the block-aligned span of a range and answers 500 if a block no longer matches, so damage after the first check is not served. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
661 lines
21 KiB
Go
661 lines
21 KiB
Go
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
|
"golang.org/x/sync/singleflight"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/util/mem"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
. "github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
const PagedReadLimit = 1024 * 1024
|
|
|
|
// A range carries no checksum of its own, so a ranged read checks the whole
|
|
// needle's CRC the first time any range of it is read, recording the CRC of
|
|
// each block on the way. verifiedNeedles keeps those block CRCs, and later
|
|
// ranges check just the blocks they cover, so damage that appears after the
|
|
// first check is still caught. Entries are per volume load, so a remounted
|
|
// copy or a compaction is checked again, and concurrent first reads of a
|
|
// needle share one check.
|
|
var (
|
|
verifiedNeedles, _ = lru.New[verifiedNeedle, []needle.CRC](64 * 1024)
|
|
verifyingNeedles singleflight.Group
|
|
volumeLoads atomic.Uint64
|
|
)
|
|
|
|
const (
|
|
rangeCheckBlocks = 64
|
|
minRangeCheckBlockSize = 64 * 1024
|
|
)
|
|
|
|
// rangeCheckBlockSize keeps a needle to 64 blocks, each 64 KiB to 1 MiB.
|
|
func rangeCheckBlockSize(dataSize int64) int64 {
|
|
blockSize := int64(minRangeCheckBlockSize)
|
|
for blockSize < PagedReadLimit && dataSize > rangeCheckBlocks*blockSize {
|
|
blockSize *= 2
|
|
}
|
|
return blockSize
|
|
}
|
|
|
|
type verifiedNeedle struct {
|
|
loadGeneration uint64
|
|
id NeedleId
|
|
appendAtNs uint64
|
|
checksum needle.CRC
|
|
}
|
|
|
|
// read fills in Needle content by looking up n.Id from NeedleMapper
|
|
func (v *Volume) readNeedle(n *needle.Needle, readOption *ReadOption, onReadSizeFn func(size Size)) (count int, err error) {
|
|
v.dataFileAccessLock.RLock()
|
|
defer v.dataFileAccessLock.RUnlock()
|
|
|
|
if err := v.UnavailableError(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if v.nm == nil {
|
|
glog.V(0).Infof("volume %d: needle map not loaded; read returns not-found", v.Id)
|
|
return -1, ErrorNotFound
|
|
}
|
|
|
|
nv, ok := v.nm.Get(n.Id)
|
|
if !ok || nv.Offset.IsZero() {
|
|
return -1, ErrorNotFound
|
|
}
|
|
readSize := nv.Size
|
|
if readSize.IsDeleted() {
|
|
if readOption != nil && readOption.ReadDeleted && readSize != TombstoneFileSize {
|
|
glog.V(3).Infof("reading deleted %s", n.String())
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ReadDeletedNeedle).Inc()
|
|
readSize = -readSize
|
|
} else {
|
|
return -1, ErrorDeleted
|
|
}
|
|
}
|
|
if readSize == 0 {
|
|
return 0, nil
|
|
}
|
|
if onReadSizeFn != nil {
|
|
onReadSizeFn(readSize)
|
|
}
|
|
// A version 1 needle has no data size field and no metadata after its
|
|
// data, which ReadNeedleMeta relies on, so it is always read in full.
|
|
if readOption != nil && readOption.AttemptMetaOnly && v.Version() != needle.Version1 && (readOption.MustMetaOnly || readOption.HasRange || readSize > PagedReadLimit) {
|
|
readOption.VolumeRevision = v.SuperBlock.CompactionRevision
|
|
err = n.ReadNeedleMeta(v.DataBackend, nv.Offset.ToActualOffset(), readSize, v.Version())
|
|
if err == needle.ErrorSizeMismatch && OffsetSize == 4 {
|
|
readOption.IsOutOfRange = true
|
|
err = n.ReadNeedleMeta(v.DataBackend, nv.Offset.ToActualOffset()+int64(MaxPossibleVolumeSize), readSize, v.Version())
|
|
}
|
|
if err != nil {
|
|
v.checkReadWriteError(err)
|
|
return 0, err
|
|
}
|
|
if !n.IsCompressed() && !n.IsChunkedManifest() {
|
|
readOption.IsMetaOnly = true
|
|
readOption.metaOffset = nv.Offset
|
|
// A must-meta-only read ends here, so its success may reset the
|
|
// streak. For a streamed GET the meta read precedes the payload read,
|
|
// which reports the outcome itself.
|
|
if readOption.MustMetaOnly {
|
|
v.checkReadWriteError(nil)
|
|
}
|
|
}
|
|
}
|
|
if readOption == nil || !readOption.IsMetaOnly {
|
|
err = n.ReadData(v.DataBackend, nv.Offset.ToActualOffset(), readSize, v.Version())
|
|
v.checkReadWriteError(err)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
count = int(n.DataSize)
|
|
if !n.HasTtl() {
|
|
return
|
|
}
|
|
ttlMinutes := n.Ttl.Minutes()
|
|
if ttlMinutes == 0 {
|
|
return
|
|
}
|
|
if !n.HasLastModifiedDate() {
|
|
return
|
|
}
|
|
if time.Now().Before(time.Unix(0, int64(n.AppendAtNs)).Add(time.Duration(ttlMinutes) * time.Minute)) {
|
|
return
|
|
}
|
|
return -1, ErrorNotFound
|
|
}
|
|
|
|
// NoteMetaOnlySuccess records a request that ended at the metadata read
|
|
// (e.g. a HEAD or a cookie mismatch), so its success may reset the streak —
|
|
// unlike a meta read that precedes a payload read.
|
|
func (v *Volume) NoteMetaOnlySuccess() {
|
|
v.checkReadWriteError(nil)
|
|
}
|
|
|
|
// read needle at a specific offset
|
|
func (v *Volume) readNeedleMetaAt(n *needle.Needle, offset int64, size int32) (err error) {
|
|
v.dataFileAccessLock.RLock()
|
|
defer v.dataFileAccessLock.RUnlock()
|
|
|
|
if err := v.UnavailableError(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// read deleted needle meta data
|
|
if size < 0 {
|
|
size = 0
|
|
}
|
|
err = n.ReadNeedleMeta(v.DataBackend, offset, Size(size), v.Version())
|
|
if err == needle.ErrorSizeMismatch && OffsetSize == 4 {
|
|
err = n.ReadNeedleMeta(v.DataBackend, offset+int64(MaxPossibleVolumeSize), Size(size), v.Version())
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// verifyMovedRecord checks that the record a compaction moved is the one n's
|
|
// metadata came from, not a replacement of the same size, and sets
|
|
// IsOutOfRange for where it now lies.
|
|
func (v *Volume) verifyMovedRecord(n *needle.Needle, moved *needle_map.NeedleValue, found bool, readOption *ReadOption) error {
|
|
if !found || moved.Offset.IsZero() || moved.Size != n.Size {
|
|
return ErrorNotFound
|
|
}
|
|
m := new(needle.Needle)
|
|
readOption.IsOutOfRange = false
|
|
err := m.ReadNeedleMeta(v.DataBackend, moved.Offset.ToActualOffset(), moved.Size, v.Version())
|
|
if err == needle.ErrorSizeMismatch && OffsetSize == 4 {
|
|
readOption.IsOutOfRange = true
|
|
err = m.ReadNeedleMeta(v.DataBackend, moved.Offset.ToActualOffset()+int64(MaxPossibleVolumeSize), moved.Size, v.Version())
|
|
}
|
|
if err != nil {
|
|
v.checkReadWriteError(err)
|
|
return fmt.Errorf("ReadNeedleMeta of moved needle: %w", err)
|
|
}
|
|
// Compaction rewrites a pre-3.09 checksum in the current encoding.
|
|
if m.AppendAtNs != n.AppendAtNs || (m.Checksum != n.Checksum && uint32(n.Checksum) != m.Checksum.Value()) {
|
|
return ErrorNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// read fills in Needle content by looking up n.Id from NeedleMapper
|
|
func (v *Volume) readNeedleDataInto(n *needle.Needle, readOption *ReadOption, writer io.Writer, offset int64, size int64) (err error) {
|
|
dataSize := int64(n.DataSize)
|
|
if size <= 0 || (offset == 0 && size == dataSize) {
|
|
return v.streamNeedleData(n, readOption, writer, offset, size)
|
|
}
|
|
// Check the CRC before writing any byte of the range, so a damaged
|
|
// replica fails the read and the reader tries another copy.
|
|
crcs, err := v.verifiedNeedleBlocks(n, readOption)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blockSize := rangeCheckBlockSize(dataSize)
|
|
start := offset / blockSize * blockSize
|
|
end := (offset + size + blockSize - 1) / blockSize * blockSize
|
|
if end > dataSize {
|
|
end = dataSize
|
|
}
|
|
checker := &rangeBlockChecker{writer: writer, blockSize: blockSize, crcs: crcs, pos: start, from: offset, to: offset + size, end: end}
|
|
defer checker.free()
|
|
if err = v.streamNeedleData(n, readOption, checker, start, end-start); err != nil {
|
|
return err
|
|
}
|
|
if checker.pos != end {
|
|
return fmt.Errorf("ReadNeedleData: needle %v,%v ended at %d of %d", v.Id, n.Id, checker.pos, end)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *Volume) verifiedNeedleBlocks(n *needle.Needle, readOption *ReadOption) ([]needle.CRC, error) {
|
|
key := verifiedNeedle{v.loadGeneration.Load(), n.Id, n.AppendAtNs, n.Checksum}
|
|
if crcs, ok := verifiedNeedles.Get(key); ok {
|
|
return crcs, nil
|
|
}
|
|
crcs, err, _ := verifyingNeedles.Do(fmt.Sprint(key), func() (any, error) {
|
|
dataSize := int64(n.DataSize)
|
|
blocks := &needleBlockCRCs{blockSize: rangeCheckBlockSize(dataSize)}
|
|
if err := v.streamNeedleData(n, readOption, blocks, 0, dataSize); err != nil {
|
|
return nil, err
|
|
}
|
|
if blocks.pos != dataSize {
|
|
return nil, fmt.Errorf("ReadNeedleData: needle %v,%v ended at %d of %d", v.Id, n.Id, blocks.pos, dataSize)
|
|
}
|
|
if blocks.pos%blocks.blockSize != 0 {
|
|
blocks.crcs = append(blocks.crcs, blocks.crc)
|
|
}
|
|
// A reload during the check may have swapped the data file.
|
|
if v.loadGeneration.Load() == key.loadGeneration {
|
|
verifiedNeedles.Add(key, blocks.crcs)
|
|
}
|
|
return blocks.crcs, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return crcs.([]needle.CRC), nil
|
|
}
|
|
|
|
// needleBlockCRCs records the CRC of each block the verification pass reads.
|
|
type needleBlockCRCs struct {
|
|
blockSize int64
|
|
pos int64
|
|
crc needle.CRC
|
|
crcs []needle.CRC
|
|
}
|
|
|
|
func (b *needleBlockCRCs) Write(p []byte) (int, error) {
|
|
written := len(p)
|
|
for len(p) > 0 {
|
|
k := len(p)
|
|
if left := b.blockSize - b.pos%b.blockSize; int64(k) > left {
|
|
k = int(left)
|
|
}
|
|
b.crc = b.crc.Update(p[:k])
|
|
b.pos += int64(k)
|
|
p = p[k:]
|
|
if b.pos%b.blockSize == 0 {
|
|
b.crcs = append(b.crcs, b.crc)
|
|
b.crc = 0
|
|
}
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
// rangeBlockChecker reads whole blocks around [from, to) and writes the bytes
|
|
// of the range once their block matches the CRC the verification recorded.
|
|
type rangeBlockChecker struct {
|
|
writer io.Writer
|
|
blockSize int64
|
|
crcs []needle.CRC
|
|
pos, from, to int64
|
|
end int64
|
|
crc needle.CRC
|
|
held []byte
|
|
}
|
|
|
|
func (c *rangeBlockChecker) Write(p []byte) (int, error) {
|
|
written := len(p)
|
|
for len(p) > 0 {
|
|
k := len(p)
|
|
if left := c.blockSize - c.pos%c.blockSize; int64(k) > left {
|
|
k = int(left)
|
|
}
|
|
if left := c.end - c.pos; int64(k) > left {
|
|
k = int(left)
|
|
}
|
|
if k <= 0 {
|
|
return 0, fmt.Errorf("ReadNeedleData: %d bytes past the range end %d", len(p), c.end)
|
|
}
|
|
c.crc = c.crc.Update(p[:k])
|
|
if lo, hi := max(c.pos, c.from), c.pos+int64(k); lo < c.to && hi > c.from {
|
|
if hi > c.to {
|
|
hi = c.to
|
|
}
|
|
if c.held == nil {
|
|
c.held = mem.Allocate(int(c.blockSize))[:0]
|
|
}
|
|
c.held = append(c.held, p[lo-c.pos:hi-c.pos]...)
|
|
}
|
|
c.pos += int64(k)
|
|
p = p[k:]
|
|
if c.pos%c.blockSize == 0 || c.pos == c.end {
|
|
block := (c.pos - 1) / c.blockSize
|
|
if block >= int64(len(c.crcs)) || c.crc != c.crcs[block] {
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorCRC).Inc()
|
|
return 0, fmt.Errorf("checksum mismatch in block %d of the needle", block)
|
|
}
|
|
c.crc = 0
|
|
if len(c.held) > 0 {
|
|
if _, err := c.writer.Write(c.held); err != nil {
|
|
return 0, err
|
|
}
|
|
c.held = c.held[:0]
|
|
}
|
|
}
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
func (c *rangeBlockChecker) free() {
|
|
if c.held != nil {
|
|
mem.Free(c.held[:cap(c.held)])
|
|
}
|
|
}
|
|
|
|
func (v *Volume) streamNeedleData(n *needle.Needle, readOption *ReadOption, writer io.Writer, offset int64, size int64) (err error) {
|
|
if !readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RLock()
|
|
defer v.dataFileAccessLock.RUnlock()
|
|
}
|
|
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RLock()
|
|
}
|
|
if err := v.UnavailableError(); err != nil {
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RUnlock()
|
|
}
|
|
return err
|
|
}
|
|
if v.nm == nil {
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RUnlock()
|
|
}
|
|
glog.V(0).Infof("volume %d: needle map not loaded; read returns not-found", v.Id)
|
|
return ErrorNotFound
|
|
}
|
|
nv, ok := v.nm.Get(n.Id)
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RUnlock()
|
|
}
|
|
|
|
if !ok || nv.Offset.IsZero() {
|
|
return ErrorNotFound
|
|
}
|
|
readSize := nv.Size
|
|
if readSize.IsDeleted() {
|
|
if readOption != nil && readOption.ReadDeleted && readSize != TombstoneFileSize {
|
|
glog.V(3).Infof("reading deleted %s", n.String())
|
|
readSize = -readSize
|
|
} else {
|
|
return ErrorDeleted
|
|
}
|
|
}
|
|
if readSize == 0 {
|
|
return nil
|
|
}
|
|
// Stream the record the metadata came from: the index may already point
|
|
// at a replacement, whose bytes must not go out under the old headers.
|
|
if !readOption.metaOffset.IsZero() {
|
|
nv = &needle_map.NeedleValue{Key: n.Id, Offset: readOption.metaOffset, Size: n.Size}
|
|
}
|
|
|
|
actualOffset := nv.Offset.ToActualOffset()
|
|
if readOption.IsOutOfRange {
|
|
actualOffset += int64(MaxPossibleVolumeSize)
|
|
}
|
|
|
|
buf := mem.Allocate(min(readOption.ReadBufferSize, int(size)))
|
|
// A full-needle read holds the last page back in `pending` until the CRC
|
|
// matches; `spare` is a second buffer swapped in so the held page is not
|
|
// overwritten while streaming.
|
|
var spare []byte
|
|
defer func() {
|
|
mem.Free(buf)
|
|
if spare != nil {
|
|
mem.Free(spare)
|
|
}
|
|
}()
|
|
|
|
// The header is not part of the streamed body, so verify the indexed
|
|
// offset really holds this needle before sending another needle's data.
|
|
// A slow read dropped the access lock after the index lookup; take it
|
|
// back for the probe so a compaction commit cannot swap the backend
|
|
// under it, and resolve the offset the way the streaming loop does.
|
|
probeHeader := func() error {
|
|
if readOption.VolumeRevision != v.SuperBlock.CompactionRevision {
|
|
if v.nm == nil {
|
|
return ErrorNotFound
|
|
}
|
|
refreshed, found := v.nm.Get(n.Id)
|
|
if err := v.verifyMovedRecord(n, refreshed, found, readOption); err != nil {
|
|
return err
|
|
}
|
|
nv = refreshed
|
|
actualOffset = nv.Offset.ToActualOffset()
|
|
if readOption.IsOutOfRange {
|
|
actualOffset += int64(MaxPossibleVolumeSize)
|
|
}
|
|
readOption.VolumeRevision = v.SuperBlock.CompactionRevision
|
|
readOption.metaOffset = nv.Offset
|
|
}
|
|
header := make([]byte, NeedleHeaderSize)
|
|
headerCount, headerErr := v.DataBackend.ReadAt(header, actualOffset)
|
|
if headerErr == io.EOF && headerCount == NeedleHeaderSize {
|
|
headerErr = nil
|
|
}
|
|
if headerErr != nil {
|
|
if headerErr != io.EOF {
|
|
v.checkReadWriteError(headerErr)
|
|
}
|
|
return fmt.Errorf("ReadNeedleData header: %w", headerErr)
|
|
}
|
|
probe := new(needle.Needle)
|
|
probe.ParseNeedleHeader(header)
|
|
if probe.Id != n.Id && probe.Size != nv.Size {
|
|
glog.V(0).Infof("volume %d index entry for needle %x resolves to needle %x; reporting not-found",
|
|
v.Id, uint64(n.Id), uint64(probe.Id))
|
|
return ErrorNotFound
|
|
}
|
|
return nil
|
|
}
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RLock()
|
|
}
|
|
probeErr := probeHeader()
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RUnlock()
|
|
}
|
|
if probeErr != nil {
|
|
return probeErr
|
|
}
|
|
|
|
// read needle data
|
|
crc := needle.CRC(0)
|
|
var pending []byte
|
|
checkCRC := offset == 0 && size == int64(n.DataSize)
|
|
for x := offset; x < offset+size; x += int64(len(buf)) {
|
|
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RLock()
|
|
if err := v.UnavailableError(); err != nil {
|
|
v.dataFileAccessLock.RUnlock()
|
|
return err
|
|
}
|
|
}
|
|
// possibly re-read needle offset if volume is compacted
|
|
if readOption.VolumeRevision != v.SuperBlock.CompactionRevision {
|
|
if v.nm == nil {
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RUnlock()
|
|
}
|
|
glog.V(0).Infof("volume %d: needle map not loaded mid-read", v.Id)
|
|
return ErrorNotFound
|
|
}
|
|
// the volume is compacted
|
|
nv, ok = v.nm.Get(n.Id)
|
|
if err := v.verifyMovedRecord(n, nv, ok, readOption); err != nil {
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RUnlock()
|
|
}
|
|
return err
|
|
}
|
|
actualOffset = nv.Offset.ToActualOffset()
|
|
if readOption.IsOutOfRange {
|
|
actualOffset += int64(MaxPossibleVolumeSize)
|
|
}
|
|
readOption.VolumeRevision = v.SuperBlock.CompactionRevision
|
|
readOption.metaOffset = nv.Offset
|
|
}
|
|
count, err := n.ReadNeedleData(v.DataBackend, actualOffset, buf, x)
|
|
if readOption.HasSlowRead {
|
|
v.dataFileAccessLock.RUnlock()
|
|
}
|
|
// Thread the underlying read error through the EIO tracker.
|
|
// Without this, large/range GETs through readNeedleDataInto
|
|
// would never trip IoErrorTolerance even on a failing disk.
|
|
// io.EOF is treated as a clean end-of-stream below, not an
|
|
// error.
|
|
if err != nil && err != io.EOF {
|
|
v.checkReadWriteError(err)
|
|
}
|
|
|
|
toWrite := min(count, int(offset+size-x))
|
|
if toWrite > 0 {
|
|
crc = crc.Update(buf[0:toWrite])
|
|
// The CRC is known only after the last byte; hold each page until
|
|
// the next one is read so a bad needle is never fully written.
|
|
if checkCRC {
|
|
if pending != nil {
|
|
if _, err = writer.Write(pending); err != nil {
|
|
return fmt.Errorf("ReadNeedleData write: %w", err)
|
|
}
|
|
}
|
|
pending = buf[:toWrite]
|
|
if x+int64(len(buf)) < offset+size {
|
|
if spare == nil {
|
|
spare = mem.Allocate(len(buf))
|
|
}
|
|
buf, spare = spare, buf
|
|
}
|
|
} else if _, err = writer.Write(buf[0:toWrite]); err != nil {
|
|
return fmt.Errorf("ReadNeedleData write: %w", err)
|
|
}
|
|
}
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
err = nil
|
|
break
|
|
}
|
|
return fmt.Errorf("ReadNeedleData: %w", err)
|
|
}
|
|
if count <= 0 {
|
|
break
|
|
}
|
|
}
|
|
if checkCRC && (n.Checksum != crc && uint32(n.Checksum) != crc.Value()) {
|
|
// the crc.Value() function is to be deprecated. this double checking is for backward compatibility
|
|
// with seaweed version using crc.Value() instead of uint32(crc), which appears in commit 056c480eb
|
|
// and switch appeared in version 3.09.
|
|
// pending is the last page and is intentionally not written.
|
|
stats.VolumeServerHandlerCounter.WithLabelValues(stats.ErrorCRC).Inc()
|
|
return fmt.Errorf("ReadNeedleData checksum %v expected %v for Needle: %v,%v", crc, n.Checksum, v.Id, n)
|
|
}
|
|
// The read succeeded only once the checksum passes; a failed needle
|
|
// leaves the io error streak untouched, matching the streamed path. A
|
|
// ranged read's verification pass leaves it alone too: the range read
|
|
// that follows reports the outcome.
|
|
if _, verifying := writer.(*needleBlockCRCs); !verifying {
|
|
v.checkReadWriteError(nil)
|
|
}
|
|
if pending != nil {
|
|
if _, err = writer.Write(pending); err != nil {
|
|
return fmt.Errorf("ReadNeedleData write: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
func min(x, y int) int {
|
|
if x < y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
// read fills in Needle content by looking up n.Id from NeedleMapper
|
|
func (v *Volume) ReadNeedleBlob(offset int64, size Size) ([]byte, error) {
|
|
// A deletion marker is not a record length; reject it before taking the lock.
|
|
if size.IsDeleted() {
|
|
return nil, fmt.Errorf("invalid needle size %d", size)
|
|
}
|
|
|
|
v.dataFileAccessLock.RLock()
|
|
defer v.dataFileAccessLock.RUnlock()
|
|
|
|
if err := v.UnavailableError(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blob, err := needle.ReadNeedleBlob(v.DataBackend, offset, size, v.Version())
|
|
v.checkReadWriteError(err)
|
|
return blob, err
|
|
}
|
|
|
|
type VolumeFileScanner interface {
|
|
VisitSuperBlock(super_block.SuperBlock) error
|
|
ReadNeedleBody() bool
|
|
VisitNeedle(n *needle.Needle, offset int64, needleHeader, needleBody []byte) error
|
|
}
|
|
|
|
func ScanVolumeFile(dirname string, collection string, id needle.VolumeId,
|
|
needleMapKind NeedleMapKind,
|
|
volumeFileScanner VolumeFileScanner) (err error) {
|
|
var v *Volume
|
|
if v, err = loadVolumeWithoutIndex(dirname, collection, id, needleMapKind, needle.GetCurrentVersion()); err != nil {
|
|
return fmt.Errorf("failed to load volume %d: %w", id, err)
|
|
}
|
|
if err = volumeFileScanner.VisitSuperBlock(v.SuperBlock); err != nil {
|
|
return fmt.Errorf("failed to process volume %d super block: %w", id, err)
|
|
}
|
|
defer v.Close()
|
|
|
|
version := v.Version()
|
|
|
|
offset := int64(v.SuperBlock.BlockSize())
|
|
|
|
return ScanVolumeFileFrom(version, v.DataBackend, offset, volumeFileScanner)
|
|
}
|
|
|
|
func ScanVolumeFileFrom(version needle.Version, datBackend backend.BackendStorageFile, offset int64, volumeFileScanner VolumeFileScanner) (err error) {
|
|
n, nh, rest, e := needle.ReadNeedleHeader(datBackend, version, offset)
|
|
if e != nil {
|
|
if e == io.EOF {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("cannot read %s at offset %d: %w", datBackend.Name(), offset, e)
|
|
}
|
|
for n != nil {
|
|
var needleBody []byte
|
|
if volumeFileScanner.ReadNeedleBody() {
|
|
// println("needle", n.Id.String(), "offset", offset, "size", n.Size, "rest", rest)
|
|
if needleBody, err = n.ReadNeedleBody(datBackend, version, offset+NeedleHeaderSize, rest); err != nil {
|
|
glog.V(0).Infof("cannot read needle head [%d, %d) body [%d, %d) body length %d: %v", offset, offset+NeedleHeaderSize, offset+NeedleHeaderSize, offset+NeedleHeaderSize+rest, rest, err)
|
|
// err = fmt.Errorf("cannot read needle body: %v", err)
|
|
// return
|
|
}
|
|
}
|
|
err := volumeFileScanner.VisitNeedle(n, offset, nh, needleBody)
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
glog.V(0).Infof("visit needle error: %v", err)
|
|
return fmt.Errorf("visit needle error: %w", err)
|
|
}
|
|
// A corrupt header can carry a size so negative that the record length
|
|
// is zero or less; the scan cannot advance past it.
|
|
recordSize := NeedleHeaderSize + rest
|
|
if recordSize <= 0 {
|
|
return fmt.Errorf("%s: needle header at offset %d has size %d, record length %d: %w", datBackend.Name(), offset, n.Size, recordSize, needle.ErrorCorrupted)
|
|
}
|
|
offset += recordSize
|
|
glog.V(4).Infof("==> new entry offset %d", offset)
|
|
if n, nh, rest, err = needle.ReadNeedleHeader(datBackend, version, offset); err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("cannot read needle header at offset %d: %w", offset, err)
|
|
}
|
|
glog.V(4).Infof("new entry needle size:%d rest:%d", n.Size, rest)
|
|
}
|
|
return nil
|
|
}
|