package storage import ( "bytes" "errors" "fmt" "os" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/storage/backend" "github.com/seaweedfs/seaweedfs/weed/storage/needle" . "github.com/seaweedfs/seaweedfs/weed/storage/types" ) var ErrorNotFound = errors.New("not found") var ErrorDeleted = errors.New("already deleted") var ErrorSizeMismatch = errors.New("size mismatch") type batchNeedleSnapshot struct { id NeedleId found bool offset Offset size Size changed bool } func newBatchNeedleSnapshot(v *Volume, id NeedleId) *batchNeedleSnapshot { snapshot := &batchNeedleSnapshot{id: id} if element, found := v.nm.Get(id); found && element != nil { snapshot.found = true snapshot.offset = element.Offset snapshot.size = element.Size } return snapshot } func (s *batchNeedleSnapshot) observeCurrent(v *Volume) { element, found := v.nm.Get(s.id) if found != s.found { s.changed = true return } if found && (element == nil || element.Offset != s.offset || element.Size != s.size) { s.changed = true } } func (v *Volume) restoreBatchNeedle(snapshot *batchNeedleSnapshot) error { mapRollbacker, ok := v.nm.(batchMapRollbacker) if !ok { return fmt.Errorf("mapper %T cannot restore its mappings", v.nm) } if snapshot.found { return mapRollbacker.restoreMapping(snapshot.id, snapshot.offset, snapshot.size) } // A plain Delete would leave a tombstoned entry whose stale offset makes // the next write to this needle fail reading a header that no longer exists. return mapRollbacker.removeMapping(snapshot.id) } func (v *Volume) rollbackBatch(end int64, indexEnd int64, snapshots []*batchNeedleSnapshot, metricRollbacker batchMetricRollbacker, metrics batchMapMetricSnapshot) error { var recoveryErrors []error for _, snapshot := range snapshots { if !snapshot.changed { continue } if err := v.restoreBatchNeedle(snapshot); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("restore needle %d: %w", snapshot.id, err)) } } if len(recoveryErrors) == 0 { rollbacker, ok := v.nm.(batchIndexRollbacker) if !ok { recoveryErrors = append(recoveryErrors, fmt.Errorf("mapper %T cannot truncate its index", v.nm)) } else if err := rollbacker.truncateIndex(indexEnd); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("truncate index to %d: %w", indexEnd, err)) } } if len(recoveryErrors) == 0 { if err := v.nm.Sync(); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("sync recovered index: %w", err)) } } if len(recoveryErrors) == 0 { if err := v.DataBackend.Truncate(end); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("truncate %s to %d: %w", v.DataBackend.Name(), end, err)) } else if err := v.DataBackend.Sync(); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("sync truncated %s: %w", v.DataBackend.Name(), err)) } } if len(recoveryErrors) == 0 { if metricRollbacker == nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("mapper %T cannot restore its metrics", v.nm)) } else { metricRollbacker.restoreBatchMetrics(metrics) } } return errors.Join(recoveryErrors...) } // isFileUnchanged checks whether this needle to write is same as last one. // It requires serialized access in the same volume. func (v *Volume) isFileUnchanged(n *needle.Needle) bool { if v.Ttl.String() != "" { return false } nv, ok := v.nm.Get(n.Id) if ok && !nv.Offset.IsZero() && nv.Size.IsValid() { oldNeedle := new(needle.Needle) err := oldNeedle.ReadData(v.DataBackend, nv.Offset.ToActualOffset(), nv.Size, v.Version()) if err != nil { glog.V(0).Infof("Failed to check updated file at offset %d size %d: %v", nv.Offset.ToActualOffset(), nv.Size, err) return false } if oldNeedle.Cookie == n.Cookie && oldNeedle.Checksum == n.Checksum && bytes.Equal(oldNeedle.Data, n.Data) { n.DataSize = oldNeedle.DataSize return true } } return false } var ErrVolumeNotEmpty = fmt.Errorf("volume not empty") // Destroy removes everything related to this volume. When keepRemoteData is // true the cloud-tier object backing the volume is left intact — used by // moves where another server is taking over the same .vif. func (v *Volume) Destroy(onlyEmpty bool, keepRemoteData bool) (err error) { v.dataFileAccessLock.Lock() defer v.dataFileAccessLock.Unlock() if onlyEmpty { isEmpty, e := v.doIsEmpty() if e != nil { err = fmt.Errorf("failed to read isEmpty %v", e) return } if !isEmpty { err = ErrVolumeNotEmpty return } } if !v.isCompactionInProgress.CompareAndSwap(false, true) { err = fmt.Errorf("volume %d is compacting", v.Id) return } v.stopWorker() if !keepRemoteData { storageName, storageKey := v.RemoteStorageNameKey() if v.HasRemoteFile() && storageName != "" && storageKey != "" { if backendStorage, found := backend.BackendStorages[storageName]; found { backendStorage.DeleteFile(storageKey) } } } // A regular volume and an EC volume for the same id share .vif. When // EC artefacts coexist on this disk (e.g. shards distributed onto a source // replica before it is deleted), keep the .vif so removing the regular // volume does not strip the EC volume's info file. keepVif := v.sharesVifWithEcVolume() v.doClose() removeVolumeFiles(v.DataFileName(), keepVif) removeVolumeFiles(v.IndexFileName(), keepVif) return } // sharesVifWithEcVolume reports whether an EC volume for this volume id lives // on the same disk, in which case its .vif is the same file as the regular // volume's and must outlive the regular volume's deletion. func (v *Volume) sharesVifWithEcVolume() bool { if v.location == nil { return false } if _, found := v.location.FindEcVolume(v.Id); found { return true } return v.location.HasEcxFileOnDisk(v.Collection, v.Id) } func removeVolumeFiles(filename string, keepVif bool) { // .dat/.idx removals log at V(0) so destructive calls are traceable. deleteAndLog := func(ext string) { fullFilename := filename + "." + ext st, statErr := os.Stat(fullFilename) err := os.RemoveAll(fullFilename) if err != nil { glog.V(0).Infof("failed to remove volume file %s: %s", fullFilename, err) return } if statErr == nil && (ext == "dat" || ext == "idx") { glog.Infof("removed volume file %s (size=%d)", fullFilename, st.Size()) } } deleteAndLog("dat") deleteAndLog("idx") if !keepVif { deleteAndLog("vif") } // sorted index file deleteAndLog("sdx") // compaction deleteAndLog("cpd") deleteAndLog("cpx") // compaction commit marker deleteAndLog("cpc") // level db index file deleteAndLog("ldb") // redb index file (Rust volume server) deleteAndLog("rdb") // marker for damaged or incomplete volume deleteAndLog("note") // marker for a volume whose failed-batch recovery could not be verified deleteAndLog("unavailable") } // asyncRequestAppend queues a request for the batch worker, starting it on the // first one. It reports false for a destroyed volume, so the caller writes // inline rather than wait on a worker that will never answer. func (v *Volume) asyncRequestAppend(request *needle.AsyncRequest) bool { requests := v.startWorker() if requests == nil { return false } requests <- request return true } func (v *Volume) syncWrite(n *needle.Needle, checkCookie bool, fsync bool) (offset uint64, size Size, isUnchanged bool, err error) { // glog.V(4).Infof("writing needle %s", needle.NewFileIdFromNeedle(v.Id, n).String()) v.dataFileAccessLock.Lock() defer v.dataFileAccessLock.Unlock() if err := v.UnavailableError(); err != nil { return 0, 0, false, err } // A caller can still hold the volume after it was closed or destroyed, which // leaves both of these nil. Refuse the write rather than dereference them. if v.nm == nil || v.DataBackend == nil { return 0, 0, false, fmt.Errorf("volume %d is closed", v.Id) } if !fsync { return v.doWriteRequest(n, checkCookie) } end, _, statErr := v.DataBackend.GetStat() if statErr != nil { return 0, 0, false, fmt.Errorf("cannot read current volume position: %v", statErr) } priorOffset, priorSize, hasPrior := Offset{}, Size(0), false if nv, found := v.nm.Get(n.Id); found { priorOffset, priorSize, hasPrior = nv.Offset, nv.Size, true } offset, size, isUnchanged, err = v.doWriteRequest(n, checkCookie) if err != nil { return } if syncErr := v.DataBackend.Sync(); syncErr != nil { v.checkReadWriteError(syncErr) if !isUnchanged { v.rollbackUnflushedWrite(n, offset, end, priorOffset, priorSize, hasPrior) } return 0, 0, false, syncErr } return } // rollbackUnflushedWrite undoes an append whose fsync failed: the bytes are not // data we can vouch for, so they come back off the .dat and the needle map goes // back to what it pointed at before, rather than at an offset past the new end. func (v *Volume) rollbackUnflushedWrite(n *needle.Needle, offset uint64, end int64, priorOffset Offset, priorSize Size, hasPrior bool) { var recoveryErr error if te := v.DataBackend.Truncate(end); te != nil { recoveryErr = fmt.Errorf("truncate %s back to %d: %w", v.DataBackend.Name(), end, te) } current, found := v.nm.Get(n.Id) if found && current.Offset.ToActualOffset() == int64(offset) { var err error if hasPrior { err = v.nm.Put(n.Id, priorOffset, priorSize) } else { // The tombstone must reach .idx so a replay forgets the needle, but // the negated entry it leaves in memory points at truncated bytes // and would fail the next write, so erase the mapping as well. err = v.nm.Delete(n.Id, ToOffset(int64(offset))) if err == nil { if rb, ok := v.nm.(batchMapRollbacker); ok { err = rb.removeMapping(n.Id) } else { err = fmt.Errorf("mapper %T cannot remove mapping", v.nm) } } } if err != nil { recoveryErr = errors.Join(recoveryErr, fmt.Errorf("roll back the index of needle %d in volume %d: %w", n.Id, v.Id, err)) } } if recoveryErr != nil { v.markIoUnavailable(recoveryErr) } } // writeNeedle2 appends a needle. A durable write normally goes through the // async batch worker, which fsyncs once for the whole batch; while the server // is stopping the worker is winding down, so it is flushed inline instead. Both // paths only return once the .dat is on disk. func (v *Volume) writeNeedle2(n *needle.Needle, checkCookie bool, fsync bool, isStopping bool) (offset uint64, size Size, isUnchanged bool, err error) { // glog.V(4).Infof("writing needle %s", needle.NewFileIdFromNeedle(v.Id, n).String()) if err := v.UnavailableError(); err != nil { return 0, 0, false, err } if n.Ttl == needle.EMPTY_TTL && v.Ttl != needle.EMPTY_TTL { n.SetHasTtl() n.Ttl = v.Ttl } if !fsync || isStopping { return v.syncWrite(n, checkCookie, fsync) } else { asyncRequest := needle.NewAsyncRequest(n, true) // using len(n.Data) here instead of n.Size before n.Size is populated in n.Append() asyncRequest.ActualSize = needle.GetActualSize(Size(len(n.Data)), v.Version()) if !v.asyncRequestAppend(asyncRequest) { return v.syncWrite(n, checkCookie, fsync) } offset, _, isUnchanged, err = asyncRequest.WaitComplete() return } } func (v *Volume) doWriteRequest(n *needle.Needle, checkCookie bool) (offset uint64, size Size, isUnchanged bool, err error) { // glog.V(4).Infof("writing needle %s", needle.NewFileIdFromNeedle(v.Id, n).String()) if v.isFileUnchanged(n) { size = Size(n.DataSize) isUnchanged = true return } // check whether existing needle cookie matches nv, ok := v.nm.Get(n.Id) if ok { existingNeedle, _, _, existingNeedleReadErr := needle.ReadNeedleHeader(v.DataBackend, v.Version(), nv.Offset.ToActualOffset()) if existingNeedleReadErr != nil { err = fmt.Errorf("reading existing needle: %w", existingNeedleReadErr) return } if n.Cookie == 0 && !checkCookie { // this is from batch deletion, and read back again when tailing a remote volume // which only happens when checkCookie == false and fsync == false n.Cookie = existingNeedle.Cookie } if existingNeedle.Cookie != n.Cookie { glog.V(0).Infof("write cookie mismatch: existing %s, new %s", needle.NewFileIdFromNeedle(v.Id, existingNeedle), needle.NewFileIdFromNeedle(v.Id, n)) err = fmt.Errorf("mismatching cookie %x", n.Cookie) return } } // append to dat file n.UpdateAppendAtNs(v.lastAppendAtNs) var actualSize int64 offset, size, actualSize, err = n.Append(v.DataBackend, v.Version()) v.checkReadWriteError(err) if err != nil { err = fmt.Errorf("append to volume %d size %d actualSize %d: %v", v.Id, size, actualSize, err) return } v.lastAppendAtNs = n.AppendAtNs // add to needle map if !ok || uint64(nv.Offset.ToActualOffset()) < offset { if err = v.nm.Put(n.Id, ToOffset(int64(offset)), n.Size); err != nil { err = fmt.Errorf("index needle %d of volume %d at offset %d: %w", n.Id, v.Id, offset, err) glog.V(0).Info(err) } } if v.lastModifiedTsSeconds < n.LastModified { v.lastModifiedTsSeconds = n.LastModified } return } func (v *Volume) syncDelete(n *needle.Needle) (Size, error) { // glog.V(4).Infof("delete needle %s", needle.NewFileIdFromNeedle(v.Id, n).String()) v.dataFileAccessLock.Lock() defer v.dataFileAccessLock.Unlock() if err := v.UnavailableError(); err != nil { return 0, err } if v.nm == nil { return 0, nil } return v.doDeleteRequest(n) } func (v *Volume) deleteNeedle2(n *needle.Needle) (Size, error) { // todo: delete info is always appended no fsync, it may need fsync in future fsync := false if !fsync { return v.syncDelete(n) } else { asyncRequest := needle.NewAsyncRequest(n, false) asyncRequest.ActualSize = needle.GetActualSize(0, v.Version()) if !v.asyncRequestAppend(asyncRequest) { return v.syncDelete(n) } _, size, _, err := asyncRequest.WaitComplete() return Size(size), err } } func (v *Volume) doDeleteRequest(n *needle.Needle) (Size, error) { glog.V(4).Infof("delete needle %s", needle.NewFileIdFromNeedle(v.Id, n).String()) nv, ok := v.nm.Get(n.Id) // fmt.Println("key", n.Id, "volume offset", nv.Offset, "data_size", n.Size, "cached size", nv.Size) if ok && !nv.Size.IsDeleted() { var offset uint64 var err error size := nv.Size if !v.HasRemoteFile() { n.Data = nil n.UpdateAppendAtNs(v.lastAppendAtNs) offset, _, _, err = n.Append(v.DataBackend, v.Version()) v.checkReadWriteError(err) if err != nil { return size, err } } v.lastAppendAtNs = n.AppendAtNs if err = v.nm.Delete(n.Id, ToOffset(int64(offset))); err != nil { return size, err } return size, err } return 0, nil } func (v *Volume) processBatch(currentRequests []*needle.AsyncRequest) { v.dataFileAccessLock.Lock() defer v.dataFileAccessLock.Unlock() end, e := int64(0), error(nil) if unavailableErr := v.UnavailableError(); unavailableErr != nil { e = unavailableErr } else if v.nm == nil || v.DataBackend == nil { e = fmt.Errorf("volume %d is closed", v.Id) } else { end, _, e = v.DataBackend.GetStat() } if e != nil { for i := 0; i < len(currentRequests); i++ { currentRequests[i].Complete(0, 0, false, fmt.Errorf("cannot read current volume position: %v", e)) } return } batchSnapshots := make(map[NeedleId]*batchNeedleSnapshot, len(currentRequests)) orderedSnapshots := make([]*batchNeedleSnapshot, 0, len(currentRequests)) metricRollbacker, _ := v.nm.(batchMetricRollbacker) var batchMetrics batchMapMetricSnapshot if metricRollbacker != nil { batchMetrics = metricRollbacker.snapshotBatchMetrics() } indexEnd := int64(v.nm.IndexFileSize()) batchLastAppendAtNs := v.lastAppendAtNs batchLastModifiedTsSeconds := v.lastModifiedTsSeconds for i := 0; i < len(currentRequests); i++ { needleID := currentRequests[i].N.Id snapshot, found := batchSnapshots[needleID] if !found { snapshot = newBatchNeedleSnapshot(v, needleID) batchSnapshots[needleID] = snapshot orderedSnapshots = append(orderedSnapshots, snapshot) } if currentRequests[i].IsWriteRequest { offset, size, isUnchanged, err := v.doWriteRequest(currentRequests[i].N, true) currentRequests[i].UpdateResult(offset, uint64(size), isUnchanged, err) } else { size, err := v.doDeleteRequest(currentRequests[i].N) currentRequests[i].UpdateResult(0, uint64(size), false, err) } snapshot.observeCurrent(v) } // if sync error the batch is not durable; restore it before another // operation observes the volume if syncErr := v.DataBackend.Sync(); syncErr != nil { v.checkReadWriteError(syncErr) v.lastAppendAtNs = batchLastAppendAtNs v.lastModifiedTsSeconds = batchLastModifiedTsSeconds batchErr := syncErr if recoveryErr := v.rollbackBatch(end, indexEnd, orderedSnapshots, metricRollbacker, batchMetrics); recoveryErr != nil { batchErr = errors.Join(syncErr, recoveryErr) v.markIoUnavailable(batchErr) } for i := 0; i < len(currentRequests); i++ { currentRequests[i].UpdateResult(0, 0, false, batchErr) } } for i := 0; i < len(currentRequests); i++ { currentRequests[i].Submit() } } // startWorker returns the volume's batch-write channel, creating it and its // goroutine on first use, and nil once stopWorker has run. func (v *Volume) startWorker() chan *needle.AsyncRequest { v.asyncWorkerLock.Lock() defer v.asyncWorkerLock.Unlock() if v.asyncWorkerClosed { return nil } if v.asyncRequestsChan != nil { return v.asyncRequestsChan } requests := make(chan *needle.AsyncRequest, 128) v.asyncRequestsChan = requests go func() { chanClosed := false for { // chan closed. go thread will exit if chanClosed { break } currentRequests := make([]*needle.AsyncRequest, 0, 128) currentBytesToWrite := int64(0) for { request, ok := <-requests // volume may be closed if !ok { chanClosed = true break } if MaxPossibleVolumeSize < v.ContentSize()+uint64(currentBytesToWrite+request.ActualSize) { request.Complete(0, 0, false, fmt.Errorf("volume size limit %d exceeded! current size is %d", MaxPossibleVolumeSize, v.ContentSize())) break } currentRequests = append(currentRequests, request) currentBytesToWrite += request.ActualSize // submit at most 4M bytes or 128 requests at one time to decrease request delay. // it also need to break if there is no data in channel to avoid io hang. if currentBytesToWrite >= 4*1024*1024 || len(currentRequests) >= 128 || len(requests) == 0 { break } } if len(currentRequests) == 0 { continue } v.processBatch(currentRequests) } }() return requests } // stopWorker closes the batch-write channel so the worker drains what is queued // and exits. It stays closed: a destroyed volume takes no more writes. func (v *Volume) stopWorker() { v.asyncWorkerLock.Lock() defer v.asyncWorkerLock.Unlock() if v.asyncWorkerClosed { return } v.asyncWorkerClosed = true if v.asyncRequestsChan != nil { close(v.asyncRequestsChan) v.asyncRequestsChan = nil } } func (v *Volume) WriteNeedleBlob(needleId NeedleId, needleBlob []byte, size Size) error { v.dataFileAccessLock.Lock() defer v.dataFileAccessLock.Unlock() if err := v.UnavailableError(); err != nil { return err } // nm.Put on a read-only volume fails only after the blob is appended to .dat. if v.IsReadOnly() { return fmt.Errorf("volume %d is read only", v.Id) } if size.IsDeleted() { return fmt.Errorf("needle %d has invalid size %d", needleId, size) } // size indexes the needle and places the v3 append timestamp, so a caller using // the payload-only DataSize corrupts both, silently until the needle is read back. if len(needleBlob) < NeedleHeaderSize { return fmt.Errorf("needle %d blob of %d bytes is shorter than a needle header", needleId, len(needleBlob)) } var blobHeader needle.Needle blobHeader.ParseNeedleHeader(needleBlob) if blobHeader.Size != size { return fmt.Errorf("needle %d size %d does not match its blob header size %d", needleId, size, blobHeader.Size) } // The blob is appended as is: its length must be the record this size takes in this volume's version. if actualSize := needle.GetActualSize(size, v.Version()); int64(len(needleBlob)) != actualSize { return fmt.Errorf("needle %d blob of %d bytes does not match the %d bytes size %d takes in a version %d volume", needleId, len(needleBlob), actualSize, size, v.Version()) } if MaxPossibleVolumeSize < v.nm.ContentSize()+uint64(len(needleBlob)) { return fmt.Errorf("volume size limit %d exceeded! current size is %d", MaxPossibleVolumeSize, v.nm.ContentSize()) } nv, ok := v.nm.Get(needleId) if ok && nv.Size == size { oldNeedle := new(needle.Needle) err := oldNeedle.ReadData(v.DataBackend, nv.Offset.ToActualOffset(), nv.Size, v.Version()) if err == nil { newNeedle := new(needle.Needle) err = newNeedle.ReadBytes(needleBlob, nv.Offset.ToActualOffset(), size, v.Version()) if err == nil && oldNeedle.Cookie == newNeedle.Cookie && oldNeedle.Checksum == newNeedle.Checksum && bytes.Equal(oldNeedle.Data, newNeedle.Data) { glog.V(0).Infof("needle %v already exists", needleId) return nil } } } appendAtNs := needle.GetAppendAtNs(v.lastAppendAtNs) offset, err := needle.WriteNeedleBlob(v.DataBackend, needleBlob, size, appendAtNs, v.Version()) v.checkReadWriteError(err) if err != nil { return err } v.lastAppendAtNs = appendAtNs // add to needle map if err = v.nm.Put(needleId, ToOffset(int64(offset)), size); err != nil { err = fmt.Errorf("index needle %d of volume %d at offset %d: %w", needleId, v.Id, offset, err) glog.V(0).Info(err) } return err }