Files
seaweedfs/weed/storage/vid_stamp.go
T
6ebc055a94 master, volume, filer: start on a read-only mounted directory and serve existing data read-only (#12081)
* master, volume, filer: start on a read-only mounted directory and serve existing data read-only

A read-only mount was treated as writable (permission bits only), so every
service failed on its first write: the master panicked writing raft conf.tmp,
the filer exited opening the leveldb LOCK, and the volume server skipped every
volume while still offering the directory as free capacity.

- util.IsReadOnlyMount: access(W_OK) == EROFS.
- volume: on a read-only -dir take the existing O_RDONLY/noWriteOrDelete load,
  index .idx in memory when no .sdx exists, offer no free slots (same branches
  as isDiskSpaceLow), skip the vid stamp there.
- master: on a read-only -mdir run raft from a private copy and refuse new
  volume ids.
- filer: leveldb2/leveldb3 set their existing ReadOnly option.

* volume: skip read-only mounts when picking EC shard copy targets

A shard copy landing on a read-only mount fails at write time; offer
only writable locations like the other placement paths do.

* volume: mirror the read-only mount handling in the Rust volume server

The .dat open only fell back to read-only on EACCES, so a real read-only
mount (EROFS) still failed the load. The index side already degrades
once no_write_or_delete is set. Add is_read_only_mount via access(W_OK)
== EROFS, mark DiskLocation.read_only_mount, and mirror the Go checks:
no free slots, heartbeat max = used, no vid-stamp writes, no EC shard
copy targets, and a read-only index dir forces the volume read-only.

* volume: write vid stamps while only the index dir is read-only

* filer: skip the write-bit check when a leveldb store opens read-only

* filer: serve reads of buckets with no leveldb3 dir on a read-only store

* volume: derive a stable dir uuid when a read-only mount cannot store one

* master: keep the read-only raft copy across restarts so votes survive

* volume: index a missing .idx in memory on a read-only mount

* volume: release the .idx handle after an in-memory read-only load

* volume: mount EC shards read-only when their index dir is on a read-only mount

* volume: mark the index dir read-only before the tail repair opens .idx

* volume: write vid stamps while only the index dir is read-only (Rust)

* volume: derive a stable dir uuid and report no free slots on a read-only mount (Rust)

* volume: index a missing .idx in memory on a read-only mount (Rust)

* volume: refuse an EC copy whose shard owner is on a read-only disk

* master: key the read-only raft copy by the absolute -mdir

Two masters sharing the same relative -mdir text from different working
directories would have collided on one temp copy and shared raft state.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* volume: mix device and host into the read-only dir uuid

Deriving the uuid from the path alone let independent servers at the
same mount point report one uuid, which RegisterUuids rejects. Hash in
the device id and hostname so restarts stay stable but disks differ.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: vic6996-web <320442181+vic6996-web@users.noreply.github.com>
Co-authored-by: user.email <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-26 09:28:49 -07:00

154 lines
3.7 KiB
Go

package storage
import (
"encoding/json"
"os"
"path/filepath"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/util"
)
const vidStampFileName = ".vid_stamp"
type vidStamp struct {
MaxVolumeId uint32 `json:"maxVolumeId"`
TopologyId string `json:"topologyId,omitempty"`
}
func (s *Store) MaxVolumeIdSeen() needle.VolumeId {
return needle.VolumeId(s.maxVolumeIdSeen.Load())
}
func (s *Store) LastSeenTopologyId() string {
if v := s.lastSeenTopologyId.Load(); v != nil {
return v.(string)
}
return ""
}
// UpdateMaxVolumeIdSeen raises the server's persisted bound and stamps every
// volume dir when it grows. Feeds: heartbeat echoes, AllocateVolume, local
// volume and EC shard creation.
func (s *Store) UpdateMaxVolumeIdSeen(vid needle.VolumeId) {
grew := false
for {
current := s.maxVolumeIdSeen.Load()
if current >= uint32(vid) {
break
}
if s.maxVolumeIdSeen.CompareAndSwap(current, uint32(vid)) {
grew = true
break
}
}
if s.vidStampReady.Load() && (grew || s.vidStampDirty.Load()) {
s.saveVidStamps()
}
}
func (s *Store) UpdateLastSeenTopologyId(id string) {
if id == "" || (s.LastSeenTopologyId() == id && !s.vidStampDirty.Load()) {
return
}
s.lastSeenTopologyId.Store(id)
if s.vidStampReady.Load() {
s.saveVidStamps()
}
}
// bootstrapMaxVolumeIdSeen seeds the bound from persisted stamps plus the max
// of every volume and EC shard id on disk, so a server upgraded mid-incident
// still reports a bound covering its own holdings.
func (s *Store) bootstrapMaxVolumeIdSeen() {
var maxVid needle.VolumeId
for _, location := range s.Locations {
if stamp, err := readVidStamp(location.Directory); err == nil {
if needle.VolumeId(stamp.MaxVolumeId) > maxVid {
maxVid = needle.VolumeId(stamp.MaxVolumeId)
}
if stamp.TopologyId != "" && s.LastSeenTopologyId() == "" {
s.lastSeenTopologyId.Store(stamp.TopologyId)
}
}
location.volumesLock.RLock()
for vid := range location.volumes {
if vid > maxVid {
maxVid = vid
}
}
location.volumesLock.RUnlock()
location.ecVolumesLock.RLock()
for vid := range location.ecVolumes {
if vid > maxVid {
maxVid = vid
}
}
location.ecVolumesLock.RUnlock()
}
s.maxVolumeIdSeen.Store(uint32(maxVid))
s.vidStampReady.Store(true)
s.saveVidStamps()
}
func (s *Store) saveVidStamps() {
s.vidStampLock.Lock()
defer s.vidStampLock.Unlock()
stamp, err := json.Marshal(vidStamp{
MaxVolumeId: s.maxVolumeIdSeen.Load(),
TopologyId: s.LastSeenTopologyId(),
})
if err != nil {
return
}
failed := false
for _, location := range s.Locations {
// The stamp lives on the data directory, which stays writable when
// only IdxDirectory is read-only.
if util.IsReadOnlyMount(location.Directory) {
continue
}
if err := writeVidStamp(location.Directory, stamp); err != nil {
glog.Warningf("write vid stamp in %s: %v", location.Directory, err)
failed = true
}
}
s.vidStampDirty.Store(failed)
}
func readVidStamp(dir string) (*vidStamp, error) {
data, err := os.ReadFile(filepath.Join(dir, vidStampFileName))
if err != nil {
return nil, err
}
stamp := &vidStamp{}
if err := json.Unmarshal(data, stamp); err != nil {
return nil, err
}
return stamp, nil
}
func writeVidStamp(dir string, stamp []byte) error {
tmp := filepath.Join(dir, vidStampFileName+".tmp")
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
if _, err := f.Write(stamp); err != nil {
f.Close()
return err
}
if err := f.Sync(); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(tmp, filepath.Join(dir, vidStampFileName)); err != nil {
return err
}
return util.FsyncDir(dir)
}