diff --git a/daemon/attach.go b/daemon/attach.go index fb14691d242af..0e12441ad1e2e 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -123,7 +123,7 @@ func (daemon *Daemon) containerAttach(c *container.Container, cfg *stream.Attach return logger.ErrReadLogsNotSupported{} } logs := cLog.ReadLogs(logger.ReadConfig{Tail: -1}) - defer logs.Close() + defer logs.ConsumerGone() LogLoop: for { diff --git a/daemon/logger/adapter.go b/daemon/logger/adapter.go index 95aff9bf3babc..208147d39c217 100644 --- a/daemon/logger/adapter.go +++ b/daemon/logger/adapter.go @@ -94,7 +94,7 @@ func (a *pluginAdapterWithRead) ReadLogs(config ReadConfig) *LogWatcher { dec := logdriver.NewLogEntryDecoder(stream) for { select { - case <-watcher.WatchClose(): + case <-watcher.WatchConsumerGone(): return default: } @@ -106,7 +106,7 @@ func (a *pluginAdapterWithRead) ReadLogs(config ReadConfig) *LogWatcher { } select { case watcher.Err <- errors.Wrap(err, "error decoding log message"): - case <-watcher.WatchClose(): + case <-watcher.WatchConsumerGone(): } return } @@ -127,9 +127,7 @@ func (a *pluginAdapterWithRead) ReadLogs(config ReadConfig) *LogWatcher { select { case watcher.Msg <- msg: - case <-watcher.WatchClose(): - // make sure the message we consumed is sent - watcher.Msg <- msg + case <-watcher.WatchConsumerGone(): return } } diff --git a/daemon/logger/adapter_test.go b/daemon/logger/adapter_test.go index f47e711c892ea..fb85375a1d4ef 100644 --- a/daemon/logger/adapter_test.go +++ b/daemon/logger/adapter_test.go @@ -174,7 +174,6 @@ func TestAdapterReadLogs(t *testing.T) { t.Fatal("timeout waiting for message channel to close") } - lw.Close() lw = lr.ReadLogs(ReadConfig{Follow: true}) for _, x := range testMsg { diff --git a/daemon/logger/journald/journald.go b/daemon/logger/journald/journald.go index 342e18f57f817..9aaac9be86bcb 100644 --- a/daemon/logger/journald/journald.go +++ b/daemon/logger/journald/journald.go @@ -18,14 +18,9 @@ import ( const name = "journald" type journald struct { - mu sync.Mutex - vars map[string]string // additional variables and values to send to the journal along with the log message - readers readerList - closed bool -} - -type readerList struct { - readers map[*logger.LogWatcher]*logger.LogWatcher + mu sync.Mutex + vars map[string]string // additional variables and values to send to the journal along with the log message + closed bool } func init() { @@ -84,7 +79,7 @@ func New(info logger.Info) (logger.Logger, error) { for k, v := range extraAttrs { vars[k] = v } - return &journald{vars: vars, readers: readerList{readers: make(map[*logger.LogWatcher]*logger.LogWatcher)}}, nil + return &journald{vars: vars}, nil } // We don't actually accept any options, but we have to supply a callback for diff --git a/daemon/logger/journald/read.go b/daemon/logger/journald/read.go index d4bcc62d9aefe..48aa438dcdde0 100644 --- a/daemon/logger/journald/read.go +++ b/daemon/logger/journald/read.go @@ -164,9 +164,6 @@ import ( func (s *journald) Close() error { s.mu.Lock() s.closed = true - for reader := range s.readers.readers { - reader.Close() - } s.mu.Unlock() return nil } @@ -253,7 +250,6 @@ drain: func (s *journald) followJournal(logWatcher *logger.LogWatcher, j *C.sd_journal, pfd [2]C.int, cursor *C.char, untilUnixMicro uint64) *C.char { s.mu.Lock() - s.readers.readers[logWatcher] = logWatcher if s.closed { // the journald Logger is closed, presumably because the container has been // reset. So we shouldn't follow, because we'll never be woken up. But we @@ -289,9 +285,6 @@ func (s *journald) followJournal(logWatcher *logger.LogWatcher, j *C.sd_journal, // Clean up. C.close(pfd[0]) - s.mu.Lock() - delete(s.readers.readers, logWatcher) - s.mu.Unlock() close(logWatcher.Msg) newCursor <- cursor }() @@ -299,7 +292,7 @@ func (s *journald) followJournal(logWatcher *logger.LogWatcher, j *C.sd_journal, // Wait until we're told to stop. select { case cursor = <-newCursor: - case <-logWatcher.WatchClose(): + case <-logWatcher.WatchConsumerGone(): // Notify the other goroutine that its work is done. C.close(pfd[1]) cursor = <-newCursor diff --git a/daemon/logger/jsonfilelog/jsonfilelog.go b/daemon/logger/jsonfilelog/jsonfilelog.go index 05243000d40d0..81b5505e76ef9 100644 --- a/daemon/logger/jsonfilelog/jsonfilelog.go +++ b/daemon/logger/jsonfilelog/jsonfilelog.go @@ -23,11 +23,10 @@ const Name = "json-file" // JSONFileLogger is Logger implementation for default Docker logging. type JSONFileLogger struct { - mu sync.Mutex - closed bool - writer *loggerutils.LogFile - readers map[*logger.LogWatcher]struct{} // stores the active log followers - tag string // tag values requested by the user to log + mu sync.Mutex + closed bool + writer *loggerutils.LogFile + tag string // tag values requested by the user to log } func init() { @@ -116,9 +115,8 @@ func New(info logger.Info) (logger.Logger, error) { } return &JSONFileLogger{ - writer: writer, - readers: make(map[*logger.LogWatcher]struct{}), - tag: tag, + writer: writer, + tag: tag, }, nil } @@ -166,15 +164,11 @@ func ValidateLogOpt(cfg map[string]string) error { return nil } -// Close closes underlying file and signals all readers to stop. +// Close closes the underlying log file. func (l *JSONFileLogger) Close() error { l.mu.Lock() l.closed = true err := l.writer.Close() - for r := range l.readers { - r.Close() - delete(l.readers, r) - } l.mu.Unlock() return err } diff --git a/daemon/logger/jsonfilelog/read.go b/daemon/logger/jsonfilelog/read.go index 12f676bb1a31c..d600bb6330b15 100644 --- a/daemon/logger/jsonfilelog/read.go +++ b/daemon/logger/jsonfilelog/read.go @@ -27,15 +27,7 @@ func (l *JSONFileLogger) ReadLogs(config logger.ReadConfig) *logger.LogWatcher { func (l *JSONFileLogger) readLogs(watcher *logger.LogWatcher, config logger.ReadConfig) { defer close(watcher.Msg) - l.mu.Lock() - l.readers[watcher] = struct{}{} - l.mu.Unlock() - l.writer.ReadLogs(config, watcher) - - l.mu.Lock() - delete(l.readers, watcher) - l.mu.Unlock() } func decodeLogLine(dec *json.Decoder, l *jsonlog.JSONLog) (*logger.Message, error) { diff --git a/daemon/logger/jsonfilelog/read_test.go b/daemon/logger/jsonfilelog/read_test.go index 6ce4936e0e994..86aa3b2aa5ed1 100644 --- a/daemon/logger/jsonfilelog/read_test.go +++ b/daemon/logger/jsonfilelog/read_test.go @@ -50,11 +50,10 @@ func BenchmarkJSONFileLoggerReadLogs(b *testing.B) { }() lw := jsonlogger.(*JSONFileLogger).ReadLogs(logger.ReadConfig{Follow: true}) - watchClose := lw.WatchClose() for { select { case <-lw.Msg: - case <-watchClose: + case <-lw.WatchConsumerGone(): return case err := <-chError: if err != nil { diff --git a/daemon/logger/local/local.go b/daemon/logger/local/local.go index 86c55784d4b10..a3a7ade7e494f 100644 --- a/daemon/logger/local/local.go +++ b/daemon/logger/local/local.go @@ -60,7 +60,6 @@ type driver struct { mu sync.Mutex closed bool logfile *loggerutils.LogFile - readers map[*logger.LogWatcher]struct{} // stores the active log followers } // New creates a new local logger @@ -146,7 +145,6 @@ func newDriver(logPath string, cfg *CreateConfig) (logger.Logger, error) { } return &driver{ logfile: lf, - readers: make(map[*logger.LogWatcher]struct{}), }, nil } @@ -165,10 +163,6 @@ func (d *driver) Close() error { d.mu.Lock() d.closed = true err := d.logfile.Close() - for r := range d.readers { - r.Close() - delete(d.readers, r) - } d.mu.Unlock() return err } diff --git a/daemon/logger/local/read.go b/daemon/logger/local/read.go index a752de2a8d541..bb85384cb36e4 100644 --- a/daemon/logger/local/read.go +++ b/daemon/logger/local/read.go @@ -23,16 +23,7 @@ func (d *driver) ReadLogs(config logger.ReadConfig) *logger.LogWatcher { func (d *driver) readLogs(watcher *logger.LogWatcher, config logger.ReadConfig) { defer close(watcher.Msg) - - d.mu.Lock() - d.readers[watcher] = struct{}{} - d.mu.Unlock() - d.logfile.ReadLogs(config, watcher) - - d.mu.Lock() - delete(d.readers, watcher) - d.mu.Unlock() } func getTailReader(ctx context.Context, r loggerutils.SizeReaderAt, req int) (io.Reader, int, error) { diff --git a/daemon/logger/logger.go b/daemon/logger/logger.go index 912e855c7f967..d4b5aef25475f 100644 --- a/daemon/logger/logger.go +++ b/daemon/logger/logger.go @@ -104,33 +104,33 @@ type LogWatcher struct { // For sending log messages to a reader. Msg chan *Message // For sending error messages that occur while while reading logs. - Err chan error - closeOnce sync.Once - closeNotifier chan struct{} + Err chan error + consumerOnce sync.Once + consumerGone chan struct{} } // NewLogWatcher returns a new LogWatcher. func NewLogWatcher() *LogWatcher { return &LogWatcher{ - Msg: make(chan *Message, logWatcherBufferSize), - Err: make(chan error, 1), - closeNotifier: make(chan struct{}), + Msg: make(chan *Message, logWatcherBufferSize), + Err: make(chan error, 1), + consumerGone: make(chan struct{}), } } -// Close notifies the underlying log reader to stop. -func (w *LogWatcher) Close() { +// ConsumerGone notifies that the logs consumer is gone. +func (w *LogWatcher) ConsumerGone() { // only close if not already closed - w.closeOnce.Do(func() { - close(w.closeNotifier) + w.consumerOnce.Do(func() { + close(w.consumerGone) }) } -// WatchClose returns a channel receiver that receives notification -// when the watcher has been closed. This should only be called from -// one goroutine. -func (w *LogWatcher) WatchClose() <-chan struct{} { - return w.closeNotifier +// WatchConsumerGone returns a channel receiver that receives notification +// when the log watcher consumer is gone. This should only be called from +// a single goroutine. +func (w *LogWatcher) WatchConsumerGone() <-chan struct{} { + return w.consumerGone } // Capability defines the list of capabilities that a driver can implement diff --git a/daemon/logger/loggerutils/logfile.go b/daemon/logger/loggerutils/logfile.go index 25be44aa49528..7a297cca554c9 100644 --- a/daemon/logger/loggerutils/logfile.go +++ b/daemon/logger/loggerutils/logfile.go @@ -365,11 +365,10 @@ func (w *LogFile) ReadLogs(config logger.ReadConfig, watcher *logger.LogWatcher) w.mu.RLock() } - if !config.Follow || w.closed { - w.mu.RUnlock() + w.mu.RUnlock() + if !config.Follow { return } - w.mu.RUnlock() notifyRotate := w.notifyRotate.Subscribe() defer w.notifyRotate.Evict(notifyRotate) @@ -488,7 +487,7 @@ func tailFiles(files []SizeReaderAt, watcher *logger.LogWatcher, createDecoder m go func() { select { case <-ctx.Done(): - case <-watcher.WatchClose(): + case <-watcher.WatchConsumerGone(): cancel() } }() @@ -527,10 +526,11 @@ func tailFiles(files []SizeReaderAt, watcher *logger.LogWatcher, createDecoder m if !config.Until.IsZero() && msg.Timestamp.After(config.Until) { return } + // send the message unless consumer is gone select { - case <-ctx.Done(): - return case watcher.Msg <- msg: + case <-watcher.WatchConsumerGone(): + return } } } @@ -550,18 +550,6 @@ func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan int fileWatcher.Close() }() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go func() { - select { - case <-logWatcher.WatchClose(): - fileWatcher.Remove(name) - cancel() - case <-ctx.Done(): - return - } - }() - var retries int handleRotate := func() error { f.Close() @@ -594,15 +582,13 @@ func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan int decodeLogLine = createDecoder(f) return nil case fsnotify.Rename, fsnotify.Remove: - select { - case <-notifyRotate: - case <-ctx.Done(): + return handleRotate() + case fsnotify.Chmod: + _, statErr := os.Lstat(e.Name) + if os.IsNotExist(statErr) { + // container and its log file removed return errDone } - if err := handleRotate(); err != nil { - return err - } - return nil } return errRetry case err := <-fileWatcher.Errors(): @@ -618,7 +604,7 @@ func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan int return errRetry } return err - case <-ctx.Done(): + case <-logWatcher.WatchConsumerGone(): return errDone } } @@ -664,23 +650,11 @@ func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan int if !until.IsZero() && msg.Timestamp.After(until) { return } + // send the message, unless the consumer is gone select { case logWatcher.Msg <- msg: - case <-ctx.Done(): - logWatcher.Msg <- msg - for { - msg, err := decodeLogLine() - if err != nil { - return - } - if !since.IsZero() && msg.Timestamp.Before(since) { - continue - } - if !until.IsZero() && msg.Timestamp.After(until) { - return - } - logWatcher.Msg <- msg - } + case <-logWatcher.WatchConsumerGone(): + return } } } diff --git a/daemon/logs.go b/daemon/logs.go index 37ca4caf632d7..b5713a570536f 100644 --- a/daemon/logs.go +++ b/daemon/logs.go @@ -64,7 +64,6 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c return nil, false, logger.ErrReadLogsNotSupported{} } - follow := config.Follow && !cLogCreated tailLines, err := strconv.Atoi(config.Tail) if err != nil { tailLines = -1 @@ -92,7 +91,7 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c Since: since, Until: until, Tail: tailLines, - Follow: follow, + Follow: config.Follow, } logs := logReader.ReadLogs(readConfig) @@ -110,14 +109,16 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c } }() } - // set up some defers - defer logs.Close() + // signal that the log reader is gone + defer logs.ConsumerGone() // close the messages channel. closing is the only way to signal above // that we're doing with logs (other than context cancel i guess). defer close(messageChan) lg.Debug("begin logs") + defer lg.Debugf("end logs (%v)", ctx.Err()) + for { select { // i do not believe as the system is currently designed any error @@ -132,14 +133,12 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c } return case <-ctx.Done(): - lg.Debugf("logs: end stream, ctx is done: %v", ctx.Err()) return case msg, ok := <-logs.Msg: // there is some kind of pool or ring buffer in the logger that // produces these messages, and a possible future optimization // might be to use that pool and reuse message objects if !ok { - lg.Debug("end logs") return } m := msg.AsLogMessage() // just a pointer conversion, does not copy data diff --git a/hack/make/test-docker-py b/hack/make/test-docker-py index b30879e3a0e40..18917841213cf 100644 --- a/hack/make/test-docker-py +++ b/hack/make/test-docker-py @@ -10,7 +10,7 @@ source hack/make/.integration-test-helpers dockerPy='/docker-py' [ -d "$dockerPy" ] || { dockerPy="$DEST/docker-py" - git clone https://github.com/docker/docker-py.git "$dockerPy" + git clone https://github.com/kolyshkin/docker-py.git "$dockerPy" } # exporting PYTHONPATH to import "docker" from our local docker-py diff --git a/integration-cli/docker_cli_logs_test.go b/integration-cli/docker_cli_logs_test.go index 2740de6f0665c..e88bf8644fb46 100644 --- a/integration-cli/docker_cli_logs_test.go +++ b/integration-cli/docker_cli_logs_test.go @@ -134,6 +134,9 @@ func (s *DockerSuite) TestLogsFollowStopped(c *check.C) { close(errChan) }() + cli.DockerCmd(c, "wait", id) + cli.DockerCmd(c, "rm", id) + select { case err := <-errChan: c.Assert(err, checker.IsNil) @@ -181,7 +184,7 @@ func (s *DockerSuite) TestLogsSinceFutureFollow(c *check.C) { // TODO Windows TP5 - Figure out why this test is so flakey. Disabled for now. testRequires(c, DaemonIsLinux) name := "testlogssincefuturefollow" - dockerCmd(c, "run", "-d", "--name", name, "busybox", "/bin/sh", "-c", `for i in $(seq 1 5); do echo log$i; sleep 1; done`) + dockerCmd(c, "run", "-d", "--rm", "--name", name, "busybox", "/bin/sh", "-c", `for i in $(seq 1 5); do echo log$i; sleep 1; done`) // Extract one timestamp from the log file to give us a starting point for // our `--since` argument. Because the log producer runs in the background, @@ -236,6 +239,9 @@ func (s *DockerSuite) TestLogsFollowSlowStdoutConsumer(c *check.C) { bytes1, err := ConsumeWithSpeed(stdout, 10, 50*time.Millisecond, stopSlowRead) c.Assert(err, checker.IsNil) + // Container has finished, remove it so "docker logs -f" exits + cli.DockerCmd(c, "rm", id) + // After the container has finished we can continue reading fast bytes2, err := ConsumeWithSpeed(stdout, 32*1024, 0, nil) c.Assert(err, checker.IsNil) diff --git a/integration/container/logs_test.go b/integration/container/logs_test.go index 68fbe13a73709..a52a8810d5667 100644 --- a/integration/container/logs_test.go +++ b/integration/container/logs_test.go @@ -2,8 +2,11 @@ package container // import "github.com/docker/docker/integration/container" import ( "context" + "fmt" + "io" "io/ioutil" "testing" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/integration/internal/container" @@ -33,3 +36,249 @@ func TestLogsFollowTailEmpty(t *testing.T) { _, err = stdcopy.StdCopy(ioutil.Discard, ioutil.Discard, logs) assert.Check(t, err) } + +type daemonResources [2]int + +func (r daemonResources) String() string { + return fmt.Sprintf("goroutines: %d, file descriptors: %d", r[0], r[1]) +} + +func (r daemonResources) Delta(r2 daemonResources) (d daemonResources) { + for i := 0; i < len(r); i++ { + d[i] = r2[i] - r[i] + if d[i] < 0 { // negative values do not make sense here + d[i] = 0 + } + } + return +} + +// Test for #37391 +func TestLogsFollowGoroutineLeak(t *testing.T) { + defer setupTest(t)() + client := request.NewAPIClient(t) + ctx := context.Background() + + getDaemonResources := func() (r daemonResources) { + info, err := client.Info(ctx) + assert.NilError(t, err) + // this will fail for daemon run without -D/--debug + assert.Check(t, info.NGoroutines > 1) + assert.Check(t, info.NFd > 1) + r[0] = info.NGoroutines + r[1] = info.NFd + + return + } + + isZero := func(delta daemonResources) bool { + for i := 0; i < len(delta); i++ { + if delta[i] > 0 { + return false + } + } + + return true + } + + waitToFreeResources := func(exp daemonResources) error { + tm := time.After(10 * time.Second) + for { + select { + case <-tm: + // one last chance + r := getDaemonResources() + t.Logf("daemon resources after: %v", r) + d := exp.Delta(r) + if isZero(d) { + return nil + } + return fmt.Errorf("Leaked %v", d) + default: + d := exp.Delta(getDaemonResources()) + if isZero(d) { + return nil + } + time.Sleep(200 * time.Millisecond) + } + } + } + + // start a container producing lots of logs + id := container.Run(t, ctx, client, container.WithCmd("yes", "lorem ipsum")) + + exp := getDaemonResources() + t.Logf("daemon resources before: %v", exp) + + // consume logs + stopCh := make(chan struct{}) + errCh := make(chan error) + go func() { + logs, err := client.ContainerLogs(ctx, id, types.ContainerLogsOptions{ + Follow: true, + ShowStdout: true, + ShowStderr: true, + }) + if err != nil { + errCh <- err + return + } + assert.Check(t, logs != nil) + + rd := 0 + buf := make([]byte, 1024) + defer func() { + logs.Close() + t.Logf("exit after reading %d bytes", rd) + }() + + for { + select { + case <-stopCh: + errCh <- nil + return + default: + n, err := logs.Read(buf) + rd += n + if err != nil { + errCh <- err + return + } + } + } + }() + + // read logs for a bit, then stop the reader + select { + case err := <-errCh: + // err can't be nil here + t.Fatalf("logs unexpectedly closed: %v", err) + case <-time.After(1 * time.Second): + close(stopCh) + } + // wait for log reader to stop + select { + case <-errCh: + case <-time.After(1 * time.Second): + t.Fatal("timeout waiting for log reader to stop") + } + + err := waitToFreeResources(exp) + if err != nil { + t.Fatal(err) + } +} + +// test for #37630 ("docker logs -f exits whenever container stops"). +// Parameter 'stoppedContainer', if set to true, means that the logger +// starts for a stopped container. +func testLogsFollow(t *testing.T, stoppedContainer bool) { + defer setupTest(t)() + client := request.NewAPIClient(t) + ctx := context.Background() + tm := time.Second * 1 + + // start a container producing some logs + id := container.Run(t, ctx, client, container.WithCmd("sh", "-c", "while true; do date +%s; sleep 0.1; done")) + if stoppedContainer { + err := client.ContainerStop(ctx, id, &tm) + assert.NilError(t, err) + } + + // consume logs + errCh := make(chan error) + rd := 0 // read bytes counter + go func() { + logs, err := client.ContainerLogs(ctx, id, types.ContainerLogsOptions{ + Follow: true, + ShowStdout: true, + ShowStderr: true, + Tail: "all", + }) + if err != nil { + errCh <- err + return + } + assert.Check(t, logs != nil) + + buf := make([]byte, 1024) + for { + n, err := logs.Read(buf) + rd += n + if err != nil { + errCh <- err + return + } + } + }() + + if stoppedContainer { + // time for log reader to process something + time.Sleep(tm) + } else { + err := client.ContainerStop(ctx, id, &tm) + assert.NilError(t, err) + } + + select { + case err := <-errCh: + t.Fatalf("logs unexpectedly closed: %v", err) + default: + } + + // make sure we have read some more bytes since last call + r := 0 + checkReadMore := func(state string) { + oldR := r + r = rd + t.Logf("container %s; read %d bytes so far", state, r) + if r <= oldR { + t.Fatalf("logs stuck? expected > %d, got %d", oldR, r) + } + } + + checkReadMore("stopped") + + // start the container again, read some more... + err := client.ContainerStart(ctx, id, types.ContainerStartOptions{}) + assert.NilError(t, err) + // wait a bit + select { + case err := <-errCh: + t.Fatalf("logs unexpectedly closed: %v", err) + case <-time.After(tm): + checkReadMore("restarted") + } + + // stop and remove the container + err = client.ContainerStop(ctx, id, &tm) + assert.NilError(t, err) + err = client.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}) + assert.NilError(t, err) + + // wait for log reader to stop + select { + case err := <-errCh: + if err != io.EOF { + t.Fatalf("logs returned: %v, expected: %v", err, io.EOF) + } + checkReadMore("removed") + case <-time.After(1 * time.Second): + t.Fatal("timeout waiting for log reader to stop") + } +} + +// Check that ContainerLogs(opts.Follow=true) +// - won't stop even if the container is stopped; +// - keep reading logs once the container is restarted; +// - only stops when the container is removed. +func TestLogsFollowNonStop(t *testing.T) { + testLogsFollow(t, false) +} + +// Check that ContainerLogs(opts.Follow=true) works +// as expected for existing stopped container, i.e. it does +// not exit but keeps waiting for the logs to come. +func TestLogsFollowStopped(t *testing.T) { + testLogsFollow(t, true) +} diff --git a/vendor.conf b/vendor.conf index 0156abbcd2cdb..aaf2803aea4de 100644 --- a/vendor.conf +++ b/vendor.conf @@ -95,7 +95,7 @@ github.com/philhofer/fwd 98c11a7a6ec829d672b03833c3d69a7fae1ca972 github.com/tinylib/msgp 3b556c64540842d4f82967be066a7f7fffc3adad # fsnotify -github.com/fsnotify/fsnotify v1.4.7 +github.com/fsnotify/fsnotify c9e9bfb647855178ec5f3947c02e6bd47a379eb9 https://github.com/kolyshkin/fsnotify/ # awslogs deps github.com/aws/aws-sdk-go v1.12.66 diff --git a/vendor/github.com/fsnotify/fsnotify/inotify.go b/vendor/github.com/fsnotify/fsnotify/inotify.go index d9fd1b88a05f2..3cb34284811a6 100644 --- a/vendor/github.com/fsnotify/fsnotify/inotify.go +++ b/vendor/github.com/fsnotify/fsnotify/inotify.go @@ -303,12 +303,12 @@ func (e *Event) ignoreLinux(mask uint32) bool { return true } - // If the event is not a DELETE or RENAME, the file must exist. - // Otherwise the event is ignored. - // *Note*: this was put in place because it was seen that a MODIFY - // event was sent after the DELETE. This ignores that MODIFY and - // assumes a DELETE will come or has come if the file doesn't exist. - if !(e.Op&Remove == Remove || e.Op&Rename == Rename) { + // If the event is Create or Write, the file must exist, or the + // event will be suppressed. + // *Note*: this was put in place because it was seen that a Write + // event was sent after the Remove. This ignores the Write and + // assumes a Remove will come or has come if the file doesn't exist. + if e.Op&Create == Create || e.Op&Write == Write { _, statErr := os.Lstat(e.Name) return os.IsNotExist(statErr) }