mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-07 23:07:48 +02:00
* fix(shell): error on missing volume id in fsck, mergeVolumes, vacuum Three shell commands silently report success when -volumeId / -fromVolumeId / -toVolumeId names a volume the master doesn't know about: typos, already-deleted volumes, and stale scripts all look identical to a clean no-op, which is what made the confusion in #9116 take as long as it did to diagnose. - volume.fsck: filter at the per-datanode loop drops unknown ids and findExtraChunksInVolumeServers ends with totalOrphanChunkCount==0, printing "no orphan data". - fs.mergeVolumes: createMergePlan iterates only known volumes, so an unknown -fromVolumeId produces an empty plan and we print just the "max volume size: N MB" header (indistinguishable from "nothing to merge"). - volume.vacuum: the master's VacuumVolume RPC silently iterates matching volumes; a missing id returns success having done nothing. Validate the requested ids against the current topology up front and return an explicit "volume(s) not found on master: [X Y]" error. Also drop a stale duplicate `if err != nil` in volume.fsck.Do left over from a prior refactor. Surfaces #9116 follow-up from madalee-com. * address review: propagate reloadVolumesInfo error; dedupe vacuum missing ids - fs.mergeVolumes: c.reloadVolumesInfo's return was ignored. If the master is unreachable or VolumeList fails, c.volumes stays empty and the new validation block reports "fromVolumeId X not found on master" — masking the real connection/RPC failure. Return the wrapped error instead. - volume.vacuum: "volume.vacuum -volumeId 5,5,5" on a missing volume 5 listed [5 5 5] in the error. Collect missing ids in a set so each missing id appears once. * address review: reject fromVolumeId/toVolumeId values that overflow uint32 flag.Uint produces a uint (64-bit on amd64), and the existing cast to needle.VolumeId silently truncates to uint32. A typo like `-fromVolumeId=4294967297` would wrap to volume 1 and slip past every other validation, so the merge would run against a completely different volume than the operator intended. Bail out with an explicit error when the raw flag value exceeds the uint32 range, before the cast.
701 lines
23 KiB
Go
701 lines
23 KiB
Go
package shell
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"slices"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
"golang.org/x/exp/maps"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
)
|
|
|
|
func init() {
|
|
Commands = append(Commands, &commandFsMergeVolumes{})
|
|
}
|
|
|
|
type commandFsMergeVolumes struct {
|
|
volumes map[needle.VolumeId]*master_pb.VolumeInformationMessage
|
|
volumeSizeLimit uint64
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) Name() string {
|
|
return "fs.mergeVolumes"
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) Help() string {
|
|
return `re-locate chunks into target volumes and try to clear lighter volumes.
|
|
|
|
This would help clear half-full volumes and let vacuum system to delete them later.
|
|
|
|
fs.mergeVolumes [-toVolumeId=y] [-fromVolumeId=x] [-collection="*"] [-dir=/] [-apply]
|
|
`
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) HasTag(CommandTag) bool {
|
|
return false
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
|
|
|
|
fsMergeVolumesCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
|
dirArg := fsMergeVolumesCommand.String("dir", "/", "base directory to find and update files")
|
|
fromVolumeArg := fsMergeVolumesCommand.Uint("fromVolumeId", 0, "move chunks with this volume id")
|
|
toVolumeArg := fsMergeVolumesCommand.Uint("toVolumeId", 0, "change chunks to this volume id")
|
|
collectionArg := fsMergeVolumesCommand.String("collection", "*", "Name of collection to merge")
|
|
apply := fsMergeVolumesCommand.Bool("apply", false, "applying the metadata changes")
|
|
if err = fsMergeVolumesCommand.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := *dirArg
|
|
if dir != "/" {
|
|
dir = strings.TrimRight(dir, "/")
|
|
}
|
|
|
|
// flag.Uint is a 64-bit uint on amd64 but needle.VolumeId is uint32, so a
|
|
// value that overflows (e.g. 4294967297) silently wraps to a valid id
|
|
// like 1. Reject instead of wrapping.
|
|
const maxVolumeID = uint(^uint32(0))
|
|
if *fromVolumeArg > maxVolumeID {
|
|
return fmt.Errorf("fromVolumeId %d exceeds max volume id %d", *fromVolumeArg, maxVolumeID)
|
|
}
|
|
if *toVolumeArg > maxVolumeID {
|
|
return fmt.Errorf("toVolumeId %d exceeds max volume id %d", *toVolumeArg, maxVolumeID)
|
|
}
|
|
|
|
fromVolumeId := needle.VolumeId(*fromVolumeArg)
|
|
toVolumeId := needle.VolumeId(*toVolumeArg)
|
|
|
|
if err = c.reloadVolumesInfo(commandEnv.MasterClient); err != nil {
|
|
return fmt.Errorf("reload volumes info: %w", err)
|
|
}
|
|
|
|
// Reject unknown ids before createMergePlan silently produces an empty plan
|
|
// and we print just the "max volume size" header. That output is
|
|
// indistinguishable from a legitimate "nothing to merge" and hides typos,
|
|
// already-deleted volumes, and stale scripts.
|
|
if fromVolumeId != 0 {
|
|
if _, err := c.getVolumeInfoById(fromVolumeId); err != nil {
|
|
return fmt.Errorf("fromVolumeId %d not found on master", fromVolumeId)
|
|
}
|
|
}
|
|
if toVolumeId != 0 {
|
|
if _, err := c.getVolumeInfoById(toVolumeId); err != nil {
|
|
return fmt.Errorf("toVolumeId %d not found on master", toVolumeId)
|
|
}
|
|
}
|
|
|
|
if fromVolumeId != 0 && toVolumeId != 0 {
|
|
if fromVolumeId == toVolumeId {
|
|
return fmt.Errorf("no volume id changes, %d == %d", fromVolumeId, toVolumeId)
|
|
}
|
|
compatible, err := c.volumesAreCompatible(fromVolumeId, toVolumeId)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot determine volumes are compatible: %d and %d", fromVolumeId, toVolumeId)
|
|
}
|
|
if !compatible {
|
|
return fmt.Errorf("volume %d is not compatible with volume %d", fromVolumeId, toVolumeId)
|
|
}
|
|
fromSize := c.getVolumeSizeById(fromVolumeId)
|
|
toSize := c.getVolumeSizeById(toVolumeId)
|
|
if fromSize+toSize > c.volumeSizeLimit {
|
|
return fmt.Errorf(
|
|
"volume %d (%d MB) cannot merge into volume %d (%d MB_ due to volume size limit (%d MB)",
|
|
fromVolumeId, fromSize/1024/1024,
|
|
toVolumeId, toSize/1024/1024,
|
|
c.volumeSizeLimit/1024/1024,
|
|
)
|
|
}
|
|
}
|
|
|
|
plan, err := c.createMergePlan(*collectionArg, toVolumeId, fromVolumeId)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.printPlan(plan)
|
|
|
|
if len(plan) == 0 {
|
|
return nil
|
|
}
|
|
|
|
defer util_http.GetGlobalHttpClient().CloseIdleConnections()
|
|
|
|
lookupFn := filer.LookupFn(commandEnv)
|
|
|
|
return commandEnv.WithFilerClient(false, func(filerClient filer_pb.SeaweedFilerClient) error {
|
|
return filer_pb.TraverseBfs(context.Background(), commandEnv, util.FullPath(dir), func(parentPath util.FullPath, entry *filer_pb.Entry) error {
|
|
if entry.IsDirectory {
|
|
return nil
|
|
}
|
|
entryPath := parentPath.Child(entry.Name)
|
|
entryChanged := false
|
|
// Every successful moveChunk or rewriteManifestChunk leaves the old
|
|
// needle sitting on its source volume as a silent orphan — until
|
|
// now the source only shrank after a separate volume.fsck +
|
|
// volume.vacuum cycle, which is what made #9116 (comment 4282692876)
|
|
// look like mergeVolumes hadn't done anything. Track the old fids
|
|
// and delete them below after the filer update commits, so the
|
|
// filer never points at a fid we already deleted.
|
|
var movedSources []movedSourceNeedle
|
|
for i, chunk := range entry.Chunks {
|
|
if chunk.IsChunkManifest {
|
|
oldManifestFid := chunk.GetFileIdString()
|
|
oldManifestVid := chunk.Fid.VolumeId
|
|
newChunk, changed, subSources, mErr := c.rewriteManifestChunk(context.Background(), commandEnv, lookupFn, plan, entryPath, chunk, *apply)
|
|
if mErr != nil {
|
|
fmt.Printf("failed to rewrite manifest %s(%s): %v\n", entryPath, oldManifestFid, mErr)
|
|
continue
|
|
}
|
|
if !changed || !*apply {
|
|
continue
|
|
}
|
|
entry.Chunks[i] = newChunk
|
|
entryChanged = true
|
|
movedSources = append(movedSources, subSources...)
|
|
// The old manifest needle is always orphaned when we
|
|
// replace it with a freshly uploaded one, even when the
|
|
// rewrite was triggered by sub-chunk moves rather than the
|
|
// manifest volume itself being in the plan.
|
|
movedSources = append(movedSources, movedSourceNeedle{volumeId: oldManifestVid, fileId: oldManifestFid})
|
|
continue
|
|
}
|
|
|
|
chunkVolumeId := needle.VolumeId(chunk.Fid.VolumeId)
|
|
toVolumeId, found := plan[chunkVolumeId]
|
|
if !found {
|
|
continue
|
|
}
|
|
|
|
oldFid := chunk.GetFileIdString()
|
|
oldVid := chunk.Fid.VolumeId
|
|
fmt.Printf("move %s(%s)\n", entryPath, oldFid)
|
|
if !*apply {
|
|
continue
|
|
}
|
|
if mvErr := moveChunk(chunk, toVolumeId, commandEnv.MasterClient); mvErr != nil {
|
|
fmt.Printf("failed to move %s(%s): %v\n", entryPath, oldFid, mvErr)
|
|
continue
|
|
}
|
|
entryChanged = true
|
|
movedSources = append(movedSources, movedSourceNeedle{volumeId: oldVid, fileId: oldFid})
|
|
}
|
|
if entryChanged {
|
|
if uErr := filer_pb.UpdateEntry(context.Background(), filerClient, &filer_pb.UpdateEntryRequest{
|
|
Directory: string(parentPath),
|
|
Entry: entry,
|
|
}); uErr != nil {
|
|
fmt.Printf("failed to update %s: %v\n", entryPath, uErr)
|
|
// Filer still references the source fids. Deleting them
|
|
// now would lose data — abandon the cleanup for this
|
|
// entry and let fsck reconcile later.
|
|
return nil
|
|
}
|
|
c.deleteMovedSourceNeedles(commandEnv, entryPath, movedSources)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
// movedSourceNeedle is a needle that was copied out of its source volume by
|
|
// a move/rewrite operation and is safe to delete once the filer update that
|
|
// re-pointed references to the new location has committed.
|
|
type movedSourceNeedle struct {
|
|
volumeId uint32
|
|
fileId string
|
|
}
|
|
|
|
// deleteMovedSourceNeedles fans out BatchDelete RPCs to every replica of each
|
|
// source volume. Errors are logged but never returned — the source data is
|
|
// already orphan at this point, so a failed cleanup just leaves work for a
|
|
// later fsck. Propagating an error here would abort TraverseBfs and strand
|
|
// the remaining entries mid-merge, which is strictly worse.
|
|
func (c *commandFsMergeVolumes) deleteMovedSourceNeedles(commandEnv *CommandEnv, entryPath util.FullPath, sources []movedSourceNeedle) {
|
|
if len(sources) == 0 {
|
|
return
|
|
}
|
|
byVolume := make(map[uint32][]string)
|
|
for _, s := range sources {
|
|
byVolume[s.volumeId] = append(byVolume[s.volumeId], s.fileId)
|
|
}
|
|
for vid, fids := range byVolume {
|
|
locations, found := commandEnv.MasterClient.GetLocations(vid)
|
|
if !found {
|
|
fmt.Printf("source cleanup %s: no locations for volume %d\n", entryPath, vid)
|
|
continue
|
|
}
|
|
for _, loc := range locations {
|
|
results := operation.DeleteFileIdsAtOneVolumeServer(loc.ServerAddress(), commandEnv.option.GrpcDialOption, fids, false)
|
|
for _, r := range results {
|
|
// StatusNotModified means the needle was already deleted
|
|
// (e.g. a concurrent fsck purge or a replica that had
|
|
// already reconciled). That's the desired end state, so
|
|
// don't warn about it. Cast to int because r.Status is
|
|
// an int32 protobuf field and linters flag the mixed-type
|
|
// compare even though Go's untyped-constant rules make it
|
|
// valid.
|
|
if r.Error != "" && int(r.Status) != http.StatusNotModified {
|
|
fmt.Printf("source cleanup %s: delete %s on %v: %s\n", entryPath, r.FileId, loc.ServerAddress(), r.Error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) getVolumeInfoById(vid needle.VolumeId) (*master_pb.VolumeInformationMessage, error) {
|
|
info := c.volumes[vid]
|
|
var err error
|
|
if info == nil {
|
|
err = errors.New("cannot find volume")
|
|
}
|
|
return info, err
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) volumesAreCompatible(src needle.VolumeId, dest needle.VolumeId) (bool, error) {
|
|
srcInfo, err := c.getVolumeInfoById(src)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
destInfo, err := c.getVolumeInfoById(dest)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return (srcInfo.Collection == destInfo.Collection &&
|
|
srcInfo.Ttl == destInfo.Ttl &&
|
|
srcInfo.ReplicaPlacement == destInfo.ReplicaPlacement), nil
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) reloadVolumesInfo(masterClient *wdclient.MasterClient) error {
|
|
c.volumes = make(map[needle.VolumeId]*master_pb.VolumeInformationMessage)
|
|
|
|
return masterClient.WithClient(false, func(client master_pb.SeaweedClient) error {
|
|
volumes, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.volumeSizeLimit = volumes.GetVolumeSizeLimitMb() * 1024 * 1024
|
|
|
|
for _, dc := range volumes.TopologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, node := range rack.DataNodeInfos {
|
|
for _, disk := range node.DiskInfos {
|
|
for _, volume := range disk.VolumeInfos {
|
|
vid := needle.VolumeId(volume.Id)
|
|
if found := c.volumes[vid]; found == nil {
|
|
c.volumes[vid] = volume
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) createMergePlan(collection string, toVolumeId needle.VolumeId, fromVolumeId needle.VolumeId) (map[needle.VolumeId]needle.VolumeId, error) {
|
|
plan := make(map[needle.VolumeId]needle.VolumeId)
|
|
volumeIds := maps.Keys(c.volumes)
|
|
sort.Slice(volumeIds, func(a, b int) bool {
|
|
return c.volumes[volumeIds[b]].Size < c.volumes[volumeIds[a]].Size
|
|
})
|
|
|
|
l := len(volumeIds)
|
|
for i := 0; i < l; i++ {
|
|
volume := c.volumes[volumeIds[i]]
|
|
if volume.GetReadOnly() || c.getVolumeSize(volume) == 0 || (collection != "*" && collection != volume.GetCollection()) {
|
|
|
|
if fromVolumeId != 0 && volumeIds[i] == fromVolumeId || toVolumeId != 0 && volumeIds[i] == toVolumeId {
|
|
if volume.GetReadOnly() {
|
|
return nil, fmt.Errorf("volume %d is readonly", volumeIds[i])
|
|
}
|
|
if c.getVolumeSize(volume) == 0 {
|
|
return nil, fmt.Errorf("volume %d is empty", volumeIds[i])
|
|
}
|
|
}
|
|
volumeIds = slices.Delete(volumeIds, i, i+1)
|
|
i--
|
|
l--
|
|
}
|
|
}
|
|
for i := l - 1; i >= 0; i-- {
|
|
src := volumeIds[i]
|
|
if fromVolumeId != 0 && src != fromVolumeId {
|
|
continue
|
|
}
|
|
for j := 0; j < i; j++ {
|
|
candidate := volumeIds[j]
|
|
if toVolumeId != 0 && candidate != toVolumeId {
|
|
continue
|
|
}
|
|
if _, moving := plan[candidate]; moving {
|
|
continue
|
|
}
|
|
compatible, err := c.volumesAreCompatible(src, candidate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !compatible {
|
|
fmt.Printf("volume %d is not compatible with volume %d\n", src, candidate)
|
|
continue
|
|
}
|
|
candidatePlannedSize := c.getVolumeSizeBasedOnPlan(plan, candidate)
|
|
if candidatePlannedSize+c.getVolumeSizeById(src) > c.volumeSizeLimit {
|
|
fmt.Printf("volume %d (%d MB) merge into volume %d (%d MB, %d MB with plan) exceeds volume size limit (%d MB)\n",
|
|
src, c.getVolumeSizeById(src)/1024/1024,
|
|
candidate, c.getVolumeSizeById(candidate)/1024/1024, candidatePlannedSize/1024/1024,
|
|
c.volumeSizeLimit/1024/1024)
|
|
continue
|
|
}
|
|
plan[src] = candidate
|
|
break
|
|
}
|
|
}
|
|
|
|
return plan, nil
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) getVolumeSizeBasedOnPlan(plan map[needle.VolumeId]needle.VolumeId, vid needle.VolumeId) uint64 {
|
|
size := c.getVolumeSizeById(vid)
|
|
for src, dest := range plan {
|
|
if dest == vid {
|
|
size += c.getVolumeSizeById(src)
|
|
}
|
|
}
|
|
return size
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) getVolumeSize(volume *master_pb.VolumeInformationMessage) uint64 {
|
|
return volume.Size - volume.DeletedByteCount
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) getVolumeSizeById(vid needle.VolumeId) uint64 {
|
|
return c.getVolumeSize(c.volumes[vid])
|
|
}
|
|
|
|
func (c *commandFsMergeVolumes) printPlan(plan map[needle.VolumeId]needle.VolumeId) {
|
|
fmt.Printf("max volume size: %d MB\n", c.volumeSizeLimit/1024/1024)
|
|
reversePlan := make(map[needle.VolumeId][]needle.VolumeId)
|
|
for src, dest := range plan {
|
|
reversePlan[dest] = append(reversePlan[dest], src)
|
|
}
|
|
for dest, srcs := range reversePlan {
|
|
currentSize := c.getVolumeSizeById(dest)
|
|
for _, src := range srcs {
|
|
srcSize := c.getVolumeSizeById(src)
|
|
newSize := currentSize + srcSize
|
|
fmt.Printf(
|
|
"volume %d (%d MB) merge into volume %d (%d MB => %d MB)\n",
|
|
src, srcSize/1024/1024,
|
|
dest, currentSize/1024/1024, newSize/1024/1024,
|
|
)
|
|
currentSize = newSize
|
|
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// rewriteManifestChunk walks the sub-chunks referenced by a manifest chunk and
|
|
// moves any that live in a source volume from the merge plan. If any sub-chunk
|
|
// moves, or the manifest chunk itself lives in a source volume, the manifest
|
|
// blob is re-serialized and uploaded to a freshly assigned file id.
|
|
//
|
|
// The returned movedSourceNeedle slice lists every source needle the caller
|
|
// should delete once the filer update commits — sub-chunks that were moved and
|
|
// nested manifest chunks that got rewritten. The OUTER manifest needle is the
|
|
// caller's responsibility to record, since only the caller knows its pre-move
|
|
// fid (this function's own chunk argument still reports the old fid on return,
|
|
// but that couples manifest-nesting logic to a fact that is easier to capture
|
|
// at the top-level callsite).
|
|
func (c *commandFsMergeVolumes) rewriteManifestChunk(
|
|
ctx context.Context,
|
|
commandEnv *CommandEnv,
|
|
lookupFn wdclient.LookupFileIdFunctionType,
|
|
plan map[needle.VolumeId]needle.VolumeId,
|
|
entryPath util.FullPath,
|
|
chunk *filer_pb.FileChunk,
|
|
apply bool,
|
|
) (*filer_pb.FileChunk, bool, []movedSourceNeedle, error) {
|
|
if !chunk.IsChunkManifest {
|
|
return chunk, false, nil, fmt.Errorf("not a manifest chunk: %s", chunk.GetFileIdString())
|
|
}
|
|
|
|
subChunks, err := filer.ResolveOneChunkManifest(ctx, lookupFn, chunk)
|
|
if err != nil {
|
|
return chunk, false, nil, err
|
|
}
|
|
|
|
var movedSources []movedSourceNeedle
|
|
anySubChanged := false
|
|
for i, sub := range subChunks {
|
|
if sub.IsChunkManifest {
|
|
oldSubManifestFid := sub.GetFileIdString()
|
|
oldSubManifestVid := sub.Fid.VolumeId
|
|
newSub, changed, nestedSources, rErr := c.rewriteManifestChunk(ctx, commandEnv, lookupFn, plan, entryPath, sub, apply)
|
|
if rErr != nil {
|
|
return chunk, false, nil, rErr
|
|
}
|
|
if changed {
|
|
subChunks[i] = newSub
|
|
anySubChanged = true
|
|
if apply {
|
|
movedSources = append(movedSources, nestedSources...)
|
|
// Nested manifest got replaced — its old needle is now
|
|
// orphan on the same volume it used to live on.
|
|
movedSources = append(movedSources, movedSourceNeedle{volumeId: oldSubManifestVid, fileId: oldSubManifestFid})
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
subVid := needle.VolumeId(sub.Fid.VolumeId)
|
|
toVid, ok := plan[subVid]
|
|
if !ok {
|
|
continue
|
|
}
|
|
oldSubFid := sub.GetFileIdString()
|
|
oldSubVid := sub.Fid.VolumeId
|
|
fmt.Printf("move %s(%s) [inside manifest %s]\n", entryPath, oldSubFid, chunk.GetFileIdString())
|
|
if !apply {
|
|
anySubChanged = true
|
|
continue
|
|
}
|
|
if mErr := moveChunk(sub, toVid, commandEnv.MasterClient); mErr != nil {
|
|
fmt.Printf("failed to move %s(%s): %v\n", entryPath, oldSubFid, mErr)
|
|
continue
|
|
}
|
|
anySubChanged = true
|
|
movedSources = append(movedSources, movedSourceNeedle{volumeId: oldSubVid, fileId: oldSubFid})
|
|
}
|
|
|
|
manifestVid := needle.VolumeId(chunk.Fid.VolumeId)
|
|
_, manifestMustMove := plan[manifestVid]
|
|
|
|
if !anySubChanged && !manifestMustMove {
|
|
return chunk, false, nil, nil
|
|
}
|
|
|
|
fmt.Printf("rewrite manifest %s(%s)\n", entryPath, chunk.GetFileIdString())
|
|
if !apply {
|
|
// Propagate "would change" so nested callers also announce their
|
|
// rewrites in dry-run mode. The top-level caller gates any actual
|
|
// filer writes on *apply, so returning true here is safe.
|
|
return chunk, true, nil, nil
|
|
}
|
|
|
|
filer_pb.BeforeEntrySerialization(subChunks)
|
|
defer filer_pb.AfterEntryDeserialization(subChunks)
|
|
data, err := proto.Marshal(&filer_pb.FileChunkManifest{Chunks: subChunks})
|
|
if err != nil {
|
|
return chunk, false, nil, fmt.Errorf("marshal manifest: %w", err)
|
|
}
|
|
|
|
collection := ""
|
|
if info, ok := c.volumes[manifestVid]; ok {
|
|
collection = info.Collection
|
|
}
|
|
newChunk, err := c.uploadManifestChunk(ctx, commandEnv, entryPath, collection, plan, data)
|
|
if err != nil {
|
|
return chunk, false, nil, fmt.Errorf("upload new manifest: %w", err)
|
|
}
|
|
|
|
newChunk.IsChunkManifest = true
|
|
newChunk.Offset = chunk.Offset
|
|
newChunk.Size = chunk.Size
|
|
if chunk.ModifiedTsNs != 0 {
|
|
newChunk.ModifiedTsNs = chunk.ModifiedTsNs
|
|
}
|
|
newChunk.FileId = ""
|
|
|
|
return newChunk, true, movedSources, nil
|
|
}
|
|
|
|
// uploadManifestChunk assigns a fresh file id via the filer and uploads the
|
|
// given manifest bytes to the chosen volume server. If the filer picks a
|
|
// volume that is a source in the merge plan, the assignment is rejected and
|
|
// retried up to manifestAssignAttempts times — otherwise the replacement
|
|
// manifest would land on the very volume this command is trying to empty.
|
|
func (c *commandFsMergeVolumes) uploadManifestChunk(
|
|
ctx context.Context,
|
|
commandEnv *CommandEnv,
|
|
entryPath util.FullPath,
|
|
collection string,
|
|
plan map[needle.VolumeId]needle.VolumeId,
|
|
data []byte,
|
|
) (*filer_pb.FileChunk, error) {
|
|
const manifestAssignAttempts = 10
|
|
var assignResp *filer_pb.AssignVolumeResponse
|
|
if err := commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
for attempt := 1; attempt <= manifestAssignAttempts; attempt++ {
|
|
resp, err := client.AssignVolume(ctx, &filer_pb.AssignVolumeRequest{
|
|
Count: 1,
|
|
Collection: collection,
|
|
Path: string(entryPath),
|
|
ExpectedDataSize: uint64(len(data)),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Error != "" {
|
|
return fmt.Errorf("%s", resp.Error)
|
|
}
|
|
fid, parseErr := filer_pb.ToFileIdObject(resp.FileId)
|
|
if parseErr != nil {
|
|
return fmt.Errorf("parse assigned fid %q: %w", resp.FileId, parseErr)
|
|
}
|
|
if _, isSource := plan[needle.VolumeId(fid.VolumeId)]; !isSource {
|
|
assignResp = resp
|
|
return nil
|
|
}
|
|
fmt.Printf("rejecting manifest assignment to merge-source volume %d (attempt %d/%d)\n",
|
|
fid.VolumeId, attempt, manifestAssignAttempts)
|
|
}
|
|
return fmt.Errorf("filer kept assigning manifest uploads to merge-source volumes after %d attempts", manifestAssignAttempts)
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("assign volume: %w", err)
|
|
}
|
|
if assignResp.Location == nil {
|
|
return nil, fmt.Errorf("assign volume returned no location")
|
|
}
|
|
|
|
uploader, err := operation.NewUploader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uploadUrl := fmt.Sprintf("http://%s/%s", commandEnv.AdjustedUrl(assignResp.Location), assignResp.FileId)
|
|
|
|
jwt := security.EncodedJwt(assignResp.Auth)
|
|
if jwt == "" {
|
|
v := util.GetViper()
|
|
if signingKey := v.GetString("jwt.signing.key"); signingKey != "" {
|
|
expiresAfterSec := v.GetInt("jwt.signing.expires_after_seconds")
|
|
jwt = security.GenJwtForVolumeServer(security.SigningKey(signingKey), expiresAfterSec, assignResp.FileId)
|
|
}
|
|
}
|
|
|
|
uploadResult, err := uploader.UploadData(ctx, data, &operation.UploadOption{
|
|
UploadUrl: uploadUrl,
|
|
Jwt: jwt,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if uploadResult.Error != "" {
|
|
return nil, fmt.Errorf("upload: %s", uploadResult.Error)
|
|
}
|
|
|
|
return uploadResult.ToPbFileChunk(assignResp.FileId, 0, time.Now().UnixNano()), nil
|
|
}
|
|
|
|
func moveChunk(chunk *filer_pb.FileChunk, toVolumeId needle.VolumeId, masterClient *wdclient.MasterClient) error {
|
|
fromFid := needle.NewFileId(needle.VolumeId(chunk.Fid.VolumeId), chunk.Fid.FileKey, chunk.Fid.Cookie)
|
|
toFid := needle.NewFileId(toVolumeId, chunk.Fid.FileKey, chunk.Fid.Cookie)
|
|
|
|
downloadURLs, err := masterClient.LookupVolumeServerUrl(fromFid.VolumeId.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
downloadURL := fmt.Sprintf("http://%s/%s?readDeleted=true", downloadURLs[0], fromFid.String())
|
|
|
|
uploadURLs, err := masterClient.LookupVolumeServerUrl(toVolumeId.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
uploadURL := fmt.Sprintf("http://%s/%s", uploadURLs[0], toFid.String())
|
|
|
|
resp, reader, err := readUrl(downloadURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer util_http.CloseResponse(resp)
|
|
defer reader.Close()
|
|
|
|
var filename string
|
|
|
|
contentDisposition := resp.Header.Get("Content-Disposition")
|
|
if len(contentDisposition) > 0 {
|
|
idx := strings.Index(contentDisposition, "filename=")
|
|
if idx != -1 {
|
|
filename = contentDisposition[idx+len("filename="):]
|
|
filename = strings.Trim(filename, "\"")
|
|
}
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
isCompressed := resp.Header.Get("Content-Encoding") == "gzip"
|
|
md5 := resp.Header.Get("Content-MD5")
|
|
|
|
uploader, err := operation.NewUploader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
v := util.GetViper()
|
|
signingKey := v.GetString("jwt.signing.key")
|
|
var jwt security.EncodedJwt
|
|
if signingKey != "" {
|
|
expiresAfterSec := v.GetInt("jwt.signing.expires_after_seconds")
|
|
jwt = security.GenJwtForVolumeServer(security.SigningKey(signingKey), expiresAfterSec, toFid.String())
|
|
}
|
|
|
|
_, err, _ = uploader.Upload(context.Background(), reader, &operation.UploadOption{
|
|
UploadUrl: uploadURL,
|
|
Filename: filename,
|
|
IsInputCompressed: isCompressed,
|
|
Cipher: false,
|
|
MimeType: contentType,
|
|
PairMap: nil,
|
|
Md5: md5,
|
|
Jwt: security.EncodedJwt(jwt),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chunk.Fid.VolumeId = uint32(toVolumeId)
|
|
chunk.FileId = ""
|
|
|
|
return nil
|
|
}
|
|
|
|
func readUrl(fileUrl string) (*http.Response, io.ReadCloser, error) {
|
|
|
|
req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
req.Header.Add("Accept-Encoding", "gzip")
|
|
|
|
r, err := util_http.GetGlobalHttpClient().Do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if r.StatusCode >= 400 {
|
|
util_http.CloseResponse(r)
|
|
return nil, nil, fmt.Errorf("%s: %s", fileUrl, r.Status)
|
|
}
|
|
|
|
return r, r.Body, nil
|
|
}
|