85b9568d0e
Before this change there was a race condition between State.Wait reading the exit code from State and the State being changed instantly after the change which ended the State.Wait. Now, each State.Wait has its own channel which is used to transmit the desired StateStatus at the time the state transitions to the awaited one. Wait no longer reads the status by itself so there is no race. The issue caused the `docker run --restart=always ...' to sometimes exit with 0 exit code, because the process was already restarted by the time State.Wait got the chance to read the exit code. Test run -------- Before: ``` $ go test -count 1 -run TestCorrectStateWaitResultAfterRestart . --- FAIL: TestCorrectStateWaitResultAfterRestart (0.00s) state_test.go:198: expected exit code 10, got 0 FAIL FAIL github.com/docker/docker/container 0.011s FAIL ``` After: ``` $ go test -count 1 -run TestCorrectStateWaitResultAfterRestart . ok github.com/docker/docker/container 0.011s ``` Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
219 lines
5.6 KiB
Go
219 lines
5.6 KiB
Go
package container // import "github.com/docker/docker/container"
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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(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(0, 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(0, 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(0, 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)
|
|
}
|
|
}
|
|
}
|