mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-24 08:54:28 +00:00
* volume: count a TTL volume's age from its last write, not the .dat mtime A delete appends a tombstone needle and vacuum rewrites the .dat wholesale, so the file's mtime moves without any write ever landing. The loader read lastModifiedTsSeconds back from that mtime, so every restart of a volume taking delete traffic re-armed expired() for another full TTL: an overwrite-heavy collection kept growing until it hit the max-volume cap. Recover the clock from the newest .idx entry that is not a tombstone and read that needle's append timestamp, falling back to the mtime when no write is recoverable. Only TTL volumes pay for the scan. Fixes #11160 * volume: count the .vif destroy time from the last write too ExpireAtSec is what an EC volume is reclaimed on, and it was recomputed as now+TTL every time the .vif was written. A read-only mark, a tier upload or an EC encode therefore handed an already expiring volume another full TTL, the same way the .dat mtime did. Derive it from the volume's last write, falling back to now for a volume that has not taken one yet so a fresh volume is not born expired. * volume: mirror the last-write TTL clock in the Rust volume server Same recovery as the Go loader: scan the .idx backwards for the newest entry that is not a tombstone and take that needle's append timestamp, leaving the clock on the .dat mtime when no write is recoverable. * volume: mirror the last-write destroy time in the Rust volume server Both .vif writers and the EC encode computed ExpireAtSec as now+TTL, the same way Go did, so the destroy time moved every time the sidecar was rewritten. Route all three through the volume's last write. * volume: report the .dat mtime in the Rust heartbeat, like Go does The Rust server reported its TTL clock as ModifiedAtSecond while Go reports the .dat mtime. The shell's quiet-period gates (volume.tier.move, volume.delete_empty) read that field as "last touched", which a delete has to count towards even though the TTL clock deliberately ignores it -- and with the clock now recovered from the last write, the two drift further apart. * volume: take the newest write by timestamp on a vacuumed volume The reverse .idx scan trusted position, which holds only while the .dat is append ordered. Vacuum rewrites it in key order, and since an overwrite keeps its original key, the highest-key survivor is not necessarily the newest write -- the recovered clock could land up to a TTL early and take the volume with data still inside its TTL. A volume that has been vacuumed (CompactionRevision > 0) now takes the maximum append timestamp over a bounded window of write entries instead. An append-ordered volume still answers in one read. * volume: never guess a vacuumed volume's last write, and resolve wrapped offsets Two holes in the reverse scan, both from review: A vacuumed volume's writes are ordered by key, so any of them can hold the newest timestamp. Reading a capped window sampled the highest keys, which could still miss a recently overwritten low-key needle and expire data inside its TTL. The scan now covers every write a vacuumed volume indexes, and a volume too large to scan keeps the .dat mtime rather than report a partial maximum -- late is recoverable, early is not. A .dat past MaxPossibleVolumeSize wraps the offsets in its .idx, so reading a timestamp at the unwrapped offset picks up an unrelated needle. Resolve the entry against the needle header first and retry one volume size in, the way doCheckAndFixVolumeData already does. * volume: drop GitHub issue references from TTL comments
118 lines
4.0 KiB
Go
118 lines
4.0 KiB
Go
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/volume_info"
|
|
)
|
|
|
|
func (v *Volume) GetVolumeInfo() *volume_server_pb.VolumeInfo {
|
|
return v.volumeInfo
|
|
}
|
|
|
|
func (v *Volume) maybeLoadVolumeInfo() (found bool) {
|
|
|
|
var err error
|
|
var hasRemoteFile bool
|
|
v.volumeInfo, hasRemoteFile, found, err = volume_info.MaybeLoadVolumeInfo(v.FileName(".vif"))
|
|
v.hasRemoteFile.Store(hasRemoteFile)
|
|
internVolumeInfoStrings(v.volumeInfo)
|
|
|
|
if v.volumeInfo.Version == 0 {
|
|
v.volumeInfo.Version = uint32(needle.GetCurrentVersion())
|
|
}
|
|
|
|
if hasRemoteFile {
|
|
glog.V(0).Infof("volume %d is tiered to %s as %s and read only", v.Id,
|
|
v.volumeInfo.Files[0].BackendName(), v.volumeInfo.Files[0].Key)
|
|
} else {
|
|
if v.volumeInfo.BytesOffset == 0 {
|
|
v.volumeInfo.BytesOffset = uint32(types.OffsetSize)
|
|
}
|
|
}
|
|
|
|
if v.volumeInfo.BytesOffset != 0 && v.volumeInfo.BytesOffset != uint32(types.OffsetSize) {
|
|
var m string
|
|
if types.OffsetSize == 5 {
|
|
m = "without"
|
|
} else {
|
|
m = "with"
|
|
}
|
|
glog.Exitf("BytesOffset mismatch in volume info file %s, try use binary version %s large_disk", v.FileName(".vif"), m)
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
glog.Warningf("load volume %d.vif file: %v", v.Id, err)
|
|
return
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// internVolumeInfoStrings shares the values every volume's .vif repeats. A
|
|
// tiered volume names its replication and its backend on every load, and the
|
|
// decode allocates a fresh copy of each, so a server holding millions of them
|
|
// otherwise holds millions of copies of the same handful of names. The remote
|
|
// key is left alone: it names one volume.
|
|
func internVolumeInfoStrings(volumeInfo *volume_server_pb.VolumeInfo) {
|
|
volumeInfo.Replication = internVolumeString(volumeInfo.Replication)
|
|
for _, remoteFile := range volumeInfo.GetFiles() {
|
|
remoteFile.BackendType = internVolumeString(remoteFile.BackendType)
|
|
remoteFile.BackendId = internVolumeString(remoteFile.BackendId)
|
|
remoteFile.Extension = internVolumeString(remoteFile.Extension)
|
|
}
|
|
}
|
|
|
|
func (v *Volume) HasRemoteFile() bool {
|
|
return v.hasRemoteFile.Load()
|
|
}
|
|
|
|
// LoadRemoteFile swaps the data backend to the remote tier object under
|
|
// dataFileAccessLock. Call this from a context that does NOT already hold the
|
|
// lock — the live tier-upload handler, where the heartbeat may be reading the
|
|
// backend concurrently. load() must instead use loadRemoteFileLocked, since it
|
|
// can be reached with the lock already held (CommitCompact).
|
|
func (v *Volume) LoadRemoteFile() error {
|
|
v.dataFileAccessLock.Lock()
|
|
defer v.dataFileAccessLock.Unlock()
|
|
return v.loadRemoteFileLocked()
|
|
}
|
|
|
|
// loadRemoteFileLocked swaps the data backend to the remote tier object. The
|
|
// caller must hold dataFileAccessLock or be single-threaded (load() during
|
|
// construction or a compaction-commit reload). It marks the volume tiered in the
|
|
// same locked step so a later heartbeat does not treat a removed local .dat as a
|
|
// phantom volume and stop reporting it to the master.
|
|
func (v *Volume) loadRemoteFileLocked() error {
|
|
// Callers only reach here for a tiered volume (HasRemoteFile / a just-appended
|
|
// remote file), but guard the index so a stray call is a clean error, not a panic.
|
|
if len(v.volumeInfo.GetFiles()) == 0 {
|
|
return fmt.Errorf("volume %d has no remote file to load", v.Id)
|
|
}
|
|
tierFile := v.volumeInfo.GetFiles()[0]
|
|
backendStorage, found := backend.BackendStorages[tierFile.BackendName()]
|
|
if !found {
|
|
return fmt.Errorf("backend storage %s not found", tierFile.BackendName())
|
|
}
|
|
v.swapDataBackendLocked(backendStorage.NewStorageFile(tierFile.Key, v.volumeInfo), true)
|
|
return nil
|
|
}
|
|
|
|
func (v *Volume) SaveVolumeInfo() error {
|
|
|
|
tierFileName := v.FileName(".vif")
|
|
if expireAtSec := v.ExpireAtSec(); expireAtSec > 0 {
|
|
v.volumeInfo.ExpireAtSec = expireAtSec
|
|
}
|
|
|
|
return volume_info.SaveVolumeInfo(tierFileName, v.volumeInfo)
|
|
|
|
}
|