-
Notifications
You must be signed in to change notification settings - Fork 633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: shutdown logger after container process exits #2337
base: main
Are you sure you want to change the base?
fix: shutdown logger after container process exits #2337
Conversation
97ef2b7
to
9d8a14f
Compare
Hi @mharwani, as mentioned here closing task io io.Close() after task exiting should close FIFOs => send EOF to stdio => logger will receive |
Hi @fahedouch, I tried closing the task IO upon container exit with
|
cmd/nerdctl/container_logs_test.go
Outdated
base := testutil.NewBase(t) | ||
containerName := testutil.Identifier(t) | ||
defer base.Cmd("rm", containerName).Run() | ||
expected := []string{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expected := []string{} | |
expected := make([]string, 10) |
} | ||
|
||
base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, | ||
"sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done").AssertOK() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done").AssertOK() | |
"sh", "-euc", "for i in `seq 1 10`; do echo $i; done").AssertOK() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added sleep to have both the container task and the logs command running concurrently
cmd/nerdctl/container_logs_test.go
Outdated
base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, | ||
"sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done").AssertOK() | ||
base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) | ||
base.Cmd("rm", "-f", containerName).AssertOK() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useless regarding defer
cmd/nerdctl/container_logs_test.go
Outdated
t.Parallel() | ||
base := testutil.NewBase(t) | ||
containerName := testutil.Identifier(t) | ||
defer base.Cmd("rm", containerName).Run() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defer base.Cmd("rm", containerName).Run() | |
defer base.Cmd("rm", "-f", containerName).Run() |
@@ -143,3 +143,49 @@ func TestLogsWithFailingContainer(t *testing.T) { | |||
base.Cmd("logs", "-f", containerName).AssertNoOut("baz") | |||
base.Cmd("rm", "-f", containerName).AssertOK() | |||
} | |||
|
|||
func TestLogsWithRunningContainer(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what this test covers comparing to this one ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other test runs the log command after the container has stopped running, but this one tests logs during the task execution and as logs are being sent to the logger with the --follow
option.
pkg/cmd/container/logs.go
Outdated
@@ -90,6 +91,8 @@ func Logs(ctx context.Context, client *containerd.Client, container string, opti | |||
// Setup goroutine to send stop event if container task finishes: | |||
go func() { | |||
<-waitCh | |||
// wait 100ms to let logViewer process data sent after container exit, if any | |||
time.Sleep(100 * time.Millisecond) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have you experienced data not processed by the LogViewer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the logger may process additional data after container exits and there is no newline or EOF. For example, the following will not print any logs without time.Sleep
:
sudo nerdctl run -d --name test alpine sh -c 'sleep 10; echo -n hello'
sudo nerdctl logs -f test
Sleeping for 100ms is undeterministic. There should be a better way to do this. Maybe somehow wait for the logger to finish instead of container wait?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, waiting for the logger to complete instead of waiting for the container to exit gives a more accurate result
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, didn't get a chance to work on it. I will send a new revision later today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fahedouch To wait for the logger to complete, it has to send a signal to the LogViewer. I'm using file locks to accomplish this IPC. PTAL here and here. Thanks!
pkg/logging/logging.go
Outdated
if err := driver.PreProcess(dataStore, config); err != nil { | ||
return err | ||
} | ||
exit, err := getContainerWait(ctx, hostAddress, config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exit, err := getContainerWait(ctx, hostAddress, config) | |
exitCh, err := getContainerWait(ctx, hostAddress, config) |
pkg/logging/logging.go
Outdated
stderrR, stderrW := io.Pipe() | ||
copyStream := func(reader io.Reader, writer *io.PipeWriter) { | ||
// copy using a buffer of size 16K | ||
buf := make([]byte, 16*1024) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buf := make([]byte, 16*1024) | |
buf := make([]byte, 16<<10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why choose a 16k buffer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it to 32k as that is what containerd uses to copy stdio:
https://github.com/containerd/containerd/blob/main/cio/io.go#L34
pkg/cmd/container/create.go
Outdated
@@ -639,8 +639,9 @@ func writeCIDFile(path, id string) error { | |||
} | |||
|
|||
// generateLogConfig creates a LogConfig for the current container store | |||
func generateLogConfig(dataStore string, id string, logDriver string, logOpt []string, ns string) (logConfig logging.LogConfig, err error) { | |||
func generateLogConfig(dataStore string, id string, logDriver string, logOpt []string, gOpt *types.GlobalCommandOptions) (logConfig logging.LogConfig, err error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why passing a pointer here gOpt *types.GlobalCommandOptions
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
9d8a14f
to
8763da2
Compare
It looks like the environment setup is failing for some of the checks because curl cannot reach the endpoint https://raw.githubusercontent.com/AkihiroSuda/containerized-systemd/v0.1.1/docker-entrypoint.sh: Any ideas? |
CI is now green for Linux. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mharwani PTAL #2337 (comment)
8763da2
to
23b24a6
Compare
Windows tests are failing
|
pkg/logging/logging.go
Outdated
@@ -234,11 +246,24 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) { | |||
if err != nil { | |||
return err | |||
} | |||
if err := ready(); err != nil { | |||
|
|||
lockFile := getLockPath(dataStore, config.Namespace, config.ID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
may be loggerLock
is more explicit here
3ce8c17
to
1df7878
Compare
Signed-off-by: Mrudul Harwani <[email protected]>
Signed-off-by: Mrudul Harwani <[email protected]>
…ocessed data Signed-off-by: Mrudul Harwani <[email protected]>
Signed-off-by: Mrudul Harwani <[email protected]>
Signed-off-by: Mrudul Harwani <[email protected]>
1df7878
to
39fe0b3
Compare
Signed-off-by: Mrudul Harwani <[email protected]>
39fe0b3
to
0050ac5
Compare
LogURI string `json:"-"` | ||
Driver string `json:"driver"` | ||
Opts map[string]string `json:"opts,omitempty"` | ||
HostAddress string `json:"host"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HostAddress string `json:"host"` | |
ContainerdAddress string `json:"containerdAddress"` |
Ideally log-config.json
should not contain the address, but it can be refactored later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need a default value for HostAddress
?
LogURI string `json:"-"` | ||
Driver string `json:"driver"` | ||
Opts map[string]string `json:"opts,omitempty"` | ||
HostAddress string `json:"host"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need a default value for HostAddress
?
for { | ||
select { | ||
case <-ctx.Done(): | ||
return nil, fmt.Errorf("timed out waiting for container task to start") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nil, fmt.Errorf("timed out waiting for container task to start") | |
return nil, errors.New("timed out waiting for container task to start") |
|
||
// If task was not found, it's possible that the container runtime is still being created. | ||
// Retry every 100ms. | ||
ticker := time.NewTicker(100 * time.Millisecond) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ticker := time.NewTicker(100 * time.Millisecond) | |
ticker := time.NewTicker(100 * time.Millisecond) | |
defer ticker.Stop() |
@mharwani PTAL |
ping @mharwani |
Hey @mharwani Would you still be interested in carrying this PR through? |
Addresses #2313
Container task is not deleted after it exits, leaving the runtime shim and logger open throughout the lifecycle of a container. This can be a problem when there is unprocessed
stdout
data after a container task finishes and the logger is blocked forever waiting for a newline or EOF.This PR fixes the issue by piping container output instead of reading directly from
stdout
andstderr
file descriptors. Closing the io pipes upon task exit allows the logger to shutdown gracefully and process leftover data without a newline .