mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 05:17:02 +00:00
To make task.Delete retriable, this patch caches the delete result in memory after the shim successfully deletes the task. If a later shutdown attempt fails, a subsequent Delete retry can complete cleanup and return the cached exit information. If containerd restarts after deleting the task, the in-memory cache is lost, and containerd will publish TaskExit and TaskDelete events again to subscribers. Currently, the exit status returned by shim-delete is always 137. In a follow-up, containerd-shim-runc-v2 should store the actual exit status in the state directory, so that shim-delete can reuse it instead of returning a hard-coded value. Implements: #8981 Signed-off-by: Wei Fu <fuweid89@gmail.com>
288 lines
9.5 KiB
Go
288 lines
9.5 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containerd/errdefs"
|
|
"github.com/containerd/typeurl/v2"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
eventtypes "github.com/containerd/containerd/api/events"
|
|
apitask "github.com/containerd/containerd/api/runtime/task/v3"
|
|
"github.com/containerd/containerd/v2/core/events"
|
|
ctrdruntime "github.com/containerd/containerd/v2/core/runtime"
|
|
"github.com/containerd/containerd/v2/integration/images"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
ptypes "github.com/containerd/containerd/v2/pkg/protobuf/types"
|
|
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
|
)
|
|
|
|
// TestIssue7496_ShouldRetryShutdown is based on https://github.com/containerd/containerd/issues/7496.
|
|
//
|
|
// The first task Delete succeeds, but an injected Shutdown failure prevents the
|
|
// shim from exiting. The CRI retry then sees the task as NotFound and must still
|
|
// shut down the shim without publishing duplicate exit or delete events.
|
|
func TestIssue7496_ShouldRetryShutdown(t *testing.T) {
|
|
const eventBarrierTopic = "/tests/issue7496/event-barrier"
|
|
|
|
ctx := namespaces.WithNamespace(t.Context(), "k8s.io")
|
|
|
|
t.Logf("Create a pod config with shutdown failpoint")
|
|
sbConfig := PodSandboxConfig("sandbox", "issue7496_shouldretryshutdown", WithHostNetwork)
|
|
injectShimFailpoint(t, sbConfig, map[string]string{
|
|
"Shutdown": "1*error(please retry)",
|
|
})
|
|
|
|
t.Logf("RunPodSandbox")
|
|
sbID, err := runtimeService.RunPodSandbox(sbConfig, failpointRuntimeHandler)
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("Connect to the shim %s", sbID)
|
|
shimCli := connectToShim(ctx, t, containerdEndpoint, 3, sbID)
|
|
|
|
t.Logf("Log shim %s's pid: %d", sbID, shimPid(ctx, t, shimCli))
|
|
|
|
t.Logf("Subscribe task exit/delete events")
|
|
eventCh, eventErrCh := containerdClient.EventService().Subscribe(ctx,
|
|
fmt.Sprintf(`topic=="%s",event.container_id=="%s"`, ctrdruntime.TaskExitEventTopic, sbID),
|
|
fmt.Sprintf(`topic=="%s",event.container_id=="%s"`, ctrdruntime.TaskDeleteEventTopic, sbID),
|
|
fmt.Sprintf(`topic=="%s"`, eventBarrierTopic),
|
|
)
|
|
|
|
t.Logf("StopPodSandbox and RemovePodSandbox")
|
|
require.NoError(t, runtimeService.StopPodSandbox(sbID))
|
|
require.NoError(t, runtimeService.RemovePodSandbox(sbID))
|
|
|
|
t.Logf("Check the shim connection")
|
|
_, err = shimCli.Connect(ctx, &apitask.ConnectRequest{})
|
|
require.Error(t, err, "should fail to call the shim Connect API")
|
|
require.ErrorContains(t, err, "ttrpc: closed")
|
|
|
|
// The task delete succeeded on the first attempt, so the shim itself has
|
|
// already delivered the exit and delete events. When the retry finally
|
|
// shuts the shim down, ttrpc-callback-on-close must not publish them a
|
|
// second time.
|
|
//
|
|
// REF: https://github.com/containerd/containerd/issues/4769
|
|
t.Logf("Check that task exit/delete events are not duplicated")
|
|
require.NoError(t, containerdClient.EventService().Publish(ctx,
|
|
eventBarrierTopic, &ptypes.Empty{},
|
|
))
|
|
exits, deletes := countTaskExitDeleteEventsUntilBarrier(
|
|
t, sbID, eventBarrierTopic, eventCh, eventErrCh, 10*time.Second,
|
|
)
|
|
require.Equal(t, 1, exits, "task exit event should be published only once")
|
|
require.Equal(t, 1, deletes, "task delete event should be published only once")
|
|
}
|
|
|
|
func TestKillShimPublishesTaskExitAndDeleteEvents(t *testing.T) {
|
|
ctx := namespaces.WithNamespace(t.Context(), "k8s.io")
|
|
|
|
sbConfig := PodSandboxConfig("sandbox", t.Name(), WithHostNetwork)
|
|
sbID, err := runtimeService.RunPodSandbox(sbConfig, "")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, runtimeService.StopPodSandbox(sbID))
|
|
require.NoError(t, runtimeService.RemovePodSandbox(sbID))
|
|
})
|
|
|
|
shimCli := connectToShim(ctx, t, containerdEndpoint, 3, sbID)
|
|
shimPID := shimPid(ctx, t, shimCli)
|
|
|
|
eventCh, eventErrCh := containerdClient.EventService().Subscribe(ctx,
|
|
fmt.Sprintf(`topic=="%s",event.container_id=="%s"`, ctrdruntime.TaskExitEventTopic, sbID),
|
|
fmt.Sprintf(`topic=="%s",event.container_id=="%s"`, ctrdruntime.TaskDeleteEventTopic, sbID),
|
|
)
|
|
|
|
require.NoError(t, syscall.Kill(int(shimPID), syscall.SIGKILL))
|
|
waitForTaskExitDeleteEvents(t, sbID, eventCh, eventErrCh, 10*time.Second)
|
|
}
|
|
|
|
func waitForTaskExitDeleteEvents(t *testing.T, id string,
|
|
ch <-chan *events.Envelope, errCh <-chan error, timeout time.Duration,
|
|
) {
|
|
t.Helper()
|
|
|
|
pending := map[string]struct{}{
|
|
ctrdruntime.TaskExitEventTopic: {},
|
|
ctrdruntime.TaskDeleteEventTopic: {},
|
|
}
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
|
|
for len(pending) != 0 {
|
|
select {
|
|
case <-timer.C:
|
|
t.Fatalf("timed out waiting for task exit/delete events for %q", id)
|
|
case err, ok := <-errCh:
|
|
if !ok {
|
|
t.Fatalf("event subscription closed while waiting for %q", id)
|
|
}
|
|
require.NoError(t, err, "event subscription failed")
|
|
case env, ok := <-ch:
|
|
require.True(t, ok, "event subscription closed while waiting for %q", id)
|
|
t.Logf("received event %q for %q", env.Topic, id)
|
|
delete(pending, env.Topic)
|
|
}
|
|
}
|
|
}
|
|
|
|
// countTaskExitDeleteEventsUntilBarrier counts task exit and delete events for
|
|
// the given container ID until the event stream reaches barrierTopic. The
|
|
// barrier is published after shim deletion has waited for its close callback,
|
|
// so any duplicate events from cleanupAfterDeadShim are ordered before it.
|
|
func countTaskExitDeleteEventsUntilBarrier(t *testing.T, id, barrierTopic string,
|
|
ch <-chan *events.Envelope, errCh <-chan error, timeout time.Duration,
|
|
) (exits, deletes int) {
|
|
t.Helper()
|
|
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-timer.C:
|
|
t.Fatalf("timed out waiting for event barrier %q", barrierTopic)
|
|
case err, ok := <-errCh:
|
|
if !ok {
|
|
t.Fatalf("event subscription closed before barrier %q", barrierTopic)
|
|
}
|
|
require.NoError(t, err, "event subscription failed")
|
|
case env, ok := <-ch:
|
|
require.True(t, ok, "event subscription closed before barrier %q", barrierTopic)
|
|
if env.Topic == barrierTopic {
|
|
return exits, deletes
|
|
}
|
|
|
|
evt, err := typeurl.UnmarshalAny(env.Event)
|
|
require.NoError(t, err, "failed to unmarshal event")
|
|
|
|
switch e := evt.(type) {
|
|
case *eventtypes.TaskExit:
|
|
if e.ContainerID == id {
|
|
t.Logf("received task exit event: %+v", e)
|
|
exits++
|
|
}
|
|
case *eventtypes.TaskDelete:
|
|
if e.ContainerID == id {
|
|
t.Logf("received task delete event: %+v", e)
|
|
deletes++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShutdownShimWhenPauseExitsBeforeWorkload(t *testing.T) {
|
|
ctx := namespaces.WithNamespace(t.Context(), "k8s.io")
|
|
|
|
t.Logf("RunPodSandbox")
|
|
sbConfig := PodSandboxConfig("sandbox", t.Name(), WithHostNetwork)
|
|
sbID, err := runtimeService.RunPodSandbox(sbConfig, "")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = runtimeService.StopPodSandbox(sbID)
|
|
_ = runtimeService.RemovePodSandbox(sbID)
|
|
})
|
|
|
|
t.Logf("Connect to the shim %s", sbID)
|
|
shimCli := connectToShim(ctx, t, containerdEndpoint, 3, sbID)
|
|
|
|
testImage := images.Get(images.BusyBox)
|
|
EnsureImageExists(t, testImage)
|
|
|
|
t.Log("Create a container - sleep 1d")
|
|
containerName := "test-container"
|
|
cnConfig := ContainerConfig(
|
|
containerName,
|
|
testImage,
|
|
WithCommand("sh", "-c", "sleep 1d"),
|
|
WithPidNamespace(runtime.NamespaceMode_CONTAINER),
|
|
)
|
|
cnID, err := runtimeService.CreateContainer(sbID, cnConfig, sbConfig)
|
|
require.NoError(t, err)
|
|
|
|
t.Log("Start the container")
|
|
require.NoError(t, runtimeService.StartContainer(cnID))
|
|
|
|
t.Log("Load pause task and wait")
|
|
pauseContainer, err := containerdClient.LoadContainer(ctx, sbID)
|
|
require.NoError(t, err)
|
|
pauseTask, err := pauseContainer.Task(ctx, nil)
|
|
require.NoError(t, err)
|
|
pauseExitCh, err := pauseTask.Wait(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Log("Load workload task and wait")
|
|
workloadContainer, err := containerdClient.LoadContainer(ctx, cnID)
|
|
require.NoError(t, err)
|
|
workloadTask, err := workloadContainer.Task(ctx, nil)
|
|
require.NoError(t, err)
|
|
workloadExitCh, err := workloadTask.Wait(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Log("Kill pause container by containerd client API")
|
|
require.NoError(t, pauseTask.Kill(ctx, syscall.SIGKILL))
|
|
|
|
select {
|
|
case status := <-pauseExitCh:
|
|
pauseExitStatus, _, err := status.Result()
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint32(137), pauseExitStatus)
|
|
case <-time.After(30 * time.Second):
|
|
t.Fatal("timed out waiting for pause task exit")
|
|
}
|
|
|
|
t.Log("Wait for pause task deletion")
|
|
require.NoError(t, Eventually(func() (bool, error) {
|
|
_, err := pauseContainer.Task(ctx, nil)
|
|
if err == nil {
|
|
return false, nil
|
|
}
|
|
|
|
if errdefs.IsNotFound(err) {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
}, time.Second, 30*time.Second))
|
|
|
|
t.Log("Stop sandbox and wait for workload")
|
|
require.NoError(t, runtimeService.StopPodSandbox(sbID))
|
|
|
|
select {
|
|
case status := <-workloadExitCh:
|
|
workloadExitStatus, _, err := status.Result()
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint32(137), workloadExitStatus)
|
|
case <-time.After(30 * time.Second):
|
|
t.Fatal("timed out waiting for workload task exit")
|
|
}
|
|
|
|
t.Log("Remove sandbox")
|
|
require.NoError(t, runtimeService.RemovePodSandbox(sbID))
|
|
|
|
t.Log("Shim should be shutdown")
|
|
_, err = shimCli.Connect(ctx, &apitask.ConnectRequest{})
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "ttrpc: closed")
|
|
}
|