Skip to content

Commit d51817c

Browse files
committed
cli: set a timeout for the initial connection ping
Note that this does not fully fix the referenced issue, but at least makes sure that API clients don't hang forever on the initialization step. See: #3652 Signed-off-by: Nick Santos <nick@tilt.dev>
1 parent b75c262 commit d51817c

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-2
lines changed

cli/command/cli.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import (
3535
notaryclient "github.com/theupdateframework/notary/client"
3636
)
3737

38+
const defaultInitTimeout = 2 * time.Second
39+
3840
// Streams is an interface which exposes the standard input and output streams
3941
type Streams interface {
4042
In() *streams.In
@@ -77,6 +79,7 @@ type DockerCli struct {
7779
currentContext string
7880
dockerEndpoint docker.Endpoint
7981
contextStoreConfig store.Config
82+
initTimeout time.Duration
8083
}
8184

8285
// DefaultVersion returns api.defaultVersion.
@@ -259,6 +262,9 @@ func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.
259262
}
260263

261264
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
265+
// NOTE(nick): Currently, this doesn't set any reasonable default timeouts, which
266+
// means that the CLI can hang forever on any operation. Repro steps:
267+
// https://github.com/docker/cli/issues/3652
262268
clientOpts, err := ep.ClientOpts()
263269
if err != nil {
264270
return nil, err
@@ -313,13 +319,21 @@ func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint
313319
}, nil
314320
}
315321

322+
func (cli *DockerCli) getInitTimeout() time.Duration {
323+
if cli.initTimeout != 0 {
324+
return cli.initTimeout
325+
}
326+
return defaultInitTimeout
327+
}
328+
316329
func (cli *DockerCli) initializeFromClient() {
317330
ctx := context.Background()
318-
if strings.HasPrefix(cli.DockerEndpoint().Host, "tcp://") {
331+
host := cli.DockerEndpoint().Host
332+
if strings.HasPrefix(host, "tcp://") || strings.HasPrefix(host, "unix://") {
319333
// @FIXME context.WithTimeout doesn't work with connhelper / ssh connections
320334
// time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
321335
var cancel func()
322-
ctx, cancel = context.WithTimeout(ctx, 2*time.Second)
336+
ctx, cancel = context.WithTimeout(ctx, cli.getInitTimeout())
323337
defer cancel()
324338
}
325339

cli/command/cli_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"context"
66
"fmt"
77
"io"
8+
"net"
89
"net/http"
910
"net/http/httptest"
1011
"os"
1112
"runtime"
1213
"strings"
1314
"testing"
15+
"time"
1416

1517
"github.com/docker/cli/cli/config"
1618
"github.com/docker/cli/cli/config/configfile"
@@ -165,6 +167,41 @@ func TestInitializeFromClient(t *testing.T) {
165167
}
166168
}
167169

170+
// Makes sure we don't hang forever on the initial connection.
171+
// https://github.com/docker/cli/issues/3652
172+
func TestInitializeFromClientHangs(t *testing.T) {
173+
dir := fs.NewDir(t, t.Name())
174+
defer dir.Remove()
175+
176+
socket := dir.Join("my.sock")
177+
l, err := net.Listen("unix", socket)
178+
assert.NilError(t, err)
179+
180+
received := false
181+
closeConn := make(chan struct{})
182+
183+
// Simulate a server that hangs on connections.
184+
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
185+
received = true
186+
<-closeConn
187+
_, _ = w.Write([]byte("OK"))
188+
}))
189+
ts.Listener = l
190+
ts.Start()
191+
defer ts.Close()
192+
193+
opts := &flags.CommonOptions{Hosts: []string{fmt.Sprintf("unix://%s", socket)}}
194+
configFile := &configfile.ConfigFile{}
195+
apiClient, err := NewAPIClientFromFlags(opts, configFile)
196+
assert.NilError(t, err)
197+
198+
cli := &DockerCli{client: apiClient}
199+
cli.initTimeout = time.Millisecond
200+
cli.Initialize(flags.NewClientOptions())
201+
assert.Assert(t, received)
202+
close(closeConn)
203+
}
204+
168205
// The CLI no longer disables/hides experimental CLI features, however, we need
169206
// to verify that existing configuration files do not break
170207
func TestExperimentalCLI(t *testing.T) {

0 commit comments

Comments
 (0)