Files
seaweedfs/weed/shell/shell_liner_test.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

86 lines
2.4 KiB
Go

package shell
import (
"errors"
"io"
"strings"
"testing"
"time"
)
type fakeCommand struct {
name string
err error
}
func (c *fakeCommand) Name() string { return c.name }
func (c *fakeCommand) Help() string { return "test helper" }
func (c *fakeCommand) HasTag(CommandTag) bool { return false }
func (c *fakeCommand) Do([]string, *CommandEnv, io.Writer) error { return c.err }
func TestProcessEachCmdReturnsErrors(t *testing.T) {
exit, err := processEachCmd("definitely.not.a.command", nil)
if exit {
t.Errorf("unknown command must not request exit")
}
if err == nil {
t.Errorf("unknown command must return an error")
}
if err != nil && !strings.Contains(err.Error(), "unknown command") {
t.Errorf("unexpected error for unknown command: %v", err)
}
exit, err = processEachCmd("exit", nil)
if !exit {
t.Errorf("exit must request exit")
}
if err != nil {
t.Errorf("exit must not return an error, got: %v", err)
}
exit, err = processEachCmd(" ", nil)
if exit || err != nil {
t.Errorf("blank input must be a no-op, got exit=%v err=%v", exit, err)
}
}
func TestRunShellConnectTimeout(t *testing.T) {
masters := "127.0.0.1:1"
filerGroup := ""
start := time.Now()
err := RunShell(ShellOptions{
Masters: &masters,
FilerGroup: &filerGroup,
ConnectTimeout: 200 * time.Millisecond,
})
if err == nil || !strings.Contains(err.Error(), "cannot connect to master") {
t.Fatalf("expected a master connection timeout, got: %v", err)
}
if elapsed := time.Since(start); elapsed > 10*time.Second {
t.Fatalf("RunShell should give up near the connect timeout, took %v", elapsed)
}
}
func TestProcessEachCmdPreservesRegisteredCommandError(t *testing.T) {
doErr := errors.New("daily_run: shard=3: recovery walk failed")
failing := &fakeCommand{name: "test.failing.command", err: doErr}
succeeding := &fakeCommand{name: "test.succeeding.command"}
Commands = append(Commands, failing, succeeding)
t.Cleanup(func() { Commands = Commands[:len(Commands)-2] })
exit, err := processEachCmd("test.failing.command -some -args", nil)
if exit {
t.Errorf("a failing command must not request exit")
}
if !errors.Is(err, doErr) {
t.Errorf("the command's own error must be preserved, got: %v", err)
}
exit, err = processEachCmd("test.succeeding.command", nil)
if exit || err != nil {
t.Errorf("a succeeding command must return no error, got exit=%v err=%v", exit, err)
}
}