312 lines
7.9 KiB
Go
312 lines
7.9 KiB
Go
/*
|
|
Copyright 2015 The Kubernetes 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 tasks
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
log "github.com/golang/glog"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
type badWriteCloser struct {
|
|
err error
|
|
}
|
|
|
|
func (b *badWriteCloser) Write(_ []byte) (int, error) { return 0, b.err }
|
|
func (b *badWriteCloser) Close() error { return b.err }
|
|
|
|
type discardCloser int
|
|
|
|
func (d discardCloser) Write(b []byte) (int, error) { return len(b), nil }
|
|
func (d discardCloser) Close() error { return nil }
|
|
|
|
var devNull = func() io.WriteCloser { return discardCloser(0) }
|
|
|
|
type fakeExitError uint32
|
|
|
|
func (f fakeExitError) Sys() interface{} { return syscall.WaitStatus(f << 8) }
|
|
func (f fakeExitError) Error() string { return fmt.Sprintf("fake-exit-error: %d", f) }
|
|
|
|
type fakeProcess struct {
|
|
done chan struct{}
|
|
pid int
|
|
err error
|
|
}
|
|
|
|
func (f *fakeProcess) Wait() error {
|
|
<-f.done
|
|
return f.err
|
|
}
|
|
func (f *fakeProcess) Kill(_ bool) (int, error) {
|
|
close(f.done)
|
|
return f.pid, f.err
|
|
}
|
|
func (f *fakeProcess) exit(code int) {
|
|
f.err = fakeExitError(code)
|
|
close(f.done)
|
|
}
|
|
|
|
func newFakeProcess() *fakeProcess {
|
|
return &fakeProcess{
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func TestBadLogger(t *testing.T) {
|
|
err := errors.New("qux")
|
|
fp := newFakeProcess()
|
|
tt := New("foo", "bar", nil, func() io.WriteCloser {
|
|
defer func() {
|
|
fp.pid = 123 // sanity check
|
|
fp.Kill(false) // this causes Wait() to return
|
|
}()
|
|
return &badWriteCloser{err}
|
|
})
|
|
|
|
tt.RestartDelay = 0 // don't slow the test down for no good reason
|
|
|
|
finishCalled := make(chan struct{})
|
|
tt.Finished = func(ok bool) bool {
|
|
log.Infof("tt.Finished: ok %t", ok)
|
|
if ok {
|
|
close(finishCalled)
|
|
}
|
|
return false // never respawn, this causes t.done to close
|
|
}
|
|
|
|
// abuse eventsImpl: we're not going to listen on the task completion or event chans,
|
|
// and we don't want to block the state machine, so discard all task events as they happen
|
|
ei := newEventsImpl(tt.completedCh, tt.done)
|
|
ei.Close()
|
|
|
|
go tt.run(func(_ *Task) taskStateFn {
|
|
log.Infof("tt initialized")
|
|
tt.initLogging(bytes.NewBuffer(([]byte)("unlogged bytes")))
|
|
tt.cmd = fp
|
|
return taskRunning
|
|
})
|
|
|
|
// if the logger fails the task will be killed
|
|
// badWriteLogger generates an error immediately and results in a task kill
|
|
<-finishCalled
|
|
<-tt.done
|
|
|
|
// this should never data race since the state machine is dead at this point
|
|
if fp.pid != 123 {
|
|
t.Fatalf("incorrect pid, expected 123 not %d", fp.pid)
|
|
}
|
|
|
|
// TODO(jdef) would be nice to check for a specific error that indicates the logger died
|
|
}
|
|
|
|
func TestMergeOutput(t *testing.T) {
|
|
var tasksStarted, tasksDone sync.WaitGroup
|
|
tasksDone.Add(2)
|
|
tasksStarted.Add(2)
|
|
|
|
t1 := New("foo", "", nil, devNull)
|
|
t1exited := make(chan struct{})
|
|
t1.RestartDelay = 0 // don't slow the test down for no good reason
|
|
t1.Finished = func(ok bool) bool {
|
|
// we expect each of these cases to happen exactly once
|
|
if !ok {
|
|
tasksDone.Done()
|
|
} else {
|
|
close(t1exited)
|
|
}
|
|
return ok
|
|
}
|
|
go t1.run(func(t *Task) taskStateFn {
|
|
defer tasksStarted.Done()
|
|
t.initLogging(bytes.NewBuffer([]byte{}))
|
|
t.cmd = newFakeProcess()
|
|
return taskRunning
|
|
})
|
|
|
|
t2 := New("bar", "", nil, devNull)
|
|
t2exited := make(chan struct{})
|
|
t2.RestartDelay = 0 // don't slow the test down for no good reason
|
|
t2.Finished = func(ok bool) bool {
|
|
// we expect each of these cases to happen exactly once
|
|
if !ok {
|
|
tasksDone.Done()
|
|
} else {
|
|
close(t2exited)
|
|
}
|
|
return ok
|
|
}
|
|
go t2.run(func(t *Task) taskStateFn {
|
|
defer tasksStarted.Done()
|
|
t.initLogging(bytes.NewBuffer([]byte{}))
|
|
t.cmd = newFakeProcess()
|
|
return taskRunning
|
|
})
|
|
|
|
shouldQuit := make(chan struct{})
|
|
te := MergeOutput([]*Task{t1, t2}, shouldQuit)
|
|
|
|
tasksStarted.Wait()
|
|
tasksStarted.Add(2) // recycle the barrier
|
|
|
|
// kill each task once, let it restart; make sure that we get the completion status?
|
|
t1.cmd.(*fakeProcess).exit(1)
|
|
t2.cmd.(*fakeProcess).exit(2)
|
|
|
|
codes := map[int]struct{}{}
|
|
for i := 0; i < 2; i++ {
|
|
switch tc := <-te.Completion(); tc.code {
|
|
case 1, 2:
|
|
codes[tc.code] = struct{}{}
|
|
default:
|
|
if tc.err != nil {
|
|
t.Errorf("unexpected task completion error: %v", tc.err)
|
|
} else {
|
|
t.Errorf("unexpected task completion code: %d", tc.code)
|
|
}
|
|
}
|
|
}
|
|
|
|
te.Close() // we're not going to read any other completion or error events
|
|
|
|
if len(codes) != 2 {
|
|
t.Fatalf("expected each task process to exit once")
|
|
}
|
|
|
|
// each task invokes Finished() once
|
|
<-t1exited
|
|
<-t2exited
|
|
|
|
log.Infoln("each task process has completed one round")
|
|
tasksStarted.Wait() // tasks will auto-restart their exited procs
|
|
|
|
// assert that the tasks are not dead; TODO(jdef) not sure that these checks are useful
|
|
select {
|
|
case <-t1.done:
|
|
t.Fatalf("t1 is unexpectedly dead")
|
|
default:
|
|
}
|
|
select {
|
|
case <-t2.done:
|
|
t.Fatalf("t2 is unexpectedly dead")
|
|
default:
|
|
}
|
|
|
|
log.Infoln("firing quit signal")
|
|
close(shouldQuit) // fire shouldQuit, and everything should terminate gracefully
|
|
|
|
log.Infoln("waiting for tasks to die")
|
|
tasksDone.Wait() // our tasks should die
|
|
|
|
log.Infoln("waiting for merge to complete")
|
|
<-te.Done() // wait for the merge to complete
|
|
}
|
|
|
|
type fakeTimer struct {
|
|
ch chan time.Time
|
|
}
|
|
|
|
func (t *fakeTimer) set(d time.Duration) {}
|
|
func (t *fakeTimer) discard() {}
|
|
func (t *fakeTimer) await() <-chan time.Time { return t.ch }
|
|
func (t *fakeTimer) expire() { t.ch = make(chan time.Time); close(t.ch) }
|
|
func (t *fakeTimer) reset() { t.ch = nil }
|
|
|
|
func TestAfterDeath(t *testing.T) {
|
|
// test kill escalation since that's not covered by other unit tests
|
|
t1 := New("foo", "", nil, devNull)
|
|
kills := 0
|
|
waitCh := make(chan *Completion, 1)
|
|
timer := &fakeTimer{}
|
|
timer.expire()
|
|
t1.killFunc = func(force bool) (int, error) {
|
|
// > 0 is intentional, multiple calls to close() should panic
|
|
if kills > 0 {
|
|
assert.True(t, force)
|
|
timer.reset() // don't want to race w/ waitCh
|
|
waitCh <- &Completion{name: t1.name, code: 123}
|
|
close(waitCh)
|
|
} else {
|
|
assert.False(t, force)
|
|
}
|
|
kills++
|
|
return 0, nil
|
|
}
|
|
wr := t1.awaitDeath(timer, 0, waitCh)
|
|
assert.Equal(t, "foo", wr.name)
|
|
assert.Equal(t, 123, wr.code)
|
|
assert.NoError(t, wr.err)
|
|
|
|
// test tie between shouldQuit and waitCh
|
|
waitCh = make(chan *Completion, 1)
|
|
waitCh <- &Completion{name: t1.name, code: 456}
|
|
close(waitCh)
|
|
t1.killFunc = func(force bool) (int, error) {
|
|
t.Fatalf("should not attempt to kill a task that has already reported completion")
|
|
return 0, nil
|
|
}
|
|
|
|
timer.reset() // don't race w/ waitCh
|
|
wr = t1.awaitDeath(timer, 0, waitCh)
|
|
assert.Equal(t, 456, wr.code)
|
|
assert.NoError(t, wr.err)
|
|
|
|
// test delayed killFunc failure
|
|
kills = 0
|
|
killFailed := errors.New("for some reason kill failed")
|
|
t1.killFunc = func(force bool) (int, error) {
|
|
// > 0 is intentional, multiple calls to close() should panic
|
|
if kills > 0 {
|
|
assert.True(t, force)
|
|
return -1, killFailed
|
|
} else {
|
|
assert.False(t, force)
|
|
}
|
|
kills++
|
|
return 0, nil
|
|
}
|
|
timer.expire()
|
|
wr = t1.awaitDeath(timer, 0, nil)
|
|
assert.Equal(t, "foo", wr.name)
|
|
assert.Error(t, wr.err)
|
|
|
|
// test initial killFunc failure
|
|
kills = 0
|
|
t1.killFunc = func(force bool) (int, error) {
|
|
// > 0 is intentional, multiple calls to close() should panic
|
|
if kills > 0 {
|
|
assert.True(t, force)
|
|
t.Fatalf("killFunc should only be invoked once, not again after is has already failed")
|
|
} else {
|
|
assert.False(t, force)
|
|
}
|
|
kills++
|
|
return 0, killFailed
|
|
}
|
|
timer.expire()
|
|
wr = t1.awaitDeath(timer, 0, nil)
|
|
assert.Equal(t, "foo", wr.name)
|
|
assert.Error(t, wr.err)
|
|
}
|