1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Fix goroutine leak on logs -f with no output

Also noticed potential hang when only stdout or stderr are used with
follow=1

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff 2015-06-04 12:15:33 -07:00
parent cbe120db58
commit 0c84604f54
3 changed files with 73 additions and 10 deletions

View file

@ -597,6 +597,11 @@ func (s *Server) getContainersLogs(version version.Version, w http.ResponseWrite
since = time.Unix(s, 0) since = time.Unix(s, 0)
} }
var closeNotifier <-chan bool
if notifier, ok := w.(http.CloseNotifier); ok {
closeNotifier = notifier.CloseNotify()
}
logsConfig := &daemon.ContainerLogsConfig{ logsConfig := &daemon.ContainerLogsConfig{
Follow: boolValue(r, "follow"), Follow: boolValue(r, "follow"),
Timestamps: boolValue(r, "timestamps"), Timestamps: boolValue(r, "timestamps"),
@ -605,6 +610,7 @@ func (s *Server) getContainersLogs(version version.Version, w http.ResponseWrite
UseStdout: stdout, UseStdout: stdout,
UseStderr: stderr, UseStderr: stderr,
OutStream: ioutils.NewWriteFlusher(w), OutStream: ioutils.NewWriteFlusher(w),
Stop: closeNotifier,
} }
if err := s.daemon.ContainerLogs(vars["name"], logsConfig); err != nil { if err := s.daemon.ContainerLogs(vars["name"], logsConfig); err != nil {

View file

@ -25,6 +25,7 @@ type ContainerLogsConfig struct {
Since time.Time Since time.Time
UseStdout, UseStderr bool UseStdout, UseStderr bool
OutStream io.Writer OutStream io.Writer
Stop <-chan bool
} }
func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) error { func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) error {
@ -119,7 +120,8 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
} }
if config.Follow && container.IsRunning() { if config.Follow && container.IsRunning() {
chErr := make(chan error) chErrStderr := make(chan error)
chErrStdout := make(chan error)
var stdoutPipe, stderrPipe io.ReadCloser var stdoutPipe, stderrPipe io.ReadCloser
// write an empty chunk of data (this is to ensure that the // write an empty chunk of data (this is to ensure that the
@ -131,7 +133,7 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
stdoutPipe = container.StdoutLogPipe() stdoutPipe = container.StdoutLogPipe()
go func() { go func() {
logrus.Debug("logs: stdout stream begin") logrus.Debug("logs: stdout stream begin")
chErr <- jsonlog.WriteLog(stdoutPipe, outStream, format, config.Since) chErrStdout <- jsonlog.WriteLog(stdoutPipe, outStream, format, config.Since)
logrus.Debug("logs: stdout stream end") logrus.Debug("logs: stdout stream end")
}() }()
} }
@ -139,19 +141,33 @@ func (daemon *Daemon) ContainerLogs(name string, config *ContainerLogsConfig) er
stderrPipe = container.StderrLogPipe() stderrPipe = container.StderrLogPipe()
go func() { go func() {
logrus.Debug("logs: stderr stream begin") logrus.Debug("logs: stderr stream begin")
chErr <- jsonlog.WriteLog(stderrPipe, errStream, format, config.Since) chErrStderr <- jsonlog.WriteLog(stderrPipe, errStream, format, config.Since)
logrus.Debug("logs: stderr stream end") logrus.Debug("logs: stderr stream end")
}() }()
} }
err = <-chErr select {
case err = <-chErrStderr:
if stdoutPipe != nil { if stdoutPipe != nil {
stdoutPipe.Close() stdoutPipe.Close()
<-chErrStdout
}
case err = <-chErrStdout:
if stderrPipe != nil {
stderrPipe.Close()
<-chErrStderr
}
case <-config.Stop:
if stdoutPipe != nil {
stdoutPipe.Close()
<-chErrStdout
} }
if stderrPipe != nil { if stderrPipe != nil {
stderrPipe.Close() stderrPipe.Close()
<-chErrStderr
}
return nil
} }
<-chErr // wait for 2nd goroutine to exit, otherwise bad things will happen
if err != nil && err != io.EOF && err != io.ErrClosedPipe { if err != nil && err != io.EOF && err != io.ErrClosedPipe {
if e, ok := err.(*net.OpError); ok && e.Err != syscall.EPIPE { if e, ok := err.(*net.OpError); ok && e.Err != syscall.EPIPE {

View file

@ -425,3 +425,44 @@ func (s *DockerSuite) TestLogsFollowGoroutinesWithStdout(c *check.C) {
} }
} }
} }
func (s *DockerSuite) TestLogsFollowGoroutinesNoOutput(c *check.C) {
out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "while true; do sleep 2; done")
id := strings.TrimSpace(out)
c.Assert(waitRun(id), check.IsNil)
type info struct {
NGoroutines int
}
getNGoroutines := func() int {
var i info
status, b, err := sockRequest("GET", "/info", nil)
c.Assert(err, check.IsNil)
c.Assert(status, check.Equals, 200)
c.Assert(json.Unmarshal(b, &i), check.IsNil)
return i.NGoroutines
}
nroutines := getNGoroutines()
cmd := exec.Command(dockerBinary, "logs", "-f", id)
c.Assert(cmd.Start(), check.IsNil)
time.Sleep(200 * time.Millisecond)
c.Assert(cmd.Process.Kill(), check.IsNil)
// NGoroutines is not updated right away, so we need to wait before failing
t := time.After(30 * time.Second)
for {
select {
case <-t:
if n := getNGoroutines(); n > nroutines {
c.Fatalf("leaked goroutines: expected less than or equal to %d, got: %d", nroutines, n)
}
default:
if n := getNGoroutines(); n <= nroutines {
return
}
time.Sleep(200 * time.Millisecond)
}
}
}