Files
seaweedfs/weed/shell/commands.go
T
Chris LuandDevin 85c1b990b1 shell: bound the initial master connection wait behind -connectTimeout (#11922)
* shell: bound the initial master connection wait behind -connectTimeout

weed shell waited on WaitUntilConnected with a background context, so
an unreachable master hung the command forever. The wait now runs on
a 30s (configurable) timeout and reports the failure instead.

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

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

* shell: stop the master reconnect loop when RunShell returns

KeepConnectedToMaster ran on context.Background(), so once RunShell
returned — including the new connect-timeout early return — the
reconnect goroutine kept dialing forever, and repeated RunShell calls
accumulated one each. Give it its own cancellable context, separate
from connectCtx whose deadline must not cut reconnects under a live
shell.

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

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

* shell: bound filer discovery by -connectTimeout too

The discovery WithClient and ListClusterNodes ran on the unbounded
background context, so a master disconnect after the initial connect —
or a persistently failing ListClusterNodes — could block startup past
the deadline. Reusing connectCtx bounds the whole startup phase.

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

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

* shell: report filer discovery failures instead of silently losing the filer target

* shell: give filer discovery its own budget after master connects

Discovery shared connectCtx, so a connection that landed near the
-connectTimeout deadline left ListClusterNodes almost no time and no room
for WithClient to retry. A fresh timeout keeps discovery bounded without
starving it.

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

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

* shell: cover the -connectTimeout master wait with a test

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

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

* shell: note filer discovery in the -connectTimeout help text

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

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

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-21 20:17:51 -07:00

225 lines
6.0 KiB
Go

package shell
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/cluster"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/wdclient"
"github.com/seaweedfs/seaweedfs/weed/wdclient/exclusive_locks"
)
type ShellOptions struct {
Masters *string
GrpcDialOption grpc.DialOption
// shell transient context
FilerHost string
FilerPort int64
FilerGroup *string
FilerAddress pb.ServerAddress
Directory string
Debug bool
ConnectTimeout time.Duration
}
type CommandEnv struct {
env map[string]string
MasterClient *wdclient.MasterClient
option *ShellOptions
locker *exclusive_locks.ExclusiveLocker
noLock bool
forceNoLock bool
verbose bool
}
func NewCommandEnv(options *ShellOptions) *CommandEnv {
ce := &CommandEnv{
env: make(map[string]string),
MasterClient: wdclient.NewMasterClient(options.GrpcDialOption, *options.FilerGroup, pb.AdminShellClient, "", "", "", *pb.ServerAddresses(*options.Masters).ToServiceDiscovery()),
option: options,
noLock: false,
}
ce.locker = exclusive_locks.NewExclusiveLocker(ce.MasterClient, cluster.AdminShellLockName)
return ce
}
func (ce *CommandEnv) parseUrl(input string) (path string, err error) {
if strings.HasPrefix(input, "http") {
err = fmt.Errorf("http://<filer>:<port> prefix is not supported any more")
return
}
if !strings.HasPrefix(input, "/") {
input = util.Join(ce.option.Directory, input)
}
return input, err
}
func (ce *CommandEnv) isDirectory(path string) bool {
return ce.checkDirectory(path) == nil
}
func (ce *CommandEnv) confirmIsLocked(args []string) error {
if ce.noLock || ce.forceNoLock {
return nil
}
if ce.locker.IsLocked() {
return nil
}
ce.locker.SetMessage(fmt.Sprintf("%v", args))
return fmt.Errorf("need to run \"lock\" first to continue")
}
func (ce *CommandEnv) SetNoLock(noLock bool) {
if ce == nil {
return
}
ce.noLock = noLock
}
func (ce *CommandEnv) ForceNoLock() {
if ce == nil {
return
}
ce.forceNoLock = true
}
func (ce *CommandEnv) isLocked() bool {
if ce == nil {
return true
}
if ce.noLock || ce.forceNoLock {
return true
}
return ce.locker.IsLocked()
}
// shellLockHolder asks the master who currently holds the cluster-wide shell
// lock. Best effort: masters without GetAdminLockStatus report no holder, and
// the lookup is bounded so an unresponsive master cannot hang the shell.
func (ce *CommandEnv) shellLockHolder() (clientName string, message string, held bool) {
attemptCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ce.MasterClient.WithClient(attemptCtx, false, func(client master_pb.SeaweedClient) error {
resp, err := client.GetAdminLockStatus(attemptCtx, &master_pb.GetAdminLockStatusRequest{
LockName: cluster.AdminShellLockName,
})
if err == nil && resp.IsLocked {
clientName, message, held = resp.ClientName, resp.Message, true
}
return err
})
return
}
func (ce *CommandEnv) checkDirectory(path string) error {
dir, name := util.FullPath(path).DirAndName()
exists, err := filer_pb.Exists(context.Background(), ce, dir, name, true)
if !exists {
return fmt.Errorf("%s is not a directory", path)
}
return err
}
var _ = filer_pb.FilerClient(&CommandEnv{})
func (ce *CommandEnv) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
return pb.WithGrpcFilerClient(streamingMode, 0, ce.option.FilerAddress, ce.option.GrpcDialOption, fn)
}
func (ce *CommandEnv) AdjustedUrl(location *filer_pb.Location) string {
return location.Url
}
func (ce *CommandEnv) GetDataCenter() string {
return ce.MasterClient.GetDataCenter()
}
func findInputDirectory(args []string) (input string) {
input = "."
if len(args) > 0 {
input = args[len(args)-1]
if strings.HasPrefix(input, "-") {
input = "."
}
}
return input
}
// isHelpRequest checks if the args contain a help flag (-h, --help, or -help)
// It also handles combined short flags like -lh or -hl
func isHelpRequest(args []string) bool {
for _, arg := range args {
// Check for exact matches
if arg == "-h" || arg == "--help" || arg == "-help" {
return true
}
// Check for combined short flags (e.g., -lh, -hl, -rfh)
// Limit to reasonable length (2-4 chars total) to avoid matching long options like -verbose
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && len(arg) > 1 && len(arg) <= 4 {
for _, char := range arg[1:] {
if char == 'h' {
return true
}
}
}
}
return false
}
// handleHelpRequest checks for help flags and prints the help message if requested.
// It returns true if the help message was printed, indicating the command should exit.
func handleHelpRequest(c command, args []string, writer io.Writer) bool {
if isHelpRequest(args) {
fmt.Fprintln(writer, c.Help())
return true
}
return false
}
func readNeedleStatus(grpcDialOption grpc.DialOption, sourceVolumeServer pb.ServerAddress, volumeId uint32, needleValue needle_map.NeedleValue) (resp *volume_server_pb.VolumeNeedleStatusResponse, err error) {
err = operation.WithVolumeServerClient(false, sourceVolumeServer, grpcDialOption,
func(client volume_server_pb.VolumeServerClient) error {
if resp, err = client.VolumeNeedleStatus(context.Background(), &volume_server_pb.VolumeNeedleStatusRequest{
VolumeId: volumeId,
NeedleId: uint64(needleValue.Key),
}); err != nil {
return err
}
return nil
},
)
return
}
func getCollectionName(commandEnv *CommandEnv, bucket string) string {
if *commandEnv.option.FilerGroup != "" {
return fmt.Sprintf("%s_%s", *commandEnv.option.FilerGroup, bucket)
}
return bucket
}