mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-29 19:25:35 +00:00
* shell: let volume.deleteEmpty drop volumes with no live needles The candidate check only accepted a .dat at superblock size, so a volume whose every needle was deleted still had to be vacuumed first — minutes of compaction to rewrite bytes that were all garbage anyway. FileCount counts every indexed entry and DeleteCount every entry made garbage by overwrite or delete, so FileCount <= DeleteCount means nothing live remains and the volume can be unlinked directly. The quietFor guard is unchanged. * volume server: add only_garbage VolumeDelete guard VolumeDelete(only_empty) refuses every volume that ever held data, so a volume whose needles are all deleted could only be removed after a vacuum rewrote it. The new only_garbage flag deletes only when the byte counters show nothing live: DeletedSize covering all of ContentSize, the same all-garbage state vacuum measures. Byte counters are used because the file/delete counts drift on index reload. * rust volume: mirror only_garbage VolumeDelete guard Same check as the Go server: a volume deletes under only_garbage when its deleted bytes cover all content bytes. The grpc handler rejects before the store drops the volume from its map, since destroy errors after removal would still unmount it. * volume delete: let either enabled check pass, keep onlyEmpty on the wire An upgraded shell sending only_garbage to a pre-upgrade server would be read as an unconditional delete (field ignored, only_empty false). The request now keeps only_empty set so old servers check emptiness and refuse, while new servers delete when either check passes. * volume.deleteEmpty: skip remote-backed and protected read-only volumes A remote-tiered replica shares its cloud object with the other replicas, so keepRemoteData=false on one delete removes data they still reference. Protected read-only volumes are quarantined or under maintenance, which is exactly when a replica should not be dropped. * volume delete: validate guarded copies across disks before deleting * volume delete: hold copy locks across guarded validate-and-delete CheckVolumeDeletable released each copy's locks before Destroy ran, so a write landing on a later copy between the two passes refused its destroy after earlier copies were already removed. Pin every copy's dataFileAccessLock (and its location's volumesLock) across validation and removal so a refused delete leaves all copies intact. * volume delete: send deleted-volume notices after releasing locks A blocking send on a full DeletedVolumesChan under volumesLock can stall the heartbeat loop that drains it while it waits on the same locks. Collect the notices under the lock span and send after release. * pb: restore generated-file cosmetics to match the repo's protoc version Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
685 lines
22 KiB
Go
685 lines
22 KiB
Go
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, onlyGarbage bool, keepRemoteData bool) (err error) {
|
|
v.dataFileAccessLock.Lock()
|
|
defer v.dataFileAccessLock.Unlock()
|
|
return v.destroyLocked(onlyEmpty, onlyGarbage, keepRemoteData)
|
|
}
|
|
|
|
// destroyLocked is Destroy for callers already holding dataFileAccessLock,
|
|
// e.g. a guarded multi-copy delete that pins every copy under one lock span
|
|
// so validation and removal cannot be split by a write.
|
|
func (v *Volume) destroyLocked(onlyEmpty bool, onlyGarbage bool, keepRemoteData bool) (err error) {
|
|
if err = v.checkDeletableLocked(onlyEmpty, onlyGarbage); err != nil {
|
|
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 <base>.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
|
|
}
|
|
|
|
// checkDeletableLocked enforces the guards Destroy applies before removing
|
|
// any file: either enabled check may pass, since a volume with no live data
|
|
// qualifies whether it reads empty or as all garbage.
|
|
func (v *Volume) checkDeletableLocked(onlyEmpty bool, onlyGarbage bool) (err error) {
|
|
if !onlyEmpty && !onlyGarbage {
|
|
return nil
|
|
}
|
|
emptyOk := false
|
|
if onlyEmpty {
|
|
isEmpty, e := v.doIsEmpty()
|
|
if e != nil {
|
|
return fmt.Errorf("failed to read isEmpty %v", e)
|
|
}
|
|
emptyOk = isEmpty
|
|
}
|
|
if !emptyOk && !(onlyGarbage && v.doIsGarbage()) {
|
|
return ErrVolumeNotEmpty
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|