Files
seaweedfs/weed/storage/volume_write.go
T
b9ad62fc16 [Volume] Keep DAT and index state consistent after async batch Sync failure (#11425)
* fix 11400

* persist failed-recovery quarantine and harden rollback

- record the unavailable state in a .unavailable marker, fsync it, and
  re-arm it on load so a restart cannot serve an unverified pair
- quarantine the volume so heartbeats stop advertising it
- block MarkVolumeWritable while unavailable, rechecked under noWriteLock
- fail every request of a failed batch, not only the succeeded ones
- restore the needle map and truncate .dat on inline fsync rollback failure
- add truncateIndex for the sorted-file needle map
- mirror the fail-closed semantics in the Rust volume server

* volume: erase rolled-back mappings instead of leaving tombstones

A rolled-back batch or failed inline write used Delete() to undo a
needle that did not exist beforehand, leaving a tombstoned map entry
whose stale offset makes the next write to that needle fail reading a
header that no longer exists. Add removeMapping/restoreMapping to the
mappers so recovery erases entries that were absent before the batch
and reinstates the exact prior offset/size for ones that were,
including tombstones. The index row still goes through Delete so a
replay forgets the needle.

* volume: gate bulk readers on unavailable and fsync the marker's dir

- fsync_dir(&self.dir) synced the volume dir's parent, not the dir
  holding .unavailable; pass the marker path so the create survives
  a host crash
- export UnavailableError and check it in ReadAllNeedles,
  VolumeTailSender, VolumeIncrementalCopy, and IncrementalBackup so
  replica-sync paths cannot stream or append data from an unverified
  .dat/.idx pair; mirror on the Rust side via read_dat_slice,
  read_all_needles, dat_scan_plan, and the incremental-copy handler

* volume: drop issue references from comments near touched code

* volume: stop active scans when the volume becomes unavailable

The stream entry-point checks ran once per RPC, so a volume quarantined
by a failed recovery mid-scan kept serving data. Recheck availability
per needle/chunk on the detached read paths: tail scan and heartbeat,
read-all, incremental copy, incremental backup writes, and the Rust
StreamingBody chunk reads. Rust incremental copy also rejects a
quarantined volume before sync_to_disk touches the backend.

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-09-24 06:57:44 +08:00

666 lines
21 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, 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 <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
}
// 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
}