mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-28 19:13:17 +00:00
* regenerate master_grpc.pb.go with protoc-gen-go-grpc v1.6.2 The other generated pb files are already on v1.6.2; this one was stale. * shell: keep unlock from racing the lease renewal A renewal RPC in flight while ReleaseLock runs re-creates the lock on the master after the release deletes it, and can blank the client name if the renewal reads it mid-release. The stale-token release is then ignored, so the lock stays held (sometimes anonymously) until it expires. Serialize the renew and release RPCs, and set the client name before flipping isLocked so the renewal never sends a partial acquisition. * shell: restart lease renewal after a failed renewal The renewal goroutine exits on error but never cleared its running flag, so later locks in the same process were never renewed and silently expired after ten seconds. * shell: show who holds the cluster lock A blocked lock command gave no hint that another client holds the lock (the refusals only surfaced at -v=2), and cluster.status reported the shell's own lock state as if it were the cluster's. Add a GetAdminLockStatus RPC to the master so lock prints the holder before blocking and cluster.status shows the actual cluster-wide holder. Both degrade silently against masters without the RPC. * shell: bound admin lock RPC attempts with timeouts The lease, renew, release, and holder-status calls all ran without a deadline, so an unresponsive master could hang the renewal goroutine, an unlock (which now waits on the renewal mutex), or the shell prompt. Give each attempt its own short context; the retry loops still resolve a fresh leader on the next try. * master: reject admin token release on non-leaders A follower holds no lock state, so it answered a release with success while the leader kept the lock until expiry. Refuse like LeaseAdminToken does so the client can try the leader instead. * shell: leave the lock release call unbounded A release cut short by a deadline leaves the lock held on the master until it expires, so a slow master would turn every unlock into a ten-second ghost lock. Restore the single fire-and-forget attempt; the timeouts stay on the lease and renew paths, where a stalled call forfeits the lease anyway. * shell: release only the token unlock started with A RequestLock racing a slow release (the admin presence lock does this on shutdown) could have its freshly acquired token sent in the release request or zeroed by the trailing stores. Capture the token once under the mutex and compare on clear so a concurrent acquisition survives an in-flight unlock.
224 lines
5.9 KiB
Go
224 lines
5.9 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
|
|
}
|
|
|
|
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
|
|
// each attempt is bounded so an unresponsive master cannot hang the shell.
|
|
func (ce *CommandEnv) shellLockHolder() (clientName string, message string, held bool) {
|
|
ce.MasterClient.WithClient(false, func(client master_pb.SeaweedClient) error {
|
|
attemptCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
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
|
|
}
|