Skip to content

Commit

Permalink
Added ErrDying. Renamed ErrStillRunning to ErrStillAlive.
Browse files Browse the repository at this point in the history
  • Loading branch information
niemeyer committed Apr 3, 2012
1 parent f45c1e0 commit 4079bec
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 15 deletions.
33 changes: 27 additions & 6 deletions tomb.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ import (
// explicit blocking until the state changes, and also to selectively
// unblock select statements accordingly.
//
// When the tomb state changes to dying and there's still logic going
// on within the goroutine, nested functions and methos may choose to
// return ErrDying as their error value, as this error won't alter the
// tomb state if provied to the Kill method. This is a convenient way to
// follow standard Go practices in the context of a dying tomb.
//
// For background and a detailed example, see the following blog post:
//
// http://blog.labix.org/2011/10/09/death-of-goroutines-under-control
Expand All @@ -63,14 +69,17 @@ type Tomb struct {
reason error
}

var ErrStillRunning = errors.New("tomb: goroutine is still running")
var (
ErrStillAlive = errors.New("tomb: still alive")
ErrDying = errors.New("tomb: dying")
)

func (t *Tomb) init() {
t.m.Lock()
if t.dead == nil {
t.dead = make(chan struct{})
t.dying = make(chan struct{})
t.reason = ErrStillRunning
t.reason = ErrStillAlive
}
t.m.Unlock()
}
Expand Down Expand Up @@ -108,12 +117,24 @@ func (t *Tomb) Done() {
}

// Kill flags the goroutine as dying for the given reason.
// Kill may be called multiple times, but only the first non-nil error is
// recorded as the reason for termination.
// Kill may be called multiple times, but only the first
// non-nil error is recorded as the reason for termination.
//
// If reason is ErrDying, the previous reason isn't replaced
// even if it is nil. It's a runtime error to call Kill with
// ErrDying if t is not in a dying state.
func (t *Tomb) Kill(reason error) {
t.init()
t.m.Lock()
if t.reason == nil || t.reason == ErrStillRunning {
if reason == ErrDying {
alive := t.reason == ErrStillAlive
t.m.Unlock()
if alive {
panic("tomb: Kill with ErrDying while still alive")
}
return
}
if t.reason == nil || t.reason == ErrStillAlive {
t.reason = reason
}
// If the receive on t.dying succeeds, then
Expand All @@ -136,7 +157,7 @@ func (t *Tomb) Killf(f string, a ...interface{}) error {
}

// Err returns the reason for the goroutine death provided via Kill
// or Killf, or ErrStillRunning when the goroutine is still alive.
// or Killf, or ErrStillAlive when the goroutine is still alive.
func (t *Tomb) Err() (reason error) {
t.init()
t.m.Lock()
Expand Down
45 changes: 36 additions & 9 deletions tomb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import (
)

func TestNewTomb(t *testing.T) {
tb := new(tomb.Tomb)
testState(t, tb, false, false, tomb.ErrStillRunning)
tb := &tomb.Tomb{}
testState(t, tb, false, false, tomb.ErrStillAlive)

tb.Done()
testState(t, tb, true, true, nil)
}

func TestKill(t *testing.T) {
// a nil reason flags the goroutine as dying
tb := new(tomb.Tomb)
tb := &tomb.Tomb{}
tb.Kill(nil)
testState(t, tb, true, false, nil)

Expand All @@ -35,10 +35,12 @@ func TestKill(t *testing.T) {
}

func TestKillf(t *testing.T) {
tb := new(tomb.Tomb)
tb := &tomb.Tomb{}

err := errors.New("BOOM")
tb.Killf("BO%s", "OM")
err := tb.Killf("BO%s", "OM")
if s := err.Error(); s != "BOOM" {
t.Fatalf(`Killf("BO%s", "OM"): want "BOOM", got %q`, s)
}
testState(t, tb, true, false, err)

// another non-nil reason won't replace the first one
Expand All @@ -49,6 +51,31 @@ func TestKillf(t *testing.T) {
testState(t, tb, true, true, err)
}

func TestErrDying(t *testing.T) {
// ErrDying being used properly, after a clean death.
tb := &tomb.Tomb{}
tb.Kill(nil)
tb.Kill(tomb.ErrDying)
testState(t, tb, true, false, nil)

// ErrDying being used properly, after an errorful death.
err := errors.New("some error")
tb.Kill(err)
tb.Kill(tomb.ErrDying)
testState(t, tb, true, false, err)

// ErrDying being use badly, with an alive tomb.
tb = &tomb.Tomb{}
defer func() {
err := recover()
if err != "tomb: Kill with ErrDying while still alive" {
t.Fatalf("Wrong panic on Kill(ErrDying): %v", err)
}
testState(t, tb, false, false, tomb.ErrStillAlive)
}()
tb.Kill(tomb.ErrDying)
}

func testState(t *testing.T, tb *tomb.Tomb, wantDying, wantDead bool, wantErr error) {
select {
case <-tb.Dying():
Expand All @@ -72,14 +99,14 @@ func testState(t *testing.T, tb *tomb.Tomb, wantDying, wantDead bool, wantErr er
t.Error("<-Dead: should not block")
}
}
if err := tb.Err(); !reflect.DeepEqual(err, wantErr) {
if err := tb.Err(); err != wantErr {
t.Errorf("Err: want %#v, got %#v", wantErr, err)
}
if wantDead && seemsDead {
waitErr := tb.Wait()
switch {
case waitErr == tomb.ErrStillRunning:
t.Errorf("Wait should not return ErrStillRunning")
case waitErr == tomb.ErrStillAlive:
t.Errorf("Wait should not return ErrStillAlive")
case !reflect.DeepEqual(waitErr, wantErr):
t.Errorf("Wait: want %#v, got %#v", wantErr, waitErr)
}
Expand Down

0 comments on commit 4079bec

Please sign in to comment.