4bafaa00aa
The containerd client is very chatty at the best of times. Because the libcontained API is stateless and references containers and processes by string ID for every method call, the implementation is essentially forced to use the containerd client in a way which amplifies the number of redundant RPCs invoked to perform any operation. The libcontainerd remote implementation has to reload the containerd container, task and/or process metadata for nearly every operation. This in turn amplifies the number of context switches between dockerd and containerd to perform any container operation or handle a containerd event, increasing the load on the system which could otherwise be allocated to workloads. Overhaul the libcontainerd interface to reduce the impedance mismatch with the containerd client so that the containerd client can be used more efficiently. Split the API out into container, task and process interfaces which the consumer is expected to retain so that libcontainerd can retain state---especially the analogous containerd client objects---without having to manage any state-store inside the libcontainerd client. Signed-off-by: Cory Snider <csnider@mirantis.com>
227 lines
5.8 KiB
Go
227 lines
5.8 KiB
Go
package container // import "github.com/docker/docker/container"
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
libcontainerdtypes "github.com/docker/docker/libcontainerd/types"
|
|
)
|
|
|
|
func TestIsValidHealthString(t *testing.T) {
|
|
contexts := []struct {
|
|
Health string
|
|
Expected bool
|
|
}{
|
|
{types.Healthy, true},
|
|
{types.Unhealthy, true},
|
|
{types.Starting, true},
|
|
{types.NoHealthcheck, true},
|
|
{"fail", false},
|
|
}
|
|
|
|
for _, c := range contexts {
|
|
v := IsValidHealthString(c.Health)
|
|
if v != c.Expected {
|
|
t.Fatalf("Expected %t, but got %t", c.Expected, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
type mockTask struct {
|
|
libcontainerdtypes.Task
|
|
pid uint32
|
|
}
|
|
|
|
func (t *mockTask) Pid() uint32 { return t.pid }
|
|
|
|
func TestStateRunStop(t *testing.T) {
|
|
s := NewState()
|
|
|
|
// Begin another wait with WaitConditionRemoved. It should complete
|
|
// within 200 milliseconds.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
removalWait := s.Wait(ctx, WaitConditionRemoved)
|
|
|
|
// Full lifecycle two times.
|
|
for i := 1; i <= 2; i++ {
|
|
// A wait with WaitConditionNotRunning should return
|
|
// immediately since the state is now either "created" (on the
|
|
// first iteration) or "exited" (on the second iteration). It
|
|
// shouldn't take more than 50 milliseconds.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
// Expectx exit code to be i-1 since it should be the exit
|
|
// code from the previous loop or 0 for the created state.
|
|
if status := <-s.Wait(ctx, WaitConditionNotRunning); status.ExitCode() != i-1 {
|
|
t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i-1, status.Err())
|
|
}
|
|
|
|
// A wait with WaitConditionNextExit should block until the
|
|
// container has started and exited. It shouldn't take more
|
|
// than 100 milliseconds.
|
|
ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
initialWait := s.Wait(ctx, WaitConditionNextExit)
|
|
|
|
// Set the state to "Running".
|
|
s.Lock()
|
|
s.SetRunning(nil, &mockTask{pid: uint32(i)}, true)
|
|
s.Unlock()
|
|
|
|
// Assert desired state.
|
|
if !s.IsRunning() {
|
|
t.Fatal("State not running")
|
|
}
|
|
if s.Pid != i {
|
|
t.Fatalf("Pid %v, expected %v", s.Pid, i)
|
|
}
|
|
if s.ExitCode() != 0 {
|
|
t.Fatalf("ExitCode %v, expected 0", s.ExitCode())
|
|
}
|
|
|
|
// Now that it's running, a wait with WaitConditionNotRunning
|
|
// should block until we stop the container. It shouldn't take
|
|
// more than 100 milliseconds.
|
|
ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
exitWait := s.Wait(ctx, WaitConditionNotRunning)
|
|
|
|
// Set the state to "Exited".
|
|
s.Lock()
|
|
s.SetStopped(&ExitStatus{ExitCode: i})
|
|
s.Unlock()
|
|
|
|
// Assert desired state.
|
|
if s.IsRunning() {
|
|
t.Fatal("State is running")
|
|
}
|
|
if s.ExitCode() != i {
|
|
t.Fatalf("ExitCode %v, expected %v", s.ExitCode(), i)
|
|
}
|
|
if s.Pid != 0 {
|
|
t.Fatalf("Pid %v, expected 0", s.Pid)
|
|
}
|
|
|
|
// Receive the initialWait result.
|
|
if status := <-initialWait; status.ExitCode() != i {
|
|
t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i, status.Err())
|
|
}
|
|
|
|
// Receive the exitWait result.
|
|
if status := <-exitWait; status.ExitCode() != i {
|
|
t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i, status.Err())
|
|
}
|
|
}
|
|
|
|
// Set the state to dead and removed.
|
|
s.Lock()
|
|
s.Dead = true
|
|
s.Unlock()
|
|
s.SetRemoved()
|
|
|
|
// Wait for removed status or timeout.
|
|
if status := <-removalWait; status.ExitCode() != 2 {
|
|
// Should have the final exit code from the loop.
|
|
t.Fatalf("Removal wait exitCode %v, expected %v, err %q", status.ExitCode(), 2, status.Err())
|
|
}
|
|
}
|
|
|
|
func TestStateTimeoutWait(t *testing.T) {
|
|
s := NewState()
|
|
|
|
s.Lock()
|
|
s.SetRunning(nil, nil, true)
|
|
s.Unlock()
|
|
|
|
// Start a wait with a timeout.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
waitC := s.Wait(ctx, WaitConditionNotRunning)
|
|
|
|
// It should timeout *before* this 200ms timer does.
|
|
select {
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("Stop callback doesn't fire in 200 milliseconds")
|
|
case status := <-waitC:
|
|
t.Log("Stop callback fired")
|
|
// Should be a timeout error.
|
|
if status.Err() == nil {
|
|
t.Fatal("expected timeout error, got nil")
|
|
}
|
|
if status.ExitCode() != -1 {
|
|
t.Fatalf("expected exit code %v, got %v", -1, status.ExitCode())
|
|
}
|
|
}
|
|
|
|
s.Lock()
|
|
s.SetStopped(&ExitStatus{ExitCode: 0})
|
|
s.Unlock()
|
|
|
|
// Start another wait with a timeout. This one should return
|
|
// immediately.
|
|
ctx, cancel = context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
waitC = s.Wait(ctx, WaitConditionNotRunning)
|
|
|
|
select {
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("Stop callback doesn't fire in 200 milliseconds")
|
|
case status := <-waitC:
|
|
t.Log("Stop callback fired")
|
|
if status.ExitCode() != 0 {
|
|
t.Fatalf("expected exit code %v, got %v, err %q", 0, status.ExitCode(), status.Err())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Related issue: #39352
|
|
func TestCorrectStateWaitResultAfterRestart(t *testing.T) {
|
|
s := NewState()
|
|
|
|
s.Lock()
|
|
s.SetRunning(nil, nil, true)
|
|
s.Unlock()
|
|
|
|
waitC := s.Wait(context.Background(), WaitConditionNotRunning)
|
|
want := ExitStatus{ExitCode: 10, ExitedAt: time.Now()}
|
|
|
|
s.Lock()
|
|
s.SetRestarting(&want)
|
|
s.Unlock()
|
|
|
|
s.Lock()
|
|
s.SetRunning(nil, nil, true)
|
|
s.Unlock()
|
|
|
|
got := <-waitC
|
|
if got.exitCode != want.ExitCode {
|
|
t.Fatalf("expected exit code %v, got %v", want.ExitCode, got.exitCode)
|
|
}
|
|
}
|
|
|
|
func TestIsValidStateString(t *testing.T) {
|
|
states := []struct {
|
|
state string
|
|
expected bool
|
|
}{
|
|
{"paused", true},
|
|
{"restarting", true},
|
|
{"running", true},
|
|
{"dead", true},
|
|
{"start", false},
|
|
{"created", true},
|
|
{"exited", true},
|
|
{"removing", true},
|
|
{"stop", false},
|
|
}
|
|
|
|
for _, s := range states {
|
|
v := IsValidStateString(s.state)
|
|
if v != s.expected {
|
|
t.Fatalf("Expected %t, but got %t", s.expected, v)
|
|
}
|
|
}
|
|
}
|