mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-22 07:06:51 +00:00
* feat(ec): add encode_ts_ns to EC shard metadata and the shard read RPC EcShardConfig and VolumeEcShardReadRequest gain an int64 encode_ts_ns (encode time in unix nanos). It rides in .vif and the read request so a read can be scoped to the encode run that produced the index. * fix(ec): stamp each encode and reject cross-run shard reads Generate stamps EncodeTsNs into the volume's .vif. Reads carry it to the shard's owning volume (resolved together via FindEcVolumeWithShard, so a multi-disk server validates the disk that actually serves the bytes) and reject a shard from a different encode run, recovering from parity. A zero on either side (pre-upgrade volume) skips the guard. * fix(ec): stamp the encode identity on the worker-generated .vif The worker-local encode path now writes EncodeTsNs (and the resolved EC ratio) into the .vif, so the read guard is not silently off for volumes encoded by the maintenance worker. * fix(ec): wipe stale EC artifacts before re-encoding VolumeEcShardsGenerate evicts any in-memory EcVolume for the volume and removes its on-disk shard/index/sidecar files before writing fresh ones, so a retried encode never builds on a partial prior run and the unlink frees the inodes instead of leaving open fds serving old bytes. * fix(ec): unmount EC shards across all disks UnmountEcShards walked only the first disk holding the shard, leaving a duplicate copy mounted on a sibling disk (split-disk reconciled volumes) still serving and heartbeating. Traverse every disk and emit one deletion delta per disk. * fix(ec): delete orphan shards without a local .ecx deleteEcShardIdsForEachLocation gated shard-file removal on a local .ecx, so it could not clean an orphan .ecNN left by a failed copy on a disk with no index. Delete the requested shard files unconditionally; the index-file (.ecx/.ecj/.vif) routing stays gated as before. * fix(ec): clear stale EC shards cluster-wide before re-encoding ec.encode unmounts and deletes EC shards for the target volumes on every node before regenerating: fatal for the shards the topology reports (mounted leftovers), best-effort for the rest (a sweep that catches unmounted failed-copy orphans). A down node is a no-op. * fix(ec): don't nil EC fds on close so reads can't race eviction A reader resolves an EcVolume/shard under the lock then reads after it is released, so an eviction that nils ecxFile/ecdFile would race that read and panic. Close the fds without nilling the fields: the field is now write-once (no data race) and a concurrent read hits a closed fd, getting a clean error that the caller recovers from parity. * fix(ec): wipe stale EC artifacts on every disk and surface failures The pre-encode wipe only deleted beside the source volume, so a stale shard on a sibling disk survived and could be mounted against the new index at reconcile. Sweep every disk. Removal also ignored os.Remove errors, reporting a failed cleanup as success and letting a stale shard join the next generation; surface the first real failure (treating already-gone as success) from removeStaleEcArtifacts and the shard delete. * fix(ec): log when a local shard is skipped for a different encode run The cross-run guard returned errShardNotLocal, indistinguishable in logs from a genuinely-absent shard. Add a V(1) line naming both EncodeTsNs so operators can tell "wrong encode generation" from "shard not here". * fix(ec): surface metadata removal failures in the shard delete path deleteEcShardIdsForEachLocation still dropped os.Remove errors on the .ecx/.ecj/.vif/sidecar cleanup. A surviving stale .ecx is the orphan-index condition this path prevents, so route those through removeFileIfExists and return the first real failure instead of reporting cleanup as success. * fix(ec): fail orphan cleanup when a reachable node's delete fails The pre-encode orphan sweep swallowed every error for unreported (node, volume) pairs. That is only safe for an unreachable node, which cannot receive this encode's new generation. A reachable node whose delete genuinely failed (permission/IO) keeps an orphan shard that a later copy re-stamps with the new run's volume-level .vif identity, so the read guard would accept stale data. Surface those; stay best-effort only for unreachable nodes (gRPC Unavailable / no status). * fix(ec): guard ecjFile under its lock in the EC delete path EcVolume.Close nils ecjFile under ecjFileAccessLock; a delete that resolved its .ecx lookup before a concurrent eviction (the generate-time UnloadEcVolume) could then reach the journal append with a nil fd. Bail with a clear "volume closed" error under the lock instead. * fix(ec): reject an unstamped shard when the caller has an encode identity The read guard required both identities nonzero, so a current (stamped) caller accepted a holder with identity 0 and could be served a stale pre-upgrade shard. Reject when the caller is stamped and the holder differs (including unstamped); stay lenient only when the caller itself has no identity (pre-upgrade reader). A skipped shard recovers from parity. * fix(ec): full-teardown delete so cluster cleanup wipes a whole generation The pre-encode cluster sweep deleted only the listed canonical shards on remote nodes, leaving index/sidecar (and, on builds with versioned generations, those too) behind. Add a full_teardown flag to VolumeEcShardsDelete that evicts the volume and wipes every EC artifact for it on every disk via removeStaleEcArtifacts; the shell and worker pre-encode cleanup paths set it. Other delete callers (balance/decode/repair) are unchanged. * fix(ec): take ecjFileAccessLock before the nil-check in Sync and Close Sync and Close read ev.ecjFile before acquiring ecjFileAccessLock while Close nils it under the lock, a data race on the field. Take the lock first, then nil-check inside, in both. * fix(ec): acknowledge full_teardown so a pre-upgrade server can't fake success An old volume server silently ignores full_teardown and returns success for an ordinary delete, so the caller wrongly believes the generation was wiped and copies a fresh gen-0 onto an unwiped node. Echo full_teardown_done in the response; the worker destination cleanup fails when it is absent, and the shell cluster sweep fails for a reported (mounted) leftover while staying best-effort for an unreported node. encode_ts_ns stays an accepted transient (an old server just skips the new read guard, no regression). * fix(ec): fail the pre-encode sweep for any reachable node that can't ack teardown A reachable pre-upgrade server ignores full_teardown and returns success without wiping an orphan, which a later copy then folds into the new generation. Treat a missing full_teardown_done ack as fatal for every reachable node (best-effort only for a gRPC-unreachable one), not just for topology-reported pairs. * fix(ec): return the served shard identity and validate it client-side The encode identity was only enforced server-side, so a pre-upgrade server ignored the request field and served bytes unchecked. Echo the served shard's EncodeTsNs on every read response chunk and have the client reject a mismatch (including 0 from an old server), so the guard holds regardless of server version; a rejected read recovers from parity. * fix(ec): reject a short/empty remote shard read instead of serving zeros doReadRemoteEcShardInterval accepted an immediate EOF or a short stream and returned success with a partly zero-filled, unvalidated buffer (the server stamps the identity only on chunks that carry bytes). A non-deleted interval must arrive whole: require n == len(buf), exempting the is_deleted short-circuit (n=0), matching readLocalEcShardInterval's local check. A short read now fails so the caller recovers from parity. * test(ec): fake volume server echoes the full_teardown acknowledgement The worker now fails a teardown delete that isn't acknowledged (so a pre-upgrade server can't silently skip the wipe). The fake server's no-op VolumeEcShardsDelete returned an empty response, which the worker read as a skipped teardown and aborted the encode. Echo full_teardown_done. * feat(ec): mirror the encode-run identity guard + full_teardown into the Rust volume server The Go volume server stamps an encode-run identity (encode_ts_ns) into the .vif and rejects a read served from a shard of a different run; full_teardown wipes a whole generation and acknowledges it. The Rust volume server had none of it. Mirror the shared logic: load encode_ts_ns from the .vif onto the EcVolume, stamp it on every read response, and reject a request/response mismatch on both the server and the distributed-read client (recovering from parity); handle full_teardown by evicting the volume and wiping every EC artifact on each disk, echoing full_teardown_done so the caller can detect a server that ignored it. * fix(ec): remove a stale .vif on full teardown of a shard-only node A shard copy installs shards + .ecx before .vif, so an interrupted copy after a teardown could mount the new files under the previous run's identity / version / shard ratio / dat_file_size carried by the surviving .vif. Remove .vif during full teardown, gated on .idx absence so a source-volume holder keeps its live .vif. In Rust this lives in a teardown-only helper so the reconcile / load- fallback paths (which share the base removal) still preserve .vif. * fix(ec): treat a missing teardown ack as fatal, not as an unreachable node isNodeUnreachable returned true for any non-gRPC-status error, so a reachable pre-upgrade server's missing full_teardown_done ack (a plain error) was classified unreachable and the unreported pair was silently skipped. Classify only a real codes.Unavailable as unreachable, and wrap the missing ack in a sentinel the sweep treats as fatal regardless. A genuinely down node still surfaces as Unavailable from the RPC and stays best-effort. * fix(ec): reject a short shard read in the local EC needle reader read_ec_shard_needle ignored the byte count from shard.read_at and appended the whole pre-sized buffer, so a truncated shard's zero-filled tail passed the later length check and parsed as garbage. Require n == buf.len() per interval, erroring on a short read like the local interval reader already does. * fix(ec): probe reachability before skipping a node that returns Unavailable The pre-encode sweep skipped any node whose teardown delete returned codes.Unavailable, but a reachable volume server in maintenance mode also returns that code for the maintenance-gated delete, so its stale EC files were left behind on a node that can still receive the new generation. Confirm with a non-maintenance-gated empty-target Ping: skip only when the node fails the probe too (genuinely unreachable). * fix(ec): use try_exists for the teardown .vif .idx guard The teardown-only .vif removal gated on Path::exists(), which returns false on a permission/IO stat error, so a stat failure on a present .idx would read as a shard-only node and delete the live source volume's .vif. Gate on try_exists() == Ok(false) instead, preserving the sidecar on any stat error. * fix(ec): only skip a sweep node when a Ping confirms it is transport-down The pre-encode sweep skipped a node whenever its teardown delete and a liveness Ping both failed, but it treated ANY Ping error as down — an application-level Internal/ResourceExhausted, or Unimplemented from a pre-Ping server, left a reachable node's stale generation in place. Classify the Ping tri-state and skip only when it transport-fails with codes.Unavailable; a reachable or inconclusive node stays fatal. * fix(ec): exclude sweep-skipped nodes from the encode's rebalance The pre-encode sweep skips a genuinely-down node best-effort, but the rebalance then recollected the current topology — a node that recovered between the two could become a copy target and receive the new generation while still holding its stale, never-cleared shards. Have the sweep return the skipped set and exclude those nodes from the rebalance for this encode, so a node we could not clean cannot receive the new generation. Standalone ec.balance is unaffected. * fix(ec): re-sweep recovered nodes before generation so they aren't stranded A node skipped as down by the pre-encode sweep is excluded from the rebalance, but it can recover and become the generation host — mounting all shards locally, then being excluded from distribution. Union-only verification accepts all shards on one node and deletes the originals: a single point of failure. Re-sweep the skipped nodes just before generation; one whose teardown now succeeds leaves the skipped set and rebalances normally, while a node still down stays skipped. * fix(ec): abort the encode if a selected source is still skipped after re-sweep The re-sweep un-skips a recovered node, but the source was selected before it and a node can stay down through the re-sweep then recover just in time to be the generation host — mounting all shards locally while still excluded from the rebalance, which union-only verification accepts before deleting the originals. Abort the encode when a selected source remains skipped after the re-sweep. * fix(ec): batch delete returns retriable 503 when a volume became EC mid-batch If a volume is not EC at the batch-delete classification but is encoded to EC and its .dat deleted before the regular-volume mutation, the mutation returns an exact "not found" that the filer chunk-GC treats as completed, dropping the delete. Recheck EC presence under the mutation lock and return a retriable 503 with the "try again" token so the filer requeues it onto the EC path. * fix(ec): recheck EC state before the regular batch-delete mutation ec.encode mounts EC shards (copied from the .dat) before deleting the originals, so a volume can be EC while its .dat still exists. The batch delete only rechecked EC after a NotFound, so a successful regular-volume delete in that window wrote a tombstone to the soon-removed .dat — the delete was lost and the needle resurrected from the pre-tombstone shards. Recheck has_ec_volume under the write lock before delete_volume_needle and return a retriable 503 so the filer requeues onto the EC path. * fix(volume): make the metrics push test independent of test order test_push_metrics_once asserted the pushed body contains the request-counter family without ever touching the counter — a CounterVec with no children emits nothing, so the assertion only held when another test had already created a labelset in the shared registry. Create one in the test itself.
962 lines
37 KiB
Go
962 lines
37 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/volume_info"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
/*
|
|
|
|
Steps to apply erasure coding to .dat .idx files
|
|
0. ensure the volume is readonly
|
|
1. client call VolumeEcShardsGenerate to generate the .ecx and .ec00 ~ .ec13 files
|
|
2. client ask master for possible servers to hold the ec files
|
|
3. client call VolumeEcShardsCopy on above target servers to copy ec files from the source server
|
|
4. target servers report the new ec files to the master
|
|
5. master stores vid -> [14]*DataNode
|
|
6. client checks master. If all 14 slices are ready, delete the original .idx, .idx files
|
|
|
|
*/
|
|
|
|
// VolumeEcShardsGenerate generates the .ecx and .ec00 ~ .ec13 files
|
|
func (vs *VolumeServer) VolumeEcShardsGenerate(ctx context.Context, req *volume_server_pb.VolumeEcShardsGenerateRequest) (*volume_server_pb.VolumeEcShardsGenerateResponse, error) {
|
|
if err := vs.CheckMaintenanceMode(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
glog.V(0).Infof("VolumeEcShardsGenerate: %v", req)
|
|
|
|
v := vs.store.GetVolume(needle.VolumeId(req.VolumeId))
|
|
if v == nil {
|
|
return nil, fmt.Errorf("volume %d not found", req.VolumeId)
|
|
}
|
|
baseFileName := v.DataFileName()
|
|
|
|
if v.Collection != req.Collection {
|
|
return nil, fmt.Errorf("existing collection:%v unexpected input: %v", v.Collection, req.Collection)
|
|
}
|
|
|
|
// Create EC context - prefer existing .vif config if present (for regeneration scenarios)
|
|
ecCtx := erasure_coding.NewDefaultECContext(req.Collection, needle.VolumeId(req.VolumeId))
|
|
if volumeInfo, _, found, _ := volume_info.MaybeLoadVolumeInfo(baseFileName + ".vif"); found && volumeInfo.EcShardConfig != nil {
|
|
ds := int(volumeInfo.EcShardConfig.DataShards)
|
|
ps := int(volumeInfo.EcShardConfig.ParityShards)
|
|
|
|
// Validate and use existing EC config
|
|
if ds > 0 && ps > 0 && ds+ps <= erasure_coding.MaxShardCount {
|
|
ecCtx.DataShards = ds
|
|
ecCtx.ParityShards = ps
|
|
glog.V(0).Infof("Using existing EC config for volume %d: %s", req.VolumeId, ecCtx.String())
|
|
} else {
|
|
glog.Warningf("Invalid EC config in .vif for volume %d (data=%d, parity=%d), using defaults", req.VolumeId, ds, ps)
|
|
}
|
|
} else {
|
|
glog.V(0).Infof("Using default EC config for volume %d: %s", req.VolumeId, ecCtx.String())
|
|
}
|
|
|
|
shouldCleanup := true
|
|
defer func() {
|
|
if !shouldCleanup {
|
|
return
|
|
}
|
|
for i := 0; i < ecCtx.Total(); i++ {
|
|
os.Remove(baseFileName + ecCtx.ToExt(i))
|
|
}
|
|
os.Remove(v.IndexFileName() + ".ecx")
|
|
os.Remove(erasure_coding.BitrotSidecarPath(baseFileName, 0))
|
|
}()
|
|
|
|
// Wipe any EC artifacts from a prior encode so a retry never mixes two runs.
|
|
// Evict the in-memory EcVolume first so the unlink frees the inodes instead
|
|
// of leaving open fds serving the old bytes. Sweep every disk: stale shards
|
|
// can sit on a sibling disk and would otherwise survive and be mounted
|
|
// against the new index at reconcile. Scans the cap for custom ratios.
|
|
vs.store.UnloadEcVolume(needle.VolumeId(req.VolumeId))
|
|
for _, loc := range vs.store.Locations {
|
|
dataBase := storage.VolumeFileName(loc.Directory, req.Collection, int(req.VolumeId))
|
|
idxBase := storage.VolumeFileName(loc.IdxDirectory, req.Collection, int(req.VolumeId))
|
|
if err := removeStaleEcArtifacts(dataBase, idxBase, erasure_coding.MaxShardCount); err != nil {
|
|
return nil, fmt.Errorf("wipe stale EC artifacts for volume %d on %s: %w", req.VolumeId, loc.Directory, err)
|
|
}
|
|
}
|
|
|
|
// IMPORTANT: Generate .ecx BEFORE EC shards to prevent a race condition.
|
|
// If .ecx were generated after EC shards, any write (e.g. from WriteNeedleBlob
|
|
// during replica sync) between the two steps would add entries to .idx that
|
|
// end up in .ecx but whose data is NOT in the EC shards — causing "shard too
|
|
// short" and "size mismatch" errors on reads.
|
|
//
|
|
// By generating .ecx first, it reflects the .idx state at or before the .dat
|
|
// is read for EC encoding. If a write sneaks in after .ecx but before/during
|
|
// EC encoding, the shards contain MORE data than .ecx references, which is
|
|
// harmless (the extra data is simply not indexed).
|
|
|
|
// write .ecx file from the current .idx
|
|
if err := erasure_coding.WriteSortedFileFromIdx(v.IndexFileName(), ".ecx"); err != nil {
|
|
return nil, fmt.Errorf("WriteSortedFileFromIdx %s: %v", v.IndexFileName(), err)
|
|
}
|
|
|
|
// snapshot .dat file size before encoding — must match what .ecx references
|
|
datSize, _, _ := v.FileStat()
|
|
|
|
// write .ec00 ~ .ec[TotalShards-1] files using context
|
|
ecBitrot, err := erasure_coding.WriteEcFiles(baseFileName, ecCtx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("WriteEcFiles %s: %v", baseFileName, err)
|
|
}
|
|
// Persist the generation-0 bitrot checksum sidecar (<base>.ecsum) alongside
|
|
// the shards so it travels with them during distribution (copy_ecsum_file).
|
|
// The source loading its own canonical sidecar is correct — it holds all
|
|
// shards and a complete manifest. Best-effort: a failed sidecar write leaves
|
|
// the generation unprotected rather than failing the encode.
|
|
if erasure_coding.BitrotProtectionEnabled && ecBitrot != nil {
|
|
if serr := erasure_coding.SaveBitrotSidecar(erasure_coding.BitrotSidecarPath(baseFileName, 0), ecBitrot); serr != nil {
|
|
glog.Warningf("failed to write EC bitrot sidecar for volume %d: %v", req.VolumeId, serr)
|
|
}
|
|
}
|
|
|
|
// write .vif files
|
|
var expireAtSec uint64
|
|
if v.Ttl != nil {
|
|
ttlSecond := v.Ttl.ToSeconds()
|
|
if ttlSecond > 0 {
|
|
expireAtSec = uint64(time.Now().Unix()) + ttlSecond //calculated expiration time
|
|
}
|
|
}
|
|
volumeInfo := &volume_server_pb.VolumeInfo{Version: uint32(v.Version())}
|
|
volumeInfo.ExpireAtSec = expireAtSec
|
|
volumeInfo.DatFileSize = int64(datSize)
|
|
|
|
// Validate EC configuration before saving to .vif
|
|
if ecCtx.DataShards <= 0 || ecCtx.ParityShards <= 0 || ecCtx.Total() > erasure_coding.MaxShardCount {
|
|
return nil, fmt.Errorf("invalid EC config before saving: data=%d, parity=%d, total=%d (max=%d)",
|
|
ecCtx.DataShards, ecCtx.ParityShards, ecCtx.Total(), erasure_coding.MaxShardCount)
|
|
}
|
|
|
|
// EncodeTsNs stamps this run's identity into the .vif (copied with the
|
|
// shards), so a read served from a different run's shard is rejected.
|
|
volumeInfo.EcShardConfig = &volume_server_pb.EcShardConfig{
|
|
DataShards: uint32(ecCtx.DataShards),
|
|
ParityShards: uint32(ecCtx.ParityShards),
|
|
EncodeTsNs: time.Now().UnixNano(),
|
|
}
|
|
glog.V(1).Infof("Saving EC config to .vif for volume %d: %d+%d (total: %d)",
|
|
req.VolumeId, ecCtx.DataShards, ecCtx.ParityShards, ecCtx.Total())
|
|
|
|
if err := volume_info.SaveVolumeInfo(baseFileName+".vif", volumeInfo); err != nil {
|
|
return nil, fmt.Errorf("SaveVolumeInfo %s: %v", baseFileName, err)
|
|
}
|
|
|
|
shouldCleanup = false
|
|
|
|
return &volume_server_pb.VolumeEcShardsGenerateResponse{}, nil
|
|
}
|
|
|
|
// VolumeEcShardsRebuild generates the any of the missing .ec00 ~ .ec13 files
|
|
func (vs *VolumeServer) VolumeEcShardsRebuild(ctx context.Context, req *volume_server_pb.VolumeEcShardsRebuildRequest) (*volume_server_pb.VolumeEcShardsRebuildResponse, error) {
|
|
if err := vs.CheckMaintenanceMode(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
glog.V(0).Infof("VolumeEcShardsRebuild: %v", req)
|
|
baseFileName := erasure_coding.EcShardBaseFileName(req.Collection, int(req.VolumeId))
|
|
|
|
var rebuiltShardIds []uint32
|
|
|
|
// Find the rebuild location: the location with the most shards and an .ecx file.
|
|
// With multi-disk servers, shards may be spread across different locations.
|
|
var rebuildLocation *storage.DiskLocation
|
|
var rebuildShardCount int
|
|
var otherLocationsWithShards []*storage.DiskLocation
|
|
|
|
for _, location := range vs.store.Locations {
|
|
_, _, existingShardCount, err := checkEcVolumeStatus(baseFileName, location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
indexBaseFileName := path.Join(location.IdxDirectory, baseFileName)
|
|
if !util.FileExists(indexBaseFileName+".ecx") && location.IdxDirectory != location.Directory {
|
|
indexBaseFileName = path.Join(location.Directory, baseFileName)
|
|
}
|
|
hasEcx := util.FileExists(indexBaseFileName + ".ecx")
|
|
|
|
// Skip locations that have neither shard files nor an .ecx file.
|
|
if existingShardCount == 0 && !hasEcx {
|
|
continue
|
|
}
|
|
|
|
if hasEcx && (rebuildLocation == nil || existingShardCount > rebuildShardCount) {
|
|
if rebuildLocation != nil {
|
|
otherLocationsWithShards = append(otherLocationsWithShards, rebuildLocation)
|
|
}
|
|
rebuildLocation = location
|
|
rebuildShardCount = existingShardCount
|
|
} else {
|
|
otherLocationsWithShards = append(otherLocationsWithShards, location)
|
|
}
|
|
}
|
|
|
|
if rebuildLocation == nil {
|
|
return &volume_server_pb.VolumeEcShardsRebuildResponse{}, nil
|
|
}
|
|
|
|
// Collect additional directories where shard files may exist.
|
|
// On multi-disk servers, existing local shards may be on a different disk
|
|
// than where copied shards were placed during ec.rebuild.
|
|
rebuildDataDir := rebuildLocation.Directory
|
|
var additionalDirs []string
|
|
for _, otherLocation := range otherLocationsWithShards {
|
|
additionalDirs = append(additionalDirs, otherLocation.Directory)
|
|
}
|
|
|
|
// Rebuild missing EC files, searching all disk locations for input shards.
|
|
// Present input shards are verified against the bitrot sidecar (when present)
|
|
// and corrupt ones are regenerated; unsafe_ignore_sidecar bypasses the guard.
|
|
dataBaseFileName := path.Join(rebuildDataDir, baseFileName)
|
|
if generatedShardIds, err := erasure_coding.RebuildEcFiles(dataBaseFileName, erasure_coding.BackgroundECContext(), req.UnsafeIgnoreSidecar, additionalDirs...); err != nil {
|
|
return nil, fmt.Errorf("RebuildEcFiles %s: %v", dataBaseFileName, err)
|
|
} else {
|
|
rebuiltShardIds = generatedShardIds
|
|
}
|
|
|
|
indexBaseFileName := path.Join(rebuildLocation.IdxDirectory, baseFileName)
|
|
if !util.FileExists(indexBaseFileName+".ecx") && rebuildLocation.IdxDirectory != rebuildLocation.Directory {
|
|
indexBaseFileName = path.Join(rebuildLocation.Directory, baseFileName)
|
|
}
|
|
if err := erasure_coding.RebuildEcxFile(indexBaseFileName); err != nil {
|
|
return nil, fmt.Errorf("RebuildEcxFile %s: %v", indexBaseFileName, err)
|
|
}
|
|
|
|
// Opportunistic bitrot backfill: if protection is enabled, no sidecar exists
|
|
// yet (a volume encoded before this feature), and this rebuilder can reach
|
|
// every shard, compute and write a generation-0 sidecar. The TOFU baseline
|
|
// blesses current bytes; ComputeProtectionFromShards refuses a partial
|
|
// manifest, so a multi-server rebuild that cannot reach all shards just skips.
|
|
if erasure_coding.BitrotProtectionEnabled {
|
|
sidecarPath := erasure_coding.BitrotSidecarPath(dataBaseFileName, 0)
|
|
if _, statErr := os.Stat(sidecarPath); os.IsNotExist(statErr) {
|
|
ctx := erasure_coding.NewDefaultECContext("", 0)
|
|
if vi, _, found, _ := volume_info.MaybeLoadVolumeInfo(dataBaseFileName + ".vif"); found && vi.EcShardConfig != nil {
|
|
if ds, ps := int(vi.EcShardConfig.DataShards), int(vi.EcShardConfig.ParityShards); ds > 0 && ps > 0 && ds+ps <= erasure_coding.MaxShardCount {
|
|
ctx = &erasure_coding.ECContext{DataShards: ds, ParityShards: ps}
|
|
}
|
|
}
|
|
if prot, berr := erasure_coding.ComputeProtectionFromShards(dataBaseFileName, ctx, 0, additionalDirs); berr != nil {
|
|
glog.V(2).Infof("bitrot backfill skipped for %s: %v", dataBaseFileName, berr)
|
|
} else if werr := erasure_coding.SaveBitrotSidecar(sidecarPath, prot); werr != nil {
|
|
glog.Warningf("bitrot backfill: write sidecar for %s: %v", dataBaseFileName, werr)
|
|
} else {
|
|
glog.V(0).Infof("bitrot backfill: wrote sidecar for %s after rebuild", dataBaseFileName)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &volume_server_pb.VolumeEcShardsRebuildResponse{
|
|
RebuiltShardIds: rebuiltShardIds,
|
|
}, nil
|
|
}
|
|
|
|
// VolumeEcShardsCopy copy the .ecx and some ec data slices
|
|
func (vs *VolumeServer) VolumeEcShardsCopy(ctx context.Context, req *volume_server_pb.VolumeEcShardsCopyRequest) (*volume_server_pb.VolumeEcShardsCopyResponse, error) {
|
|
if err := vs.CheckMaintenanceMode(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
glog.V(0).Infof("VolumeEcShardsCopy: %v", req)
|
|
|
|
var location *storage.DiskLocation
|
|
|
|
// Select the target location for storing EC shard files.
|
|
//
|
|
// When req.DiskId > 0 the caller is explicitly choosing a disk:
|
|
// location = vs.store.Locations[req.DiskId]
|
|
// (DiskId=1 → Locations[1], DiskId=2 → Locations[2], etc.)
|
|
//
|
|
// When req.DiskId == 0 (the protobuf default, meaning "not specified")
|
|
// we auto-select location by preferring the disk that already holds EC
|
|
// shards for this volume, then falling back to any HDD, then any disk.
|
|
//
|
|
// Note: Locations[0] cannot be targeted explicitly via DiskId because 0
|
|
// is indistinguishable from "unset". It can still be chosen by the
|
|
// auto-select logic.
|
|
if req.DiskId > 0 {
|
|
// Validate disk ID is within bounds
|
|
if int(req.DiskId) >= len(vs.store.Locations) {
|
|
return nil, fmt.Errorf("invalid disk_id %d: only have %d disks", req.DiskId, len(vs.store.Locations))
|
|
}
|
|
|
|
// Use the specific disk location
|
|
location = vs.store.Locations[req.DiskId]
|
|
glog.V(1).Infof("Using disk %d for EC shard copy: %s", req.DiskId, location.Directory)
|
|
} else {
|
|
// Auto-select the target disk: prefer a disk that already has the
|
|
// EC volume mounted, then a disk that owns the .ecx on disk (the
|
|
// volume hasn't been mounted yet — relevant for ec.rebuild, where
|
|
// only the first shard carries .ecx and subsequent shards must
|
|
// land on the same disk; see #9212), then any HDD, then any disk.
|
|
// Pass the build's default data-shard count for free-slot maths;
|
|
// the helper takes it as a parameter so custom-ratio builds (e.g.
|
|
// enterprise) can swap it without touching this file.
|
|
location = vs.store.FindEcShardTargetLocation(req.Collection, needle.VolumeId(req.VolumeId), erasure_coding.DataShardsCount)
|
|
if location == nil {
|
|
return nil, fmt.Errorf("no space left")
|
|
}
|
|
}
|
|
|
|
dataBaseFileName := storage.VolumeFileName(location.Directory, req.Collection, int(req.VolumeId))
|
|
indexBaseFileName := storage.VolumeFileName(location.IdxDirectory, req.Collection, int(req.VolumeId))
|
|
|
|
err := operation.WithVolumeServerClient(true, pb.ServerAddress(req.SourceDataNode), vs.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
|
|
|
// copy ec data slices
|
|
for _, shardId := range req.ShardIds {
|
|
if _, err := vs.doCopyFile(client, true, req.Collection, req.VolumeId, math.MaxUint32, math.MaxInt64, dataBaseFileName, erasure_coding.ToExt(int(shardId)), false, false, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if req.CopyEcxFile {
|
|
|
|
// copy ecx file
|
|
if _, err := vs.doCopyFile(client, true, req.Collection, req.VolumeId, math.MaxUint32, math.MaxInt64, indexBaseFileName, ".ecx", false, false, nil); err != nil {
|
|
return err
|
|
}
|
|
// Defense in depth: writeToFile now removes partial files on
|
|
// stream error, but a source that genuinely held a 0-byte
|
|
// .ecx (e.g. a corrupted upstream replica) would otherwise
|
|
// leave a 0-byte file here and the mount path would reject
|
|
// it later. Catch that at distribute time so the orchestrator
|
|
// can pick a different source rather than learning about it
|
|
// at mount.
|
|
// Stat failure must not silently pass. doCopyFile reported
|
|
// success, but if the file is gone, unreadable, or a directory
|
|
// somehow, the orchestrator should learn now — at mount time
|
|
// the operator only sees "no .ecx found" with no useful context
|
|
// about which step actually failed.
|
|
ecxPath := indexBaseFileName + ".ecx"
|
|
info, statErr := os.Stat(ecxPath)
|
|
if statErr != nil {
|
|
return fmt.Errorf("VolumeEcShardsCopy volume %d: stat copied .ecx %s: %w", req.VolumeId, ecxPath, statErr)
|
|
}
|
|
if info.IsDir() {
|
|
return fmt.Errorf("VolumeEcShardsCopy volume %d: copied .ecx path %s is a directory", req.VolumeId, ecxPath)
|
|
}
|
|
if info.Size() == 0 {
|
|
if removeErr := os.Remove(ecxPath); removeErr != nil && !os.IsNotExist(removeErr) {
|
|
glog.Warningf("VolumeEcShardsCopy volume %d: remove 0-byte .ecx %s: %v", req.VolumeId, ecxPath, removeErr)
|
|
}
|
|
return fmt.Errorf("VolumeEcShardsCopy volume %d: source .ecx is 0 bytes", req.VolumeId)
|
|
}
|
|
}
|
|
|
|
if req.CopyEcjFile {
|
|
// copy ecj file
|
|
if _, err := vs.doCopyFile(client, true, req.Collection, req.VolumeId, math.MaxUint32, math.MaxInt64, indexBaseFileName, ".ecj", true, true, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if req.CopyVifFile {
|
|
// copy vif file
|
|
if _, err := vs.doCopyFile(client, true, req.Collection, req.VolumeId, math.MaxUint32, math.MaxInt64, dataBaseFileName, ".vif", false, true, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if req.CopyEcsumFile {
|
|
// Propagate the generation-0 bitrot checksum sidecar when the source
|
|
// has one. This non-2PC copy path (balance / fresh-encode / rebuild
|
|
// distribution) has no Prepare backstop, and fresh-encode sidecar
|
|
// writes are best-effort, so a missing source sidecar is a no-op
|
|
// (ignore-not-found): the holder is simply unprotected.
|
|
if _, err := vs.doCopyFile(client, true, req.Collection, req.VolumeId, math.MaxUint32, math.MaxInt64, dataBaseFileName, erasure_coding.BitrotSidecarExt, false, true, nil); err != nil {
|
|
return fmt.Errorf("VolumeEcShardsCopy volume %d: copy %s sidecar: %w", req.VolumeId, erasure_coding.BitrotSidecarExt, err)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("VolumeEcShardsCopy volume %d: %v", req.VolumeId, err)
|
|
}
|
|
|
|
return &volume_server_pb.VolumeEcShardsCopyResponse{}, nil
|
|
}
|
|
|
|
// VolumeEcShardsDelete local delete the .ecx and some ec data slices if not needed
|
|
// the shard should not be mounted before calling this.
|
|
func (vs *VolumeServer) VolumeEcShardsDelete(ctx context.Context, req *volume_server_pb.VolumeEcShardsDeleteRequest) (*volume_server_pb.VolumeEcShardsDeleteResponse, error) {
|
|
if err := vs.checkGrpcAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := vs.CheckMaintenanceMode(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bName := erasure_coding.EcShardBaseFileName(req.Collection, int(req.VolumeId))
|
|
|
|
if req.FullTeardown {
|
|
// Pre-encode cleanup: evict the volume and wipe every EC artifact for it on
|
|
// every disk (the same teardown the generator does locally), not just the
|
|
// listed shards, so a remote node retains no stale generation that a fresh
|
|
// gen-0 copy would later collide with.
|
|
glog.V(0).Infof("ec volume %s full teardown", bName)
|
|
vs.store.UnloadEcVolume(needle.VolumeId(req.VolumeId))
|
|
for _, location := range vs.store.Locations {
|
|
dataBase := storage.VolumeFileName(location.Directory, req.Collection, int(req.VolumeId))
|
|
idxBase := storage.VolumeFileName(location.IdxDirectory, req.Collection, int(req.VolumeId))
|
|
if err := removeStaleEcArtifacts(dataBase, idxBase, erasure_coding.MaxShardCount); err != nil {
|
|
return nil, fmt.Errorf("full teardown of ec volume %d on %s: %w", req.VolumeId, location.Directory, err)
|
|
}
|
|
}
|
|
return &volume_server_pb.VolumeEcShardsDeleteResponse{FullTeardownDone: true}, nil
|
|
}
|
|
|
|
glog.V(0).Infof("ec volume %s shard delete %v", bName, req.ShardIds)
|
|
|
|
for diskId, location := range vs.store.Locations {
|
|
if err := deleteEcShardIdsForEachLocation(bName, location, req.ShardIds); err != nil {
|
|
glog.Errorf("deleteEcShards from disk_id:%d %s %s.%v: %v", diskId, location.Directory, bName, req.ShardIds, err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &volume_server_pb.VolumeEcShardsDeleteResponse{}, nil
|
|
}
|
|
|
|
func deleteEcShardIdsForEachLocation(bName string, location *storage.DiskLocation, shardIds []uint32) error {
|
|
|
|
found := false
|
|
|
|
indexBaseFilename := path.Join(location.IdxDirectory, bName)
|
|
dataBaseFilename := path.Join(location.Directory, bName)
|
|
|
|
// Delete the requested shard files unconditionally. Gating on a local .ecx
|
|
// (still used for index-file routing below) would leak an orphan shard left
|
|
// by a failed copy that reconciliation later mounts under a foreign index.
|
|
for _, shardId := range shardIds {
|
|
shardFileName := dataBaseFilename + erasure_coding.ToExt(int(shardId))
|
|
if util.FileExists(shardFileName) {
|
|
found = true
|
|
if err := removeFileIfExists(shardFileName); err != nil {
|
|
return fmt.Errorf("remove ec shard %s: %w", shardFileName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
hasEcxFile, hasIdxFile, existingShardCount, err := checkEcVolumeStatus(bName, location)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if existingShardCount == 0 {
|
|
// The whole EC generation is gone on this disk.
|
|
|
|
// Remove the bitrot checksum sidecar(s) (.ecsum and any .ecsum.v<N>)
|
|
// from both dirs. This is gated on the shards being gone, NOT on a
|
|
// stray .ecx still being present: a sidecar whose shards have all been
|
|
// deleted is orphaned and must go even when no .ecx remains, or it
|
|
// leaks. The per-shard-id delete that ec.rebuild uses for
|
|
// copied-survivor cleanup leaves shards behind, so this guard does not
|
|
// fire there.
|
|
if err := removeBitrotSidecars(dataBaseFilename); err != nil {
|
|
return err
|
|
}
|
|
if location.IdxDirectory != location.Directory {
|
|
if err := removeBitrotSidecars(indexBaseFilename); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if hasEcxFile {
|
|
// Remove .ecx/.ecj from both idx and data directories
|
|
// since they may be in either location depending on when -dir.idx was configured.
|
|
// A surviving stale .ecx is the orphan-index condition this path prevents,
|
|
// so surface a real removal failure instead of reporting cleanup as success.
|
|
for _, p := range []string{indexBaseFilename + ".ecx", indexBaseFilename + ".ecj"} {
|
|
if err := removeFileIfExists(p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if location.IdxDirectory != location.Directory {
|
|
for _, p := range []string{dataBaseFilename + ".ecx", dataBaseFilename + ".ecj"} {
|
|
if err := removeFileIfExists(p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hasIdxFile {
|
|
// .vif is used for ec volumes and normal volumes
|
|
if err := removeFileIfExists(dataBaseFilename + ".vif"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// removeFileIfExists removes path, treating "already gone" as success and
|
|
// returning only a real failure (so a stale shard left behind is not silently
|
|
// reported as cleaned).
|
|
func removeFileIfExists(path string) error {
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// removeStaleEcArtifacts deletes the shard, index, journal, and bitrot sidecar
|
|
// files of a prior encode so a fresh encode never mixes runs. total is the
|
|
// shard-id range to scan (pass the cap for custom ratios). Returns the first
|
|
// real removal failure; does not touch the source .dat/.idx/.vif.
|
|
func removeStaleEcArtifacts(dataBaseFileName, indexBaseFileName string, total int) error {
|
|
var firstErr error
|
|
record := func(err error) {
|
|
if err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
for i := 0; i < total; i++ {
|
|
record(removeFileIfExists(dataBaseFileName + erasure_coding.ToExt(i)))
|
|
}
|
|
// .ecx/.ecj/.ecsum may sit in either dir depending on -dir.idx; clear both.
|
|
record(removeFileIfExists(indexBaseFileName + ".ecx"))
|
|
record(removeFileIfExists(indexBaseFileName + ".ecj"))
|
|
record(removeBitrotSidecars(indexBaseFileName))
|
|
if dataBaseFileName != indexBaseFileName {
|
|
record(removeFileIfExists(dataBaseFileName + ".ecx"))
|
|
record(removeFileIfExists(dataBaseFileName + ".ecj"))
|
|
record(removeBitrotSidecars(dataBaseFileName))
|
|
}
|
|
|
|
// Canonical <base>.vif. A shard copy installs shards + .ecx before .vif, so an
|
|
// interrupted copy can leave a stale .vif whose run identity / shard ratio /
|
|
// dat_file_size a fresh generation would inherit. Remove it only on a shard-only
|
|
// EC node: where a normal <base>.idx exists this is the source volume holder and
|
|
// the .vif belongs to that live volume — keep it. This mirrors the !hasIdxFile
|
|
// gate in the per-shard delete path.
|
|
if _, statErr := os.Stat(indexBaseFileName + ".idx"); os.IsNotExist(statErr) {
|
|
record(removeFileIfExists(indexBaseFileName + ".vif"))
|
|
if dataBaseFileName != indexBaseFileName {
|
|
if _, dStatErr := os.Stat(dataBaseFileName + ".idx"); os.IsNotExist(dStatErr) {
|
|
record(removeFileIfExists(dataBaseFileName + ".vif"))
|
|
}
|
|
}
|
|
}
|
|
return firstErr
|
|
}
|
|
|
|
// removeBitrotSidecars removes the legacy <base>.ecsum and any versioned
|
|
// <base>.ecsum.v<N> sidecars, returning the first real removal failure.
|
|
func removeBitrotSidecars(baseFilename string) error {
|
|
var firstErr error
|
|
if err := removeFileIfExists(baseFilename + erasure_coding.BitrotSidecarExt); err != nil {
|
|
firstErr = err
|
|
}
|
|
matches, _ := filepath.Glob(baseFilename + erasure_coding.BitrotSidecarExt + ".v*")
|
|
for _, m := range matches {
|
|
if err := removeFileIfExists(m); err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
return firstErr
|
|
}
|
|
|
|
func checkEcVolumeStatus(bName string, location *storage.DiskLocation) (hasEcxFile bool, hasIdxFile bool, existingShardCount int, err error) {
|
|
// check whether to delete the .ecx and .ecj file also
|
|
fileInfos, err := os.ReadDir(location.Directory)
|
|
if err != nil {
|
|
return false, false, 0, err
|
|
}
|
|
if location.IdxDirectory != location.Directory {
|
|
idxFileInfos, err := os.ReadDir(location.IdxDirectory)
|
|
if err != nil {
|
|
return false, false, 0, err
|
|
}
|
|
fileInfos = append(fileInfos, idxFileInfos...)
|
|
}
|
|
for _, fileInfo := range fileInfos {
|
|
if fileInfo.Name() == bName+".ecx" || fileInfo.Name() == bName+".ecj" {
|
|
hasEcxFile = true
|
|
continue
|
|
}
|
|
if fileInfo.Name() == bName+".idx" {
|
|
hasIdxFile = true
|
|
continue
|
|
}
|
|
if isEcDataShardFile(fileInfo.Name(), bName) {
|
|
existingShardCount++
|
|
}
|
|
}
|
|
return hasEcxFile, hasIdxFile, existingShardCount, nil
|
|
}
|
|
|
|
func isEcDataShardFile(fileName, baseName string) bool {
|
|
const ecDataShardSuffixLen = 2 // ".ecNN"
|
|
prefix := baseName + ".ec"
|
|
if !strings.HasPrefix(fileName, prefix) {
|
|
return false
|
|
}
|
|
suffix := strings.TrimPrefix(fileName, prefix)
|
|
if len(suffix) != ecDataShardSuffixLen {
|
|
return false
|
|
}
|
|
shardId, err := strconv.Atoi(suffix)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return shardId >= 0 && shardId < erasure_coding.MaxShardCount
|
|
}
|
|
|
|
func (vs *VolumeServer) VolumeEcShardsMount(ctx context.Context, req *volume_server_pb.VolumeEcShardsMountRequest) (*volume_server_pb.VolumeEcShardsMountResponse, error) {
|
|
|
|
glog.V(0).Infof("VolumeEcShardsMount: %v", req)
|
|
|
|
for _, shardId := range req.ShardIds {
|
|
err := vs.store.MountEcShards(req.Collection, needle.VolumeId(req.VolumeId), erasure_coding.ShardId(shardId), req.SourceDiskType)
|
|
|
|
if err != nil {
|
|
glog.Errorf("ec shard mount %v: %v", req, err)
|
|
} else {
|
|
glog.V(2).Infof("ec shard mount %v", req)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mount %d.%d: %v", req.VolumeId, shardId, err)
|
|
}
|
|
}
|
|
|
|
return &volume_server_pb.VolumeEcShardsMountResponse{}, nil
|
|
}
|
|
|
|
func (vs *VolumeServer) VolumeEcShardsUnmount(ctx context.Context, req *volume_server_pb.VolumeEcShardsUnmountRequest) (*volume_server_pb.VolumeEcShardsUnmountResponse, error) {
|
|
|
|
glog.V(0).Infof("VolumeEcShardsUnmount: %v", req)
|
|
|
|
for _, shardId := range req.ShardIds {
|
|
err := vs.store.UnmountEcShards(needle.VolumeId(req.VolumeId), erasure_coding.ShardId(shardId))
|
|
|
|
if err != nil {
|
|
glog.Errorf("ec shard unmount %v: %v", req, err)
|
|
} else {
|
|
glog.V(2).Infof("ec shard unmount %v", req)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmount %d.%d: %v", req.VolumeId, shardId, err)
|
|
}
|
|
}
|
|
|
|
return &volume_server_pb.VolumeEcShardsUnmountResponse{}, nil
|
|
}
|
|
|
|
func (vs *VolumeServer) VolumeEcShardRead(req *volume_server_pb.VolumeEcShardReadRequest, stream volume_server_pb.VolumeServer_VolumeEcShardReadServer) error {
|
|
|
|
// Resolve the shard together with the EcVolume on the disk that owns it,
|
|
// rather than a first-match volume on a sibling disk: on a multi-disk server
|
|
// those can belong to different encode generations, and the guard must
|
|
// validate the identity of the volume whose bytes we serve.
|
|
ecVolume, ecShard, found := vs.store.FindEcVolumeWithShard(needle.VolumeId(req.VolumeId), erasure_coding.ShardId(req.ShardId))
|
|
if !found {
|
|
return fmt.Errorf("not found ec shard %d.%d", req.VolumeId, req.ShardId)
|
|
}
|
|
// Reject a shard whose identity doesn't match the caller's index; the caller
|
|
// then recovers from parity. Lenient only when the caller has no identity
|
|
// (pre-upgrade reader): a known caller must not accept an unstamped holder,
|
|
// which would serve a stale pre-upgrade shard.
|
|
if req.EncodeTsNs != 0 && req.EncodeTsNs != ecVolume.EncodeTsNs {
|
|
return fmt.Errorf("ec shard %d.%d belongs to a different encode run", req.VolumeId, req.ShardId)
|
|
}
|
|
|
|
if req.FileKey != 0 {
|
|
_, size, _ := ecVolume.FindNeedleFromEcx(types.Uint64ToNeedleId(req.FileKey))
|
|
if size.IsDeleted() {
|
|
return stream.Send(&volume_server_pb.VolumeEcShardReadResponse{
|
|
IsDeleted: true,
|
|
EncodeTsNs: ecVolume.EncodeTsNs,
|
|
})
|
|
}
|
|
}
|
|
|
|
bufSize := req.Size
|
|
if bufSize > BufferSizeLimit {
|
|
bufSize = BufferSizeLimit
|
|
}
|
|
buffer := make([]byte, bufSize)
|
|
|
|
startOffset, bytesToRead := req.Offset, req.Size
|
|
|
|
for bytesToRead > 0 {
|
|
// min of bytesToRead and bufSize
|
|
bufferSize := bufSize
|
|
if bufferSize > bytesToRead {
|
|
bufferSize = bytesToRead
|
|
}
|
|
bytesread, err := ecShard.ReadAt(buffer[0:bufferSize], startOffset)
|
|
|
|
// println("read", ecShard.FileName(), "startOffset", startOffset, bytesread, "bytes, with target", bufferSize)
|
|
if bytesread > 0 {
|
|
|
|
if int64(bytesread) > bytesToRead {
|
|
bytesread = int(bytesToRead)
|
|
}
|
|
err = stream.Send(&volume_server_pb.VolumeEcShardReadResponse{
|
|
Data: buffer[:bytesread],
|
|
EncodeTsNs: ecVolume.EncodeTsNs,
|
|
})
|
|
if err != nil {
|
|
// println("sending", bytesread, "bytes err", err.Error())
|
|
return err
|
|
}
|
|
|
|
startOffset += int64(bytesread)
|
|
bytesToRead -= int64(bytesread)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (vs *VolumeServer) VolumeEcBlobDelete(ctx context.Context, req *volume_server_pb.VolumeEcBlobDeleteRequest) (*volume_server_pb.VolumeEcBlobDeleteResponse, error) {
|
|
if err := vs.CheckMaintenanceMode(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
glog.V(0).Infof("VolumeEcBlobDelete: %v", req)
|
|
|
|
resp := &volume_server_pb.VolumeEcBlobDeleteResponse{}
|
|
|
|
for _, location := range vs.store.Locations {
|
|
if localEcVolume, found := location.FindEcVolume(needle.VolumeId(req.VolumeId)); found {
|
|
|
|
_, size, _, err := localEcVolume.LocateEcShardNeedle(types.NeedleId(req.FileKey), needle.Version(req.Version))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locate in local ec volume: %w", err)
|
|
}
|
|
if size.IsDeleted() {
|
|
return resp, nil
|
|
}
|
|
|
|
err = localEcVolume.DeleteNeedleFromEcx(types.NeedleId(req.FileKey))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// VolumeEcShardsToVolume generates the .idx, .dat files from .ecx, .ecj and .ec01 ~ .ec14 files
|
|
func (vs *VolumeServer) VolumeEcShardsToVolume(ctx context.Context, req *volume_server_pb.VolumeEcShardsToVolumeRequest) (*volume_server_pb.VolumeEcShardsToVolumeResponse, error) {
|
|
if err := vs.CheckMaintenanceMode(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
glog.V(0).Infof("VolumeEcShardsToVolume: %v", req)
|
|
|
|
// Collect all EC shards (NewEcVolume will load EC config from .vif into v.ECContext)
|
|
// Use MaxShardCount (32) to support custom EC ratios up to 32 total shards
|
|
tempShards := make([]string, erasure_coding.MaxShardCount)
|
|
v, found := vs.store.CollectEcShards(needle.VolumeId(req.VolumeId), tempShards)
|
|
if !found {
|
|
return nil, fmt.Errorf("ec volume %d not found", req.VolumeId)
|
|
}
|
|
|
|
if v.Collection != req.Collection {
|
|
return nil, fmt.Errorf("existing collection:%v unexpected input: %v", v.Collection, req.Collection)
|
|
}
|
|
|
|
// Use EC context (already loaded from .vif) to determine data shard count
|
|
dataShards := v.ECContext.DataShards
|
|
|
|
// Defensive validation to prevent panics from corrupted ECContext
|
|
if dataShards <= 0 || dataShards > erasure_coding.MaxShardCount {
|
|
return nil, fmt.Errorf("invalid data shard count %d for volume %d (must be 1..%d)", dataShards, req.VolumeId, erasure_coding.MaxShardCount)
|
|
}
|
|
|
|
shardFileNames := tempShards[:dataShards]
|
|
glog.V(1).Infof("Using EC config from volume %d: %d data shards", req.VolumeId, dataShards)
|
|
|
|
// Verify all data shards are present
|
|
for shardId := 0; shardId < dataShards; shardId++ {
|
|
if shardFileNames[shardId] == "" {
|
|
return nil, fmt.Errorf("ec volume %d missing shard %d", req.VolumeId, shardId)
|
|
}
|
|
}
|
|
|
|
dataBaseFileName, indexBaseFileName := v.DataBaseFileName(), v.IndexBaseFileName()
|
|
if !util.FileExists(indexBaseFileName + ".ecx") {
|
|
indexBaseFileName = dataBaseFileName
|
|
}
|
|
|
|
// Merge .ecj deletions into .ecx so that HasLiveNeedles and FindDatFileSize
|
|
// see the full set of deleted needles. Without this, needles deleted after the
|
|
// last ecx rebuild would still appear live, causing the decoded .dat to include
|
|
// data that should be skipped and HasLiveNeedles to return a false positive.
|
|
if err := erasure_coding.RebuildEcxFile(indexBaseFileName); err != nil {
|
|
return nil, fmt.Errorf("RebuildEcxFile %s: %v", indexBaseFileName, err)
|
|
}
|
|
|
|
// If the EC index contains no live entries, decoding should be a no-op:
|
|
// just allow the caller to purge EC shards and do not generate an empty normal volume.
|
|
hasLive, err := erasure_coding.HasLiveNeedles(indexBaseFileName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("HasLiveNeedles %s: %w", indexBaseFileName, err)
|
|
}
|
|
if !hasLive {
|
|
return nil, status.Errorf(codes.FailedPrecondition, "ec volume %d %s", req.VolumeId, erasure_coding.EcNoLiveEntriesSubstring)
|
|
}
|
|
|
|
// calculate .dat file size
|
|
datFileSize, err := erasure_coding.FindDatFileSize(dataBaseFileName, indexBaseFileName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("FindDatFileSize %s: %v", dataBaseFileName, err)
|
|
}
|
|
|
|
// write .dat file from .ec00 ~ .ec09 files
|
|
if err := erasure_coding.WriteDatFile(dataBaseFileName, datFileSize, shardFileNames); err != nil {
|
|
return nil, fmt.Errorf("WriteDatFile %s: %v", dataBaseFileName, err)
|
|
}
|
|
|
|
// write .idx file from .ecx and .ecj files
|
|
if err := erasure_coding.WriteIdxFileFromEcIndex(indexBaseFileName); err != nil {
|
|
return nil, fmt.Errorf("WriteIdxFileFromEcIndex %s: %v", v.IndexBaseFileName(), err)
|
|
}
|
|
|
|
// The EC generation is gone; drop its bitrot sidecar(s) so a later EC
|
|
// re-encode cannot mistake a stale .ecsum for protection.
|
|
removeBitrotSidecars(dataBaseFileName)
|
|
if indexBaseFileName != dataBaseFileName {
|
|
removeBitrotSidecars(indexBaseFileName)
|
|
}
|
|
|
|
var volumeLocation *storage.DiskLocation
|
|
for _, location := range vs.store.Locations {
|
|
if candidate, found := location.FindEcVolume(needle.VolumeId(req.VolumeId)); found && candidate == v {
|
|
volumeLocation = location
|
|
break
|
|
}
|
|
}
|
|
if volumeLocation == nil {
|
|
return nil, fmt.Errorf("ec volume %d location not found for offline compaction", req.VolumeId)
|
|
}
|
|
|
|
if err := vs.store.CompactVolumeFiles(
|
|
needle.VolumeId(req.VolumeId),
|
|
v.Collection,
|
|
volumeLocation,
|
|
vs.needleMapKind,
|
|
vs.ldbTimout,
|
|
0,
|
|
vs.compactionBytePerSecond,
|
|
); err != nil {
|
|
glog.Errorf("CompactVolumeFiles %s: %v", dataBaseFileName, err)
|
|
}
|
|
|
|
return &volume_server_pb.VolumeEcShardsToVolumeResponse{}, nil
|
|
}
|
|
|
|
func (vs *VolumeServer) VolumeEcShardsInfo(ctx context.Context, req *volume_server_pb.VolumeEcShardsInfoRequest) (*volume_server_pb.VolumeEcShardsInfoResponse, error) {
|
|
glog.V(0).Infof("VolumeEcShardsInfo: volume %d", req.VolumeId)
|
|
|
|
vid := needle.VolumeId(req.GetVolumeId())
|
|
|
|
// Multi-disk volume servers register one EcVolume per DiskLocation
|
|
// that holds shards for the same vid: shards may be spread across
|
|
// disks while the .ecx lives on whichever disk owned the original
|
|
// .dat. Walk every DiskLocation here so the response reflects the
|
|
// full local shard set; the per-disk ecVolumesLock is taken inside
|
|
// DiskLocation.FindEcVolume.
|
|
var primary *erasure_coding.EcVolume
|
|
var seenShards erasure_coding.ShardBits
|
|
shardInfos := make([]*volume_server_pb.EcShardInfo, 0, erasure_coding.MaxShardCount)
|
|
for _, location := range vs.store.Locations {
|
|
ecv, ok := location.FindEcVolume(vid)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if primary == nil {
|
|
primary = ecv
|
|
}
|
|
for _, s := range ecv.Shards {
|
|
if seenShards.Has(s.ShardId) {
|
|
continue
|
|
}
|
|
seenShards = seenShards.Set(s.ShardId)
|
|
shardInfos = append(shardInfos, s.ToEcShardInfo())
|
|
}
|
|
}
|
|
if primary == nil {
|
|
return nil, fmt.Errorf("VolumeEcShardsInfo: EC volume %d not found", vid)
|
|
}
|
|
|
|
var files, filesDeleted, totalSize uint64
|
|
err := primary.WalkIndex(func(_ types.NeedleId, _ types.Offset, size types.Size) error {
|
|
// deleted files are counted when computing EC volume sizes. this aligns with VolumeStatus(),
|
|
// which reports the raw data backend file size, regardless of deleted files.
|
|
totalSize += uint64(size.Raw())
|
|
|
|
if size.IsDeleted() {
|
|
filesDeleted++
|
|
} else {
|
|
files++
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &volume_server_pb.VolumeEcShardsInfoResponse{
|
|
EcShardInfos: shardInfos,
|
|
FileCount: files,
|
|
FileDeletedCount: filesDeleted,
|
|
VolumeSize: totalSize,
|
|
}
|
|
|
|
return res, nil
|
|
}
|