Files
seaweedfs/weed/storage/volume_write.go
T
10b0f2b8ad volume server: refuse the rest of a grouped run after a durable index failure (#11576)
* volume server: refuse the rest of a grouped run after a durable index failure

A durable write whose needle-map put fails stops the volume taking writes
(#10825): sent on its own, the next write then fails read only before it
appends. The grouped run from #11543 appends and syncs every entry before
publishing any, then kept publishing the entries after the failed one and
acked them once the shared .idx sync went through. When the failed put
tore its .idx row, the rows appended after it land off alignment, so the
next load parses them as garbage and the acked writes are gone.

Once a durable entry fails to publish, refuse every later entry of the run
with ReadOnly, as the per-needle path does. The entries before it stay
acked; their rows go down with the run's one .idx sync. The refused
records stay on the .dat unindexed, as the failed one does on its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* volume server: refuse a grouped entry staged as a cookie mismatch too

After a durable entry in a grouped run fails to index, the entries
after it are refused as they would be on their own. On its own an entry
meets check_writable before its cookie check, so one staged as a cookie
mismatch now gets the refusal too, instead of keeping its staging error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* volume server: trim a torn .idx row back so the next stays aligned

A failed write_index_entry can leave half a row in the .idx. With the
writer appending at the tail, every row written after it lands off
alignment and the next load parses them as garbage, so a write acked
behind a torn row does not come back. Trim the file back to
idx_file_offset on a failed append, in both needle maps, and cover it
with a test that writes past a torn row and reloads.

* volume server: refuse queued Go writes once a durable index update fails

processBatch kept writing after a failed nm.Put, and the single-write
path checked IsReadOnly only outside the volume lock. A durable write
whose index update fails now marks the volume noWriteOrDelete, and each
queued request is checked before it appends, so the ones after a failed
durable entry are refused the way a lone write is. Deletes get the same
noWriteOrDelete refusal a lone delete gets.

* volume server: refuse appends while a torn .idx row cannot be trimmed

When trimming back a half-written .idx row itself fails, the next append
would land after the torn bytes and every later row would parse off
alignment on load. Latch the map as torn and refuse appends until the
trim succeeds, on both CompactNeedleMap and RedbNeedleMap; the same
latch covers an orphan row that could not be trimmed after a failed
redb commit.

The .idx writer is now opened with write+append access so truncate_to
(set_len) works on Windows, where an append-only handle cannot trim.

* volume server: write .idx rows at idx_file_offset, not via append mode

Rust's OpenOptions on Windows strips FILE_WRITE_DATA whenever append is
set so the handle stays strictly append-only, which makes set_len fail -
the torn-row trim could never succeed there. Open the .idx writer with
plain write access and seek to idx_file_offset before each row, the same
positioned-write model the Go server uses.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-10-03 21:04:30 +08:00

714 lines
23 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
}
if v.IsReadOnly() {
return 0, 0, false, fmt.Errorf("volume %d is read only", v.Id)
}
// 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, fsync)
}
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, fsync)
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, fsync 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 fsync {
// The record is down but nothing indexes it. Stop taking
// writes rather than append past it, same as the Rust
// volume server.
v.noWriteLock.Lock()
v.noWriteOrDelete = true
v.noWriteLock.Unlock()
}
}
}
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 _, noWriteOrDelete, _, _ := v.ReadOnlyReasons(); noWriteOrDelete {
return 0, fmt.Errorf("volume %d is read only", v.Id)
}
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)
}
// Every queued request meets the same refusal it would get sent on
// its own: once a durable index update fails the volume stops taking
// writes, and a lone write is turned away before it appends.
if currentRequests[i].IsWriteRequest {
if unavailableErr := v.UnavailableError(); unavailableErr != nil {
currentRequests[i].UpdateResult(0, 0, false, unavailableErr)
} else if v.IsReadOnly() {
currentRequests[i].UpdateResult(0, 0, false, fmt.Errorf("volume %d is read only", v.Id))
} else {
offset, size, isUnchanged, err := v.doWriteRequest(currentRequests[i].N, true, true)
currentRequests[i].UpdateResult(offset, uint64(size), isUnchanged, err)
}
} else {
if unavailableErr := v.UnavailableError(); unavailableErr != nil {
currentRequests[i].UpdateResult(0, 0, false, unavailableErr)
} else if _, noWriteOrDelete, _, _ := v.ReadOnlyReasons(); noWriteOrDelete {
currentRequests[i].UpdateResult(0, 0, false, fmt.Errorf("volume %d is read only", v.Id))
} 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
}