Files
seaweedfs/weed/storage/volume_idx_rebuild.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

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
}