Fix attach race condition, improve unit tests, make sure the container is started before unblocking Start

This commit is contained in:
Guillaume J. Charmes 2013-10-08 18:42:53 -07:00 committed by Victor Vieux
parent 333bc23f21
commit 3e014aa662
6 changed files with 106 additions and 55 deletions

View File

@ -371,8 +371,8 @@ func TestGetContainersJSON(t *testing.T) {
if err := json.Unmarshal(r.Body.Bytes(), &containers); err != nil { if err := json.Unmarshal(r.Body.Bytes(), &containers); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(containers) != 1 { if len(containers) != beginLen+1 {
t.Fatalf("Expected %d container, %d found (started with: %d)", 1, len(containers), beginLen) t.Fatalf("Expected %d container, %d found (started with: %d)", beginLen+1, len(containers), beginLen)
} }
if containers[0].ID != container.ID { if containers[0].ID != container.ID {
t.Fatalf("Container ID mismatch. Expected: %s, received: %s\n", container.ID, containers[0].ID) t.Fatalf("Container ID mismatch. Expected: %s, received: %s\n", container.ID, containers[0].ID)

View File

@ -1597,12 +1597,10 @@ func (cli *DockerCli) CmdRun(args ...string) error {
cli.forwardAllSignals(runResult.ID) cli.forwardAllSignals(runResult.ID)
} }
//start the container var (
if _, _, err = cli.call("POST", "/containers/"+runResult.ID+"/start", hostConfig); err != nil { wait chan struct{}
return err errCh chan error
} )
var wait chan struct{}
if !config.AttachStdout && !config.AttachStderr { if !config.AttachStdout && !config.AttachStderr {
// Make this asynchrone in order to let the client write to stdin before having to read the ID // Make this asynchrone in order to let the client write to stdin before having to read the ID
@ -1621,7 +1619,6 @@ func (cli *DockerCli) CmdRun(args ...string) error {
} }
v := url.Values{} v := url.Values{}
v.Set("logs", "1")
v.Set("stream", "1") v.Set("stream", "1")
var out, stderr io.Writer var out, stderr io.Writer
@ -1641,18 +1638,18 @@ func (cli *DockerCli) CmdRun(args ...string) error {
} }
} }
signals := make(chan os.Signal, 1) errCh = utils.Go(func() error {
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) return cli.hijack("POST", "/containers/"+runResult.ID+"/attach?"+v.Encode(), config.Tty, cli.in, out, stderr)
go func() { })
for sig := range signals { }
fmt.Printf("\nReceived signal: %s; cleaning up\n", sig)
if err := cli.CmdStop("-t", "4", runResult.ID); err != nil {
fmt.Printf("failed to stop container: %v", err)
}
}
}()
if err := cli.hijack("POST", "/containers/"+runResult.ID+"/attach?"+v.Encode(), config.Tty, cli.in, out, stderr); err != nil { //start the container
if _, _, err = cli.call("POST", "/containers/"+runResult.ID+"/start", hostConfig); err != nil {
return err
}
if errCh != nil {
if err := <-errCh; err != nil {
utils.Debugf("Error hijack: %s", err) utils.Debugf("Error hijack: %s", err)
return err return err
} }
@ -1667,8 +1664,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
return err return err
} }
if autoRemove { if autoRemove {
_, _, err = cli.call("DELETE", "/containers/"+runResult.ID, nil) if _, _, err = cli.call("DELETE", "/containers/"+runResult.ID, nil); err != nil {
if err != nil {
return err return err
} }
} }
@ -1753,6 +1749,7 @@ func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int,
return nil, -1, err return nil, -1, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, -1, err return nil, -1, err

View File

@ -84,7 +84,7 @@ func TestRunHostname(t *testing.T) {
} }
}) })
setTimeout(t, "CmdRun timed out", 5*time.Second, func() { setTimeout(t, "CmdRun timed out", 10*time.Second, func() {
<-c <-c
}) })
@ -115,7 +115,7 @@ func TestRunWorkdir(t *testing.T) {
} }
}) })
setTimeout(t, "CmdRun timed out", 5*time.Second, func() { setTimeout(t, "CmdRun timed out", 10*time.Second, func() {
<-c <-c
}) })
@ -399,6 +399,8 @@ func TestRunDetach(t *testing.T) {
} }
}) })
closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
// wait for CmdRun to return // wait for CmdRun to return
setTimeout(t, "Waiting for CmdRun timed out", 15*time.Second, func() { setTimeout(t, "Waiting for CmdRun timed out", 15*time.Second, func() {
<-ch <-ch
@ -422,30 +424,52 @@ func TestAttachDetach(t *testing.T) {
cli := NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) cli := NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr)
defer cleanup(globalRuntime) defer cleanup(globalRuntime)
go stdout.Read(make([]byte, 1024)) ch := make(chan struct{})
setTimeout(t, "Starting container timed out", 2*time.Second, func() { go func() {
defer close(ch)
if err := cli.CmdRun("-i", "-t", "-d", unitTestImageID, "cat"); err != nil { if err := cli.CmdRun("-i", "-t", "-d", unitTestImageID, "cat"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) }()
container := globalRuntime.List()[0] var container *Container
setTimeout(t, "Reading container's id timed out", 10*time.Second, func() {
buf := make([]byte, 1024)
n, err := stdout.Read(buf)
if err != nil {
t.Fatal(err)
}
container = globalRuntime.List()[0]
if strings.Trim(string(buf[:n]), " \r\n") != container.ShortID() {
t.Fatalf("Wrong ID received. Expect %s, received %s", container.ShortID(), buf[:n])
}
})
setTimeout(t, "Starting container timed out", 10*time.Second, func() {
<-ch
})
stdin, stdinPipe = io.Pipe() stdin, stdinPipe = io.Pipe()
stdout, stdoutPipe = io.Pipe() stdout, stdoutPipe = io.Pipe()
cli = NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) cli = NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr)
ch := make(chan struct{}) ch = make(chan struct{})
go func() { go func() {
defer close(ch) defer close(ch)
if err := cli.CmdAttach(container.ShortID()); err != nil { if err := cli.CmdAttach(container.ShortID()); err != nil {
t.Fatal(err) if err != io.ErrClosedPipe {
t.Fatal(err)
}
} }
}() }()
setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() { setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil { if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil {
t.Fatal(err) if err != io.ErrClosedPipe {
t.Fatal(err)
}
} }
}) })
@ -455,6 +479,7 @@ func TestAttachDetach(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
}) })
closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
// wait for CmdRun to return // wait for CmdRun to return
setTimeout(t, "Waiting for CmdAttach timed out", 15*time.Second, func() { setTimeout(t, "Waiting for CmdAttach timed out", 15*time.Second, func() {
@ -568,7 +593,7 @@ func TestRunAutoRemove(t *testing.T) {
} }
}) })
setTimeout(t, "CmdRun timed out", 5*time.Second, func() { setTimeout(t, "CmdRun timed out", 10*time.Second, func() {
<-c <-c
}) })

View File

@ -99,6 +99,8 @@ type BindMap struct {
} }
var ( var (
ErrContainerStart = errors.New("The container failed to start. Unkown error")
ErrContainerStartTimeout = errors.New("The container failed to start due to timed out.")
ErrInvalidWorikingDirectory = errors.New("The working directory is invalid. It needs to be an absolute path.") ErrInvalidWorikingDirectory = errors.New("The working directory is invalid. It needs to be an absolute path.")
ErrConflictTtySigProxy = errors.New("TTY mode (-t) already imply signal proxying (-sig-proxy)") ErrConflictTtySigProxy = errors.New("TTY mode (-t) already imply signal proxying (-sig-proxy)")
ErrConflictAttachDetach = errors.New("Conflicting options: -a and -d") ErrConflictAttachDetach = errors.New("Conflicting options: -a and -d")
@ -838,12 +840,43 @@ func (container *Container) Start(hostConfig *HostConfig) (err error) {
container.ToDisk() container.ToDisk()
container.SaveHostConfig(hostConfig) container.SaveHostConfig(hostConfig)
go container.monitor(hostConfig) go container.monitor(hostConfig)
return nil
defer utils.Debugf("Container running: %v", container.State.Running)
// We wait for the container to be fully running.
// Timeout after 5 seconds. In case of broken pipe, just retry.
// Note: The container can run and finish correctly before
// the end of this loop
for now := time.Now(); time.Since(now) < 5*time.Second; {
// If the container dies while waiting for it, just reutrn
if !container.State.Running {
return nil
}
output, err := exec.Command("lxc-info", "-n", container.ID).CombinedOutput()
if err != nil {
utils.Debugf("Error with lxc-info: %s (%s)", err, output)
output, err = exec.Command("lxc-info", "-n", container.ID).CombinedOutput()
if err != nil {
utils.Debugf("Second Error with lxc-info: %s (%s)", err, output)
return err
}
}
if strings.Contains(string(output), "RUNNING") {
return nil
}
utils.Debugf("Waiting for the container to start (running: %v): %s\n", container.State.Running, output)
time.Sleep(50 * time.Millisecond)
}
if container.State.Running {
return ErrContainerStartTimeout
}
return ErrContainerStart
} }
func (container *Container) Run() error { func (container *Container) Run() error {
hostConfig := &HostConfig{} if err := container.Start(&HostConfig{}); err != nil {
if err := container.Start(hostConfig); err != nil {
return err return err
} }
container.Wait() container.Wait()

View File

@ -212,22 +212,16 @@ func TestRuntimeCreate(t *testing.T) {
} }
// Make sure crete with bad parameters returns an error // Make sure crete with bad parameters returns an error
_, err = runtime.Create( if _, err = runtime.Create(&Config{Image: GetTestImage(runtime).ID}); err == nil {
&Config{
Image: GetTestImage(runtime).ID,
},
)
if err == nil {
t.Fatal("Builder.Create should throw an error when Cmd is missing") t.Fatal("Builder.Create should throw an error when Cmd is missing")
} }
_, err = runtime.Create( if _, err := runtime.Create(
&Config{ &Config{
Image: GetTestImage(runtime).ID, Image: GetTestImage(runtime).ID,
Cmd: []string{}, Cmd: []string{},
}, },
) ); err == nil {
if err == nil {
t.Fatal("Builder.Create should throw an error when Cmd is empty") t.Fatal("Builder.Create should throw an error when Cmd is empty")
} }
@ -258,11 +252,11 @@ func TestRuntimeCreate(t *testing.T) {
func TestDestroy(t *testing.T) { func TestDestroy(t *testing.T) {
runtime := mkRuntime(t) runtime := mkRuntime(t)
defer nuke(runtime) defer nuke(runtime)
container, err := runtime.Create(&Config{ container, err := runtime.Create(&Config{
Image: GetTestImage(runtime).ID, Image: GetTestImage(runtime).ID,
Cmd: []string{"ls", "-al"}, Cmd: []string{"ls", "-al"},
}, })
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -327,11 +321,14 @@ func TestGet(t *testing.T) {
} }
func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, string) { func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, string) {
var err error var (
runtime := mkRuntime(t) err error
port := 5554 container *Container
var container *Container strPort string
var strPort string runtime = mkRuntime(t)
port = 5554
)
for { for {
port += 1 port += 1
strPort = strconv.Itoa(port) strPort = strconv.Itoa(port)
@ -359,8 +356,7 @@ func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container,
t.Logf("Port %v already in use", strPort) t.Logf("Port %v already in use", strPort)
} }
hostConfig := &HostConfig{} if err := container.Start(&HostConfig{}); err != nil {
if err := container.Start(hostConfig); err != nil {
nuke(runtime) nuke(runtime)
t.Fatal(err) t.Fatal(err)
} }

View File

@ -192,11 +192,11 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := srv.ContainerRestart(id, 1); err != nil { if err := srv.ContainerRestart(id, 15); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := srv.ContainerStop(id, 1); err != nil { if err := srv.ContainerStop(id, 15); err != nil {
t.Fatal(err) t.Fatal(err)
} }