mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-03 18:47:41 +01:00
* 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>
126 lines
4.3 KiB
Go
126 lines
4.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
// VolumeFileScanner4RebuildIdx writes one .idx row per .dat record, in .dat
|
|
// append order, which is the shape the volume server's own writes leave behind.
|
|
type VolumeFileScanner4RebuildIdx struct {
|
|
writer *bufio.Writer
|
|
datSize int64
|
|
version needle.Version
|
|
}
|
|
|
|
func (scanner *VolumeFileScanner4RebuildIdx) VisitSuperBlock(superBlock super_block.SuperBlock) error {
|
|
return nil
|
|
}
|
|
|
|
func (scanner *VolumeFileScanner4RebuildIdx) ReadNeedleBody() bool {
|
|
return false
|
|
}
|
|
|
|
func (scanner *VolumeFileScanner4RebuildIdx) VisitNeedle(n *needle.Needle, offset int64, needleHeader, needleBody []byte) error {
|
|
// Stop at the first thing that is not a record: an all-zero header is
|
|
// unwritten space, a negative size is a corrupt header, and a record
|
|
// reaching past the end of .dat is a torn append. Indexing any of them
|
|
// fabricates rows, and io.EOF here stops the walk before it advances by a
|
|
// bad size -- a negative one moves the offset backwards.
|
|
if (n.Size == 0 && n.Id == 0) || n.Size < 0 {
|
|
return io.EOF
|
|
}
|
|
if needleDiskEnd(types.ToOffset(offset), n.Size, scanner.version) > scanner.datSize {
|
|
return io.EOF
|
|
}
|
|
size := n.Size
|
|
if !size.IsValid() {
|
|
size = types.TombstoneFileSize
|
|
}
|
|
_, err := scanner.writer.Write(needle_map.ToBytes(n.Id, types.ToOffset(offset), size))
|
|
return err
|
|
}
|
|
|
|
// rebuildIdxFile regenerates the volume's .idx from its .dat. The whole index
|
|
// is derivable from the data file, so a volume whose index directory has no
|
|
// .idx -- a -dir.idx pointed at an empty directory, or a lost index -- comes
|
|
// back on its own instead of taking the volume server down. The rows go to a
|
|
// temp file that is renamed in, so an interrupted rebuild leaves no partial
|
|
// index behind.
|
|
func (v *Volume) rebuildIdxFile() error {
|
|
if v.DataBackend == nil {
|
|
return fmt.Errorf("volume %d has no data backend", v.Id)
|
|
}
|
|
|
|
datSize, _, err := v.DataBackend.GetStat()
|
|
if err != nil {
|
|
return fmt.Errorf("stat %s: %w", v.FileName(".dat"), err)
|
|
}
|
|
|
|
idxFileName := v.FileName(".idx")
|
|
if err := os.MkdirAll(filepath.Dir(idxFileName), 0755); err != nil {
|
|
return fmt.Errorf("create idx dir for %s: %w", idxFileName, err)
|
|
}
|
|
tmpFileName := idxFileName + ".tmp"
|
|
tmpFile, err := os.OpenFile(tmpFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("create %s: %w", tmpFileName, err)
|
|
}
|
|
defer os.Remove(tmpFileName)
|
|
|
|
scanner := &VolumeFileScanner4RebuildIdx{writer: bufio.NewWriter(tmpFile), datSize: datSize, version: v.Version()}
|
|
err = ScanVolumeFileFrom(scanner.version, v.DataBackend, int64(v.SuperBlock.BlockSize()), scanner)
|
|
if err == nil {
|
|
err = scanner.writer.Flush()
|
|
}
|
|
if err == nil {
|
|
err = tmpFile.Sync()
|
|
}
|
|
if closeErr := tmpFile.Close(); err == nil {
|
|
err = closeErr
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("rebuild %s from %s: %w", idxFileName, v.FileName(".dat"), err)
|
|
}
|
|
|
|
if err := os.Rename(tmpFileName, idxFileName); err != nil {
|
|
return fmt.Errorf("rename %s: %w", tmpFileName, err)
|
|
}
|
|
return fsyncDir(filepath.Dir(idxFileName))
|
|
}
|
|
|
|
// rebuildIdxToTempFile writes the volume's .idx rows to a new temporary file
|
|
// and returns it rewound for reading, for a volume whose index directory
|
|
// cannot hold the rebuilt index. The caller owns the file and its path.
|
|
func (v *Volume) rebuildIdxToTempFile() (*os.File, error) {
|
|
datSize, _, err := v.DataBackend.GetStat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat %s: %w", v.FileName(".dat"), err)
|
|
}
|
|
tmpFile, err := os.CreateTemp("", "weed-idx-*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
writer := bufio.NewWriter(tmpFile)
|
|
scanner := &VolumeFileScanner4RebuildIdx{writer: writer, datSize: datSize, version: v.Version()}
|
|
if scanErr := ScanVolumeFileFrom(scanner.version, v.DataBackend, int64(v.SuperBlock.BlockSize()), scanner); scanErr != nil {
|
|
err = scanErr
|
|
} else if err = writer.Flush(); err == nil {
|
|
_, err = tmpFile.Seek(0, io.SeekStart)
|
|
}
|
|
if err != nil {
|
|
tmpFile.Close()
|
|
os.Remove(tmpFile.Name())
|
|
return nil, fmt.Errorf("rebuild %s from %s: %w", v.FileName(".idx"), v.FileName(".dat"), err)
|
|
}
|
|
return tmpFile, nil
|
|
}
|