mirror of
https://github.com/moby/moby.git
synced 2026-08-02 22:26:52 +00:00
Improve performance of function daemon.Containers() (used by docker ps) to mitigate a latency increase when running large number of containers using the containerd image store. We do this by refactoring daemon.Containers() to collect info for containers in parallel, rather than sequentially, using up to log2(N) worker threads. This improves the performance from O(N) to O(log2(N)), where N is the number of containers. To verify correctness, this commits adds unit and integration tests. Signed-off-by: Cesar Talledo <cesar.talledo@docker.com>
209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
package container
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"runtime"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/network"
|
|
"github.com/docker/docker/client"
|
|
"github.com/docker/docker/pkg/stdcopy"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"gotest.tools/v3/assert"
|
|
)
|
|
|
|
// TestContainerConfig holds container configuration struct that
|
|
// are used in api calls.
|
|
type TestContainerConfig struct {
|
|
Name string
|
|
Config *container.Config
|
|
HostConfig *container.HostConfig
|
|
NetworkingConfig *network.NetworkingConfig
|
|
Platform *ocispec.Platform
|
|
}
|
|
|
|
// NewTestConfig creates a new TestContainerConfig with the provided options.
|
|
//
|
|
// If no options are passed, it creates a default config, which is a busybox
|
|
// container running "top" (on Linux) or "sleep" (on Windows).
|
|
func NewTestConfig(ops ...func(*TestContainerConfig)) *TestContainerConfig {
|
|
cmd := []string{"top"}
|
|
if runtime.GOOS == "windows" {
|
|
cmd = []string{"sleep", "240"}
|
|
}
|
|
config := &TestContainerConfig{
|
|
Config: &container.Config{
|
|
Image: "busybox",
|
|
Cmd: cmd,
|
|
},
|
|
HostConfig: &container.HostConfig{},
|
|
NetworkingConfig: &network.NetworkingConfig{},
|
|
}
|
|
|
|
for _, op := range ops {
|
|
op(config)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// Create creates a container with the specified options, asserting that there was no error.
|
|
func Create(ctx context.Context, t *testing.T, apiClient client.APIClient, ops ...func(*TestContainerConfig)) string {
|
|
t.Helper()
|
|
config := NewTestConfig(ops...)
|
|
c, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
|
|
assert.NilError(t, err)
|
|
|
|
return c.ID
|
|
}
|
|
|
|
// CreateFromConfig creates a container from the given TestContainerConfig.
|
|
//
|
|
// Example use:
|
|
//
|
|
// ctr, err := container.CreateFromConfig(ctx, apiClient, container.NewTestConfig(container.WithAutoRemove))
|
|
// assert.Check(t, err)
|
|
func CreateFromConfig(ctx context.Context, apiClient client.APIClient, config *TestContainerConfig) (container.CreateResponse, error) {
|
|
return apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
|
|
}
|
|
|
|
// Run creates and start a container with the specified options
|
|
func Run(ctx context.Context, t *testing.T, apiClient client.APIClient, ops ...func(*TestContainerConfig)) string {
|
|
t.Helper()
|
|
id := Create(ctx, t, apiClient, ops...)
|
|
|
|
err := apiClient.ContainerStart(ctx, id, container.StartOptions{})
|
|
assert.NilError(t, err)
|
|
|
|
return id
|
|
}
|
|
|
|
type RunResult struct {
|
|
ContainerID string
|
|
ExitCode int
|
|
Stdout *bytes.Buffer
|
|
Stderr *bytes.Buffer
|
|
}
|
|
|
|
func RunAttach(ctx context.Context, t *testing.T, apiClient client.APIClient, ops ...func(config *TestContainerConfig)) RunResult {
|
|
t.Helper()
|
|
|
|
ops = append(ops, func(c *TestContainerConfig) {
|
|
c.Config.AttachStdout = true
|
|
c.Config.AttachStderr = true
|
|
})
|
|
id := Create(ctx, t, apiClient, ops...)
|
|
|
|
aresp, err := apiClient.ContainerAttach(ctx, id, container.AttachOptions{
|
|
Stream: true,
|
|
Stdout: true,
|
|
Stderr: true,
|
|
})
|
|
assert.NilError(t, err)
|
|
|
|
err = apiClient.ContainerStart(ctx, id, container.StartOptions{})
|
|
assert.NilError(t, err)
|
|
|
|
s, err := demultiplexStreams(ctx, aresp)
|
|
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
|
|
assert.NilError(t, err)
|
|
}
|
|
|
|
// Inspect to get the exit code. A new context is used here to make sure that if the context passed as argument as
|
|
// reached timeout during the demultiplexStream call, we still return a RunResult.
|
|
resp, err := apiClient.ContainerInspect(context.Background(), id)
|
|
assert.NilError(t, err)
|
|
|
|
return RunResult{ContainerID: id, ExitCode: resp.State.ExitCode, Stdout: &s.stdout, Stderr: &s.stderr}
|
|
}
|
|
|
|
type streams struct {
|
|
stdout, stderr bytes.Buffer
|
|
}
|
|
|
|
// demultiplexStreams starts a goroutine to demultiplex stdout and stderr from the types.HijackedResponse resp and
|
|
// waits until either multiplexed stream reaches EOF or the context expires. It unconditionally closes resp and waits
|
|
// until the demultiplexing goroutine has finished its work before returning.
|
|
func demultiplexStreams(ctx context.Context, resp types.HijackedResponse) (streams, error) {
|
|
var s streams
|
|
outputDone := make(chan error, 1)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
_, err := stdcopy.StdCopy(&s.stdout, &s.stderr, resp.Reader)
|
|
outputDone <- err
|
|
wg.Done()
|
|
}()
|
|
|
|
var err error
|
|
select {
|
|
case copyErr := <-outputDone:
|
|
err = copyErr
|
|
break
|
|
case <-ctx.Done():
|
|
err = ctx.Err()
|
|
}
|
|
|
|
resp.Close()
|
|
wg.Wait()
|
|
return s, err
|
|
}
|
|
|
|
func Remove(ctx context.Context, t *testing.T, apiClient client.APIClient, container string, options container.RemoveOptions) {
|
|
t.Helper()
|
|
|
|
err := apiClient.ContainerRemove(ctx, container, options)
|
|
assert.NilError(t, err)
|
|
}
|
|
|
|
func RemoveAll(ctx context.Context, t *testing.T, apiClient client.APIClient) {
|
|
t.Helper()
|
|
|
|
containers, err := apiClient.ContainerList(ctx, container.ListOptions{All: true})
|
|
assert.NilError(t, err)
|
|
|
|
for _, c := range containers {
|
|
Remove(ctx, t, apiClient, c.ID, container.RemoveOptions{Force: true})
|
|
}
|
|
}
|
|
|
|
func Inspect(ctx context.Context, t *testing.T, apiClient client.APIClient, containerRef string) container.InspectResponse {
|
|
t.Helper()
|
|
|
|
c, err := apiClient.ContainerInspect(ctx, containerRef)
|
|
assert.NilError(t, err)
|
|
|
|
return c
|
|
}
|
|
|
|
type ContainerOutput struct {
|
|
Stdout, Stderr string
|
|
}
|
|
|
|
// Output waits for the container to end running and returns its output.
|
|
func Output(ctx context.Context, client client.APIClient, id string) (ContainerOutput, error) {
|
|
logs, err := client.ContainerLogs(ctx, id, container.LogsOptions{Follow: true, ShowStdout: true, ShowStderr: true})
|
|
if err != nil {
|
|
return ContainerOutput{}, err
|
|
}
|
|
|
|
defer logs.Close()
|
|
|
|
var stdoutBuf, stderrBuf bytes.Buffer
|
|
_, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, logs)
|
|
if err != nil {
|
|
return ContainerOutput{}, err
|
|
}
|
|
|
|
return ContainerOutput{
|
|
Stdout: stdoutBuf.String(),
|
|
Stderr: stderrBuf.String(),
|
|
}, nil
|
|
}
|