mirror of
https://github.com/docker/compose.git
synced 2025-06-01 20:30:13 +02:00
fix deadlock collecting large logs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
894ab41c3b
commit
07bda5960e
@ -32,18 +32,19 @@ type logPrinter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type printer struct {
|
type printer struct {
|
||||||
sync.Mutex
|
|
||||||
queue chan api.ContainerEvent
|
queue chan api.ContainerEvent
|
||||||
consumer api.LogConsumer
|
consumer api.LogConsumer
|
||||||
stopped bool
|
stopCh chan struct{} // stopCh is a signal channel for producers to stop sending events to the queue
|
||||||
|
stop sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// newLogPrinter builds a LogPrinter passing containers logs to LogConsumer
|
// newLogPrinter builds a LogPrinter passing containers logs to LogConsumer
|
||||||
func newLogPrinter(consumer api.LogConsumer) logPrinter {
|
func newLogPrinter(consumer api.LogConsumer) logPrinter {
|
||||||
queue := make(chan api.ContainerEvent)
|
|
||||||
printer := printer{
|
printer := printer{
|
||||||
consumer: consumer,
|
consumer: consumer,
|
||||||
queue: queue,
|
queue: make(chan api.ContainerEvent),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
stop: sync.Once{},
|
||||||
}
|
}
|
||||||
return &printer
|
return &printer
|
||||||
}
|
}
|
||||||
@ -54,24 +55,27 @@ func (p *printer) Cancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *printer) Stop() {
|
func (p *printer) Stop() {
|
||||||
p.Lock()
|
p.stop.Do(func() {
|
||||||
defer p.Unlock()
|
close(p.stopCh)
|
||||||
if !p.stopped {
|
for {
|
||||||
// only close if this is the first call to stop
|
select {
|
||||||
p.stopped = true
|
case <-p.queue:
|
||||||
close(p.queue)
|
// purge the queue to free producers goroutines
|
||||||
|
// p.queue will be garbage collected
|
||||||
|
default:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *printer) HandleEvent(event api.ContainerEvent) {
|
func (p *printer) HandleEvent(event api.ContainerEvent) {
|
||||||
p.Lock()
|
select {
|
||||||
defer p.Unlock()
|
case <-p.stopCh:
|
||||||
if p.stopped {
|
|
||||||
// prevent deadlocking, if the printer is done, there's no reader for
|
|
||||||
// queue, so this write could block indefinitely
|
|
||||||
return
|
return
|
||||||
}
|
default:
|
||||||
p.queue <- event
|
p.queue <- event
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
@ -80,8 +84,14 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
|
|||||||
aborting bool
|
aborting bool
|
||||||
exitCode int
|
exitCode int
|
||||||
)
|
)
|
||||||
|
defer p.Stop()
|
||||||
|
|
||||||
containers := map[string]struct{}{}
|
containers := map[string]struct{}{}
|
||||||
for event := range p.queue {
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
return exitCode, nil
|
||||||
|
case event := <-p.queue:
|
||||||
container, id := event.Container, event.ID
|
container, id := event.Container, event.ID
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case api.UserCancel:
|
case api.UserCancel:
|
||||||
@ -133,5 +143,5 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return exitCode, nil
|
}
|
||||||
}
|
}
|
||||||
|
6
pkg/e2e/fixtures/logs-test/cat.yaml
Normal file
6
pkg/e2e/fixtures/logs-test/cat.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: alpine
|
||||||
|
command: cat /text_file.txt
|
||||||
|
volumes:
|
||||||
|
- ${FILE}:/text_file.txt
|
@ -17,6 +17,10 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -96,6 +100,26 @@ func TestLocalComposeLogsFollow(t *testing.T) {
|
|||||||
poll.WaitOn(t, expectOutput(res, "ping-2 "), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(20*time.Second))
|
poll.WaitOn(t, expectOutput(res, "ping-2 "), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(20*time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocalComposeLargeLogs(t *testing.T) {
|
||||||
|
const projectName = "compose-e2e-large_logs"
|
||||||
|
file := filepath.Join(t.TempDir(), "large.txt")
|
||||||
|
c := NewCLI(t, WithEnv("FILE="+file))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
|
||||||
|
})
|
||||||
|
|
||||||
|
f, err := os.Create(file)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
for i := 0; i < 300_000; i++ {
|
||||||
|
_, err := io.WriteString(f, fmt.Sprintf("This is line %d in a laaaarge text file\n", i))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
assert.NilError(t, f.Close())
|
||||||
|
|
||||||
|
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/cat.yaml", "--project-name", projectName, "up", "--abort-on-container-exit")
|
||||||
|
res.Assert(t, icmd.Expected{Out: "test-1 exited with code 0"})
|
||||||
|
}
|
||||||
|
|
||||||
func expectOutput(res *icmd.Result, expected string) func(t poll.LogT) poll.Result {
|
func expectOutput(res *icmd.Result, expected string) func(t poll.LogT) poll.Result {
|
||||||
return func(t poll.LogT) poll.Result {
|
return func(t poll.LogT) poll.Result {
|
||||||
if strings.Contains(res.Stdout(), expected) {
|
if strings.Contains(res.Stdout(), expected) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user