-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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 podman rm -fa
with dependencies
#18507
Changes from all commits
e8d7456
bc1a31c
8cb5d39
ef1a22c
bafb3d6
b75ff3a
4e6efbb
2c9f181
398e48a
0e47465
a750cd9
3100824
f1ecdca
2ebc900
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -356,10 +356,20 @@ func generateResourceFile(res *spec.LinuxResources) (string, []string, error) { | |
// If all is set, send to all PIDs in the container. | ||
// All is only supported if the container created cgroups. | ||
func (r *ConmonOCIRuntime) KillContainer(ctr *Container, signal uint, all bool) error { | ||
if _, err := r.killContainer(ctr, signal, all, false); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// If captureStderr is requested, OCI runtime STDERR will be captured as a | ||
// *bytes.buffer and returned; otherwise, it is set to os.Stderr. | ||
func (r *ConmonOCIRuntime) killContainer(ctr *Container, signal uint, all, captureStderr bool) (*bytes.Buffer, error) { | ||
logrus.Debugf("Sending signal %d to container %s", signal, ctr.ID()) | ||
runtimeDir, err := util.GetRuntimeDir() | ||
if err != nil { | ||
return err | ||
return nil, err | ||
} | ||
env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} | ||
var args []string | ||
|
@@ -369,19 +379,27 @@ func (r *ConmonOCIRuntime) KillContainer(ctr *Container, signal uint, all bool) | |
} else { | ||
args = append(args, "kill", ctr.ID(), fmt.Sprintf("%d", signal)) | ||
} | ||
if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, args...); err != nil { | ||
var ( | ||
stderr io.Writer = os.Stderr | ||
stderrBuffer *bytes.Buffer | ||
) | ||
if captureStderr { | ||
stderrBuffer = new(bytes.Buffer) | ||
stderr = stderrBuffer | ||
} | ||
if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, stderr, env, r.path, args...); err != nil { | ||
// Update container state - there's a chance we failed because | ||
// the container exited in the meantime. | ||
if err2 := r.UpdateContainerStatus(ctr); err2 != nil { | ||
logrus.Infof("Error updating status for container %s: %v", ctr.ID(), err2) | ||
} | ||
if ctr.ensureState(define.ContainerStateStopped, define.ContainerStateExited) { | ||
return fmt.Errorf("%w: %s", define.ErrCtrStateInvalid, ctr.state.State) | ||
return stderrBuffer, fmt.Errorf("%w: %s", define.ErrCtrStateInvalid, ctr.state.State) | ||
} | ||
return fmt.Errorf("sending signal to container %s: %w", ctr.ID(), err) | ||
return stderrBuffer, fmt.Errorf("sending signal to container %s: %w", ctr.ID(), err) | ||
} | ||
|
||
return nil | ||
return stderrBuffer, nil | ||
} | ||
|
||
// StopContainer stops a container, first using its given stop signal (or | ||
|
@@ -400,23 +418,65 @@ func (r *ConmonOCIRuntime) StopContainer(ctr *Container, timeout uint, all bool) | |
return nil | ||
} | ||
|
||
if timeout > 0 { | ||
stopSignal := ctr.config.StopSignal | ||
if stopSignal == 0 { | ||
stopSignal = uint(syscall.SIGTERM) | ||
killCtr := func(signal uint) (bool, error) { | ||
stderr, err := r.killContainer(ctr, signal, all, true) | ||
|
||
// Before handling error from KillContainer, convert STDERR to a []string | ||
// (one string per line of output) and print it, ignoring known OCI runtime | ||
// errors that we don't care about | ||
stderrLines := strings.Split(stderr.String(), "\n") | ||
for _, line := range stderrLines { | ||
if line == "" { | ||
continue | ||
} | ||
if strings.Contains(line, "container not running") || strings.Contains(line, "open pidfd: No such process") { | ||
logrus.Debugf("Failure to kill container (already stopped?): logged %s", line) | ||
continue | ||
} | ||
fmt.Fprintf(os.Stderr, "%s\n", line) | ||
Comment on lines
+423
to
+436
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we do this only in the err != nil case? I am not happy with string matches either but cannot offer something better so am ok with it, I guess CI will tell us quickly if these strings ever change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking we'd still want to log warnings even in the err=nil case, but I don't feel that strongly and would be happy to downgrade them to debug logs if there is no error There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah honestly not sure either, I am good with this as well. |
||
} | ||
if err := r.KillContainer(ctr, stopSignal, all); err != nil { | ||
|
||
if err != nil { | ||
// There's an inherent race with the cleanup process (see | ||
// #16142, #17142). If the container has already been marked as | ||
// stopped or exited by the cleanup process, we can return | ||
// immediately. | ||
if errors.Is(err, define.ErrCtrStateInvalid) && ctr.ensureState(define.ContainerStateStopped, define.ContainerStateExited) { | ||
return true, nil | ||
} | ||
|
||
// If the PID is 0, then the container is already stopped. | ||
if ctr.state.PID == 0 { | ||
return true, nil | ||
} | ||
|
||
// Is the container gone? | ||
// If so, it probably died between the first check and | ||
// our sending the signal | ||
// The container is stopped, so exit cleanly | ||
err := unix.Kill(ctr.state.PID, 0) | ||
if err == unix.ESRCH { | ||
return nil | ||
return true, nil | ||
} | ||
|
||
return false, err | ||
} | ||
return false, nil | ||
} | ||
|
||
if timeout > 0 { | ||
stopSignal := ctr.config.StopSignal | ||
if stopSignal == 0 { | ||
stopSignal = uint(syscall.SIGTERM) | ||
} | ||
|
||
stopped, err := killCtr(stopSignal) | ||
if err != nil { | ||
return err | ||
} | ||
if stopped { | ||
return nil | ||
} | ||
|
||
if err := waitContainerStop(ctr, time.Duration(timeout)*time.Second); err != nil { | ||
logrus.Debugf("Timed out stopping container %s with %s, resorting to SIGKILL: %v", ctr.ID(), unix.SignalName(syscall.Signal(stopSignal)), err) | ||
|
@@ -427,27 +487,13 @@ func (r *ConmonOCIRuntime) StopContainer(ctr *Container, timeout uint, all bool) | |
} | ||
} | ||
|
||
// If the timeout was set to 0 or if stopping the container with the | ||
// specified signal did not work, use the big hammer with SIGKILL. | ||
if err := r.KillContainer(ctr, uint(unix.SIGKILL), all); err != nil { | ||
// There's an inherent race with the cleanup process (see | ||
// #16142, #17142). If the container has already been marked as | ||
// stopped or exited by the cleanup process, we can return | ||
// immediately. | ||
if errors.Is(err, define.ErrCtrStateInvalid) && ctr.ensureState(define.ContainerStateStopped, define.ContainerStateExited) { | ||
return nil | ||
} | ||
|
||
// If the PID is 0, then the container is already stopped. | ||
if ctr.state.PID == 0 { | ||
return nil | ||
} | ||
// Again, check if the container is gone. If it is, exit cleanly. | ||
if aliveErr := unix.Kill(ctr.state.PID, 0); errors.Is(aliveErr, unix.ESRCH) { | ||
return nil | ||
} | ||
stopped, err := killCtr(uint(unix.SIGKILL)) | ||
if err != nil { | ||
return fmt.Errorf("sending SIGKILL to container %s: %w", ctr.ID(), err) | ||
} | ||
if stopped { | ||
return nil | ||
} | ||
|
||
// Give runtime a few seconds to make it happen | ||
if err := waitContainerStop(ctr, killContainerTimeout); err != nil { | ||
|
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 a lovely default.
Note that the shm lock code already does the required
runtime.LockOSThread()
on lock so the os thread will not be reused until we call runtime.UnlockOSThread() in the unlock call. Therefore it should be impossible for go to schedule two goroutines on this thread and we should never run into this situation where we actually need this. also seeTestLockAndUnlockTwoSemaphore
Also on my fedora laptop PTHREAD_MUTEX_DEFAULT already maps to PTHREAD_MUTEX_NORMAL so this shouldn't make a difference at least on current fedora. Of course it is still required to explicitly set it given it is not specified anywhere and other c libs may work differently.
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.
You'd think that lock/unlock OS thread would do that, but I'm actually seeing that, when we start a parallel container removal (5 containers, and 48 jobs), every single one of them starts with the same PID. I looked to see if we were holding a lock that might force every part of Podman onto the same OS thread (and basically eliminate threading within Podman), but that didn't seem to be the case?
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.
Regardless, it seems that Golang is still capable of swapping between Goroutines at will on a given thread, even after LockOSThread() is run. It just guarantees that the goroutine in question will only start on the thread in question, not that it won't schedule others there as well.
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.
That goes directly against the go docs for this function
If it actually still executes other goroutines on that thread we have a major issues. We also use this to switch namespaces, set selinux lables, etc... If this doesn't work right now this needs to be fixed in golang.