Skip to content
Snippets Groups Projects
Commit 6b07f4fa authored by Jono Wenger's avatar Jono Wenger
Browse files

Clean up stoppable

parent 55b44efd
Branches
Tags
1 merge request!23Release
...@@ -16,74 +16,78 @@ import ( ...@@ -16,74 +16,78 @@ import (
"time" "time"
) )
const nameTag = " with cleanup"
type CleanFunc func(duration time.Duration) error
// Cleanup wraps any stoppable and runs a callback after to stop for cleanup // Cleanup wraps any stoppable and runs a callback after to stop for cleanup
// behavior. The cleanup is run under the remainder of the timeout but will not // behavior. The cleanup is run under the remainder of the timeout but will not
// be canceled if the timeout runs out. The cleanup function does not run if the // be canceled if the timeout runs out. The cleanup function does not run if the
// thread does not stop. // thread does not stop.
type Cleanup struct { type Cleanup struct {
stop Stoppable stop Stoppable
// the clean function receives how long it has to run before the timeout, // clean receives how long it has to run before the timeout, this is not
// this is nto expected to be used in most cases // expected to be used in most cases
clean func(duration time.Duration) error clean CleanFunc
running uint32 running uint32
once sync.Once once sync.Once
} }
// NewCleanup creates a new Cleanup from the passed stoppable and function. // NewCleanup creates a new Cleanup from the passed in stoppable and clean
func NewCleanup(stop Stoppable, clean func(duration time.Duration) error) *Cleanup { // function.
func NewCleanup(stop Stoppable, clean CleanFunc) *Cleanup {
return &Cleanup{ return &Cleanup{
stop: stop, stop: stop,
clean: clean, clean: clean,
running: 0, running: stopped,
} }
} }
// IsRunning returns true if the thread is still running and its cleanup has // IsRunning returns true if the thread is still running and its cleanup has
// completed. // completed.
func (c *Cleanup) IsRunning() bool { func (c *Cleanup) IsRunning() bool {
return atomic.LoadUint32(&c.running) == 1 return atomic.LoadUint32(&c.running) == running
} }
// Name returns the name of the stoppable denoting it has cleanup. // Name returns the name of the stoppable denoting it has cleanup.
func (c *Cleanup) Name() string { func (c *Cleanup) Name() string {
return c.stop.Name() + " with cleanup" return c.stop.Name() + nameTag
} }
// Close stops the contained stoppable and runs the cleanup function after. The // Close stops the wrapped stoppable and after, runs the cleanup function. The
// cleanup function does not run if the thread does not stop. // cleanup function does not run if the thread fails to stop.
func (c *Cleanup) Close(timeout time.Duration) error { func (c *Cleanup) Close(timeout time.Duration) error {
var err error var err error
c.once.Do( c.once.Do(
func() { func() {
defer atomic.StoreUint32(&c.running, 0) defer atomic.StoreUint32(&c.running, stopped)
start := netTime.Now() start := netTime.Now()
// Run the stoppable // Close each stoppable
if err := c.stop.Close(timeout); err != nil { if err := c.stop.Close(timeout); err != nil {
err = errors.WithMessagef(err, "Cleanup for %s not executed", err = errors.WithMessagef(err, "Cleanup not executed for %s",
c.stop.Name()) c.stop.Name())
return return
} }
// Run the cleanup function with the remaining time as a timeout // Run the cleanup function with the remaining time as a timeout
elapsed := time.Since(start) elapsed := netTime.Now().Sub(start)
complete := make(chan error, 1) complete := make(chan error, 1)
go func() { go func() {
complete <- c.clean(elapsed) complete <- c.clean(elapsed)
}() }()
timer := time.NewTimer(elapsed)
select { select {
case err := <-complete: case err := <-complete:
if err != nil { if err != nil {
err = errors.WithMessagef(err, "Cleanup for %s failed", err = errors.WithMessagef(err, "Cleanup for %s failed",
c.stop.Name()) c.stop.Name())
} }
case <-timer.C: case <-time.NewTimer(elapsed).C:
err = errors.Errorf("Clean up for %s timeout", c.stop.Name()) err = errors.Errorf("Clean up for %s timed out after %s",
c.stop.Name(), elapsed)
} }
}) })
......
...@@ -11,52 +11,68 @@ import ( ...@@ -11,52 +11,68 @@ import (
"testing" "testing"
) )
// Tests happy path of NewCleanup(). // Tests that NewCleanup returns a Cleanup that is stopped with the given
// Stoppable.
func TestNewCleanup(t *testing.T) { func TestNewCleanup(t *testing.T) {
single := NewSingle("test name") single := NewSingle("testSingle")
cleanup := NewCleanup(single, single.Close) cleanup := NewCleanup(single, single.Close)
if cleanup.stop != single || cleanup.running != 0 { if cleanup.stop != single {
t.Errorf("NewCleanup() returned Single with incorrect values."+ t.Errorf("NewCleanup returned cleanup with incorrect Stoppable."+
"\n\texpected: stop: %v running: %d\n\treceived: stop: %v running: %d", "\nexpected: %+v\nreceived: %+v", single, cleanup.stop)
single, cleanup.stop, 0, cleanup.running) }
if cleanup.running != stopped {
t.Errorf("NewMulti returned Multi with incorrect running."+
"\nexpected: %d\nreceived: %d", stopped, cleanup.running)
} }
} }
// Tests happy path of Cleanup.IsRunning(). // Tests that Cleanup.IsRunning returns the expected value when the Cleanup is
// marked as both running and not running.
func TestCleanup_IsRunning(t *testing.T) { func TestCleanup_IsRunning(t *testing.T) {
single := NewSingle("test name") single := NewSingle("threadName")
cleanup := NewCleanup(single, single.Close) cleanup := NewCleanup(single, single.Close)
if cleanup.IsRunning() { if cleanup.IsRunning() {
t.Errorf("IsRunning() returned false when it should be running.") t.Errorf("IsRunning returned the wrong value when running."+
"\nexpected: %t\nreceived: %t", true, cleanup.IsRunning())
} }
cleanup.running = 1 cleanup.running = running
if !cleanup.IsRunning() { if !single.IsRunning() {
t.Errorf("IsRunning() returned true when it should not be running.") t.Errorf("IsRunning returned the wrong value when running."+
"\nexpected: %t\nreceived: %t", false, single.IsRunning())
} }
} }
// Tests happy path of Cleanup.Name(). // Unit test of Cleanup.Name.
func TestCleanup_Name(t *testing.T) { func TestCleanup_Name(t *testing.T) {
name := "test name" name := "threadName"
single := NewSingle(name) single := NewSingle(name)
cleanup := NewCleanup(single, single.Close) cleanup := NewCleanup(single, single.Close)
if name+" with cleanup" != cleanup.Name() { if name+nameTag != cleanup.Name() {
t.Errorf("Name() returned the incorrect string."+ t.Errorf("Name did not return the expected name."+
"\n\texpected: %s\n\treceived: %s", name+" with cleanup", cleanup.Name()) "\nexpected: %s\nreceived: %s", name+nameTag, cleanup.Name())
} }
} }
// Tests happy path of Cleanup.Close(). // Tests happy path of Cleanup.Close().
func TestCleanup_Close(t *testing.T) { func TestCleanup_Close(t *testing.T) {
single := NewSingle("test name") single := NewSingle("threadName")
cleanup := NewCleanup(single, single.Close) cleanup := NewCleanup(single, single.Close)
// go func() {
// select {
// case <-time.NewTimer(10 * time.Millisecond).C:
// t.Error("Timed out waiting for quit channel.")
// case <-single.Quit():
// }
// }()
err := cleanup.Close(0) err := cleanup.Close(0)
if err != nil { if err != nil {
t.Errorf("Close() returned an error: %v", err) t.Errorf("Close() returned an error: %+v", err)
} }
} }
...@@ -8,14 +8,17 @@ ...@@ -8,14 +8,17 @@
package stoppable package stoppable
import ( import (
"fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
jww "github.com/spf13/jwalterweatherman" jww "github.com/spf13/jwalterweatherman"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
) )
// Error message.
const closeMultiErr = "MultiStopper %s failed to close %d/%d stoppers"
type Multi struct { type Multi struct {
stoppables []Stoppable stoppables []Stoppable
name string name string
...@@ -24,17 +27,17 @@ type Multi struct { ...@@ -24,17 +27,17 @@ type Multi struct {
once sync.Once once sync.Once
} }
// NewMulti returns a new multi stoppable. // NewMulti returns a new multi Stoppable.
func NewMulti(name string) *Multi { func NewMulti(name string) *Multi {
return &Multi{ return &Multi{
name: name, name: name,
running: 1, running: running,
} }
} }
// IsRunning returns true if the thread is still running. // IsRunning returns true if stoppable is marked as running.
func (m *Multi) IsRunning() bool { func (m *Multi) IsRunning() bool {
return atomic.LoadUint32(&m.running) == 1 return atomic.LoadUint32(&m.running) == running
} }
// Add adds the given stoppable to the list of stoppables. // Add adds the given stoppable to the list of stoppables.
...@@ -44,21 +47,18 @@ func (m *Multi) Add(stoppable Stoppable) { ...@@ -44,21 +47,18 @@ func (m *Multi) Add(stoppable Stoppable) {
m.mux.Unlock() m.mux.Unlock()
} }
// Name returns the name of the multi stoppable and the names of all stoppables // Name returns the name of the Multi Stoppable and the names of all stoppables
// it contains. // it contains.
func (m *Multi) Name() string { func (m *Multi) Name() string {
m.mux.RLock() m.mux.RLock()
names := m.name + ": {" defer m.mux.RUnlock()
for _, s := range m.stoppables {
names += s.Name() + ", " names := make([]string, len(m.stoppables))
} for i, s := range m.stoppables {
if len(m.stoppables) > 0 { names[i] = s.Name()
names = names[:len(names)-2]
} }
names += "}"
m.mux.RUnlock()
return names return m.name + ": {" + strings.Join(names, ", ") + "}"
} }
// Close closes all child stoppers. It does not return their errors and assumes // Close closes all child stoppers. It does not return their errors and assumes
...@@ -67,10 +67,10 @@ func (m *Multi) Close(timeout time.Duration) error { ...@@ -67,10 +67,10 @@ func (m *Multi) Close(timeout time.Duration) error {
var err error var err error
m.once.Do( m.once.Do(
func() { func() {
atomic.StoreUint32(&m.running, 0) atomic.StoreUint32(&m.running, stopped)
numErrors := uint32(0) var numErrors uint32
wg := &sync.WaitGroup{} var wg sync.WaitGroup
m.mux.Lock() m.mux.Lock()
for _, stoppable := range m.stoppables { for _, stoppable := range m.stoppables {
...@@ -87,10 +87,9 @@ func (m *Multi) Close(timeout time.Duration) error { ...@@ -87,10 +87,9 @@ func (m *Multi) Close(timeout time.Duration) error {
wg.Wait() wg.Wait()
if numErrors > 0 { if numErrors > 0 {
errStr := fmt.Sprintf("MultiStopper %s failed to close "+ err = errors.Errorf(
"%v/%v stoppers", m.name, numErrors, len(m.stoppables)) closeMultiErr, m.name, numErrors, len(m.stoppables))
jww.ERROR.Println(errStr) jww.ERROR.Print(err.Error())
err = errors.New(errStr)
} }
}) })
......
...@@ -8,107 +8,140 @@ ...@@ -8,107 +8,140 @@
package stoppable package stoppable
import ( import (
"fmt"
"reflect" "reflect"
"strconv"
"strings"
"testing" "testing"
"time" "time"
) )
// Tests happy path of NewMulti(). // Tests that NewMulti returns a Multi that is running with the given name.
func TestNewMulti(t *testing.T) { func TestNewMulti(t *testing.T) {
name := "test name" name := "testMulti"
multi := NewMulti(name) multi := NewMulti(name)
if multi.name != name || multi.running != 1 { if multi.name != name {
t.Errorf("NewMulti() returned Multi with incorrect values."+ t.Errorf("NewMulti returned Multi with incorrect name."+
"\n\texpected: name: %s running: %d\n\treceived: name: %s running: %d", "\nexpected: %s\nreceived: %s", name, multi.name)
name, 1, multi.name, multi.running) }
if multi.running != running {
t.Errorf("NewMulti returned Multi with incorrect running."+
"\nexpected: %d\nreceived: %d", running, multi.running)
} }
} }
// Tests happy path of Multi.IsRunning(). // Tests that Multi.IsRunning returns the expected value when the Multi is
// marked as both running and not running.
func TestMulti_IsRunning(t *testing.T) { func TestMulti_IsRunning(t *testing.T) {
multi := NewMulti("name") multi := NewMulti("testMulti")
if !multi.IsRunning() { if !multi.IsRunning() {
t.Errorf("IsRunning() returned false when it should be running.") t.Errorf("IsRunning returned the wrong value when running."+
"\nexpected: %t\nreceived: %t", true, multi.IsRunning())
} }
multi.running = 0 multi.running = stopped
if multi.IsRunning() { if multi.IsRunning() {
t.Errorf("IsRunning() returned true when it should not be running.") t.Errorf("IsRunning returned the wrong value when not running."+
"\nexpected: %t\nreceived: %t", false, multi.IsRunning())
} }
} }
// Tests happy path of Multi.Add(). // Tests that Multi.Add adds all the stoppables to the list.
func TestMulti_Add(t *testing.T) { func TestMulti_Add(t *testing.T) {
multi := NewMulti("multi name") multi := NewMulti("testMulti")
singles := []*Single{ expected := []Stoppable{
NewSingle("single name 1"), NewSingle("testSingle0"),
NewSingle("single name 2"), NewMulti("testMulti0"),
NewSingle("single name 3"), NewSingle("testSingle1"),
NewMulti("testMulti1"),
} }
for _, single := range singles { for _, stoppable := range expected {
multi.Add(single) multi.Add(stoppable)
} }
for i, single := range singles { if !reflect.DeepEqual(multi.stoppables, expected) {
if !reflect.DeepEqual(single, multi.stoppables[i]) { t.Errorf("Add did not add the correct Stoppables."+
t.Errorf("Add() did not add the correct Stoppables."+ "\nexpected: %+v\nreceived: %+v", multi.stoppables, expected)
"\n\texpected: %#v\n\treceived: %#v", single, multi.stoppables[i])
}
} }
} }
// Tests happy path of Multi.Name(). // Unit test of Multi.Name.
func TestMulti_Name(t *testing.T) { func TestMulti_Name(t *testing.T) {
name := "test name" name := "testMulti"
multi := NewMulti(name) multi := NewMulti(name)
singles := []*Single{
NewSingle("single name 1"), // Add stoppables and created list of their names
NewSingle("single name 2"), var nameList []string
NewSingle("single name 3"), for i := 0; i < 10; i++ {
newName := ""
if i%2 == 0 {
newName = "single" + strconv.Itoa(i)
multi.Add(NewSingle(newName))
} else {
newMulti := NewMulti("multi" + strconv.Itoa(i))
if i != 5 {
newMulti.Add(NewMulti("multiA"))
newMulti.Add(NewMulti("multiB"))
}
multi.Add(newMulti)
newName = newMulti.Name()
} }
expectedNames := []string{ nameList = append(nameList, newName)
name + ": {}",
name + ": {" + singles[0].name + "}",
name + ": {" + singles[0].name + ", " + singles[1].name + "}",
name + ": {" + singles[0].name + ", " + singles[1].name + ", " + singles[2].name + "}",
} }
for i, single := range singles { expected := name + ": {" + strings.Join(nameList, ", ") + "}"
if expectedNames[i] != multi.Name() {
t.Errorf("Name() returned the incorrect string."+ if multi.Name() != expected {
"\n\texpected: %s\n\treceived: %s", expectedNames[0], multi.Name()) t.Errorf("Name failed to return the expected string."+
"\nexpected: %s\nreceived: %s", expected, multi.Name())
} }
multi.Add(single) }
// Tests that Multi.Name returns the expected string when it has no stoppables.
func TestMulti_Name_NoStoppables(t *testing.T) {
name := "testMulti"
multi := NewMulti(name)
expected := name + ": {" + "}"
if multi.Name() != expected {
t.Errorf("Name failed to return the expected string."+
"\nexpected: %s\nreceived: %s", expected, multi.Name())
} }
} }
// Tests happy path of Multi.Close(). // Tests that Multi.Close sends on all Single quit channels.
func TestMulti_Close(t *testing.T) { func TestMulti_Close(t *testing.T) {
// Create new Multi and add Singles to it multi := NewMulti("testMulti")
multi := NewMulti("name")
singles := []*Single{ singles := []*Single{
NewSingle("single name 1"), NewSingle("testSingle0"),
NewSingle("single name 2"), NewSingle("testSingle1"),
NewSingle("single name 3"), NewSingle("testSingle2"),
NewSingle("testSingle3"),
NewSingle("testSingle4"),
} }
for _, single := range singles { for _, single := range singles[:3] {
multi.Add(single) multi.Add(single)
} }
subMulti := NewMulti("subMulti")
for _, single := range singles[3:] {
subMulti.Add(single)
}
multi.Add(subMulti)
go func() { for _, single := range singles {
go func(single *Single) {
select { select {
case <-singles[0].quit: case <-time.NewTimer(5 * time.Millisecond).C:
t.Errorf("Single %s failed to quit.", single.Name())
case <-single.Quit():
} }
select { }(single)
case <-singles[1].quit:
} }
select {
case <-singles[2].quit:
}
}()
err := multi.Close(5 * time.Millisecond) err := multi.Close(5 * time.Millisecond)
if err != nil { if err != nil {
...@@ -120,3 +153,46 @@ func TestMulti_Close(t *testing.T) { ...@@ -120,3 +153,46 @@ func TestMulti_Close(t *testing.T) {
t.Errorf("Close() returned an error: %v", err) t.Errorf("Close() returned an error: %v", err)
} }
} }
// Tests that Multi.Close sends on all Single quit channels.
func TestMulti_Close_Error(t *testing.T) {
multi := NewMulti("testMulti")
singles := []*Single{
NewSingle("testSingle0"),
NewSingle("testSingle1"),
NewSingle("testSingle2"),
NewSingle("testSingle3"),
NewSingle("testSingle4"),
}
for _, single := range singles[:3] {
multi.Add(single)
}
subMulti := NewMulti("subMulti")
for _, single := range singles[3:] {
subMulti.Add(single)
}
multi.Add(subMulti)
for _, single := range singles[:2] {
go func(single *Single) {
select {
case <-time.NewTimer(5 * time.Millisecond).C:
t.Errorf("Single %s failed to quit.", single.Name())
case <-single.Quit():
}
}(single)
}
expectedErr := fmt.Sprintf(closeMultiErr, multi.name, 0, 0)
expectedErr = strings.SplitN(expectedErr, " 0/0", 2)[0]
err := multi.Close(5 * time.Millisecond)
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Errorf("Close() did not return the expected error."+
"\nexpected: %s\nreceived: %v", expectedErr, err)
}
err = multi.Close(0)
if err != nil {
t.Errorf("Close() returned an error: %v", err)
}
}
...@@ -15,8 +15,11 @@ import ( ...@@ -15,8 +15,11 @@ import (
"time" "time"
) )
// Single allows stopping a single goroutine using a channel. // Error message.
// It adheres to the stoppable interface. const closeTimeoutErr = "stopper for %s failed to stop after timeout of %s"
// Single allows stopping a single goroutine using a channel. It adheres to the
// Stoppable interface.
type Single struct { type Single struct {
name string name string
quit chan struct{} quit chan struct{}
...@@ -24,43 +27,44 @@ type Single struct { ...@@ -24,43 +27,44 @@ type Single struct {
once sync.Once once sync.Once
} }
// NewSingle returns a new single stoppable. // NewSingle returns a new single Stoppable.
func NewSingle(name string) *Single { func NewSingle(name string) *Single {
return &Single{ return &Single{
name: name, name: name,
quit: make(chan struct{}), quit: make(chan struct{}),
running: 1, running: running,
} }
} }
// IsRunning returns true if the thread is still running. // IsRunning returns true if stoppable is marked as running.
func (s *Single) IsRunning() bool { func (s *Single) IsRunning() bool {
return atomic.LoadUint32(&s.running) == 1 return atomic.LoadUint32(&s.running) == running
} }
// Quit returns the read only channel it will send the stop signal on. // Quit returns a receive-only channel that will be triggered when the Stoppable
// quits.
func (s *Single) Quit() <-chan struct{} { func (s *Single) Quit() <-chan struct{} {
return s.quit return s.quit
} }
// Name returns the name of the thread. This is designed to be // Name returns the name of the Single Stoppable.
func (s *Single) Name() string { func (s *Single) Name() string {
return s.name return s.name
} }
// Close signals the thread to time out and closes if it is still running. // Close signals the Single to close via the quit channel. Returns an error if
// sending on the quit channel times out.
func (s *Single) Close(timeout time.Duration) error { func (s *Single) Close(timeout time.Duration) error {
var err error var err error
s.once.Do(func() { s.once.Do(func() {
timer := time.NewTimer(timeout)
select { select {
case <-timer.C: case <-time.NewTimer(timeout).C:
jww.ERROR.Printf("Stopper for %s failed to stop after "+ err = errors.Errorf(closeTimeoutErr, s.name, timeout)
"timeout of %s", s.name, timeout) jww.ERROR.Print(err.Error())
err = errors.Errorf("%s failed to close", s.name)
case s.quit <- struct{}{}: case s.quit <- struct{}{}:
} }
atomic.StoreUint32(&s.running, 0) atomic.StoreUint32(&s.running, stopped)
}) })
return err return err
} }
...@@ -8,96 +8,104 @@ ...@@ -8,96 +8,104 @@
package stoppable package stoppable
import ( import (
"fmt"
"testing" "testing"
"time" "time"
) )
// Tests happy path of NewSingle(). // Tests that NewSingle returns a Single with the correct name and running.
func TestNewSingle(t *testing.T) { func TestNewSingle(t *testing.T) {
name := "test name" name := "threadName"
single := NewSingle(name) single := NewSingle(name)
if single.name != name || single.running != 1 { if single.name != name {
t.Errorf("NewSingle() returned Single with incorrect values."+ t.Errorf("NewSingle returned Single with incorrect name."+
"\n\texpected: name: %s running: %d\n\treceived: name: %s running: %d", "\nexpected: %s\nreceived: %s", name, single.name)
name, 1, single.name, single.running) }
if single.running != running {
t.Errorf("NewSingle returned Single with incorrect running."+
"\nexpected: %d\nreceived: %d", running, single.running)
} }
} }
// Tests happy path of Single.IsRunning(). // Tests that Single.IsRunning returns the expected value when the Single is
// marked as both running and not running.
func TestSingle_IsRunning(t *testing.T) { func TestSingle_IsRunning(t *testing.T) {
single := NewSingle("name") single := NewSingle("threadName")
if !single.IsRunning() { if !single.IsRunning() {
t.Errorf("IsRunning() returned false when it should be running.") t.Errorf("IsRunning returned the wrong value when running."+
"\nexpected: %t\nreceived: %t", true, single.IsRunning())
} }
single.running = 0 single.running = stopped
if single.IsRunning() { if single.IsRunning() {
t.Errorf("IsRunning() returned true when it should not be running.") t.Errorf("IsRunning returned the wrong value when not running."+
"\nexpected: %t\nreceived: %t", false, single.IsRunning())
} }
} }
// Tests happy path of Single.Quit(). // Tests that Single.Quit returns a channel that is triggered when the Single
// quit channel is triggered.
func TestSingle_Quit(t *testing.T) { func TestSingle_Quit(t *testing.T) {
single := NewSingle("name") single := NewSingle("threadName")
go func() { go func() {
time.Sleep(150 * time.Nanosecond)
single.quit <- struct{}{}
}()
timer := time.NewTimer(2 * time.Millisecond)
select { select {
case <-timer.C: case <-time.NewTimer(5 * time.Millisecond).C:
t.Errorf("Quit signal not received.") t.Error("Timed out waiting for quit channel.")
case <-single.quit: case <-single.Quit():
} }
}()
single.quit <- struct{}{}
} }
// Tests happy path of Single.Name(). // Unit test of Single.Name.
func TestSingle_Name(t *testing.T) { func TestSingle_Name(t *testing.T) {
name := "test name" name := "threadName"
single := NewSingle(name) single := NewSingle(name)
if name != single.Name() { if name != single.Name() {
t.Errorf("Name() returned the incorrect string."+ t.Errorf("Name did not return the expected name."+
"\n\texpected: %s\n\treceived: %s", name, single.Name()) "\nexpected: %s\nreceived: %s", name, single.Name())
} }
} }
// Test happy path of Single.Close(). // Test happy path of Single.Close().
func TestSingle_Close(t *testing.T) { func TestSingle_Close(t *testing.T) {
single := NewSingle("name") single := NewSingle("threadName")
go func() { go func() {
time.Sleep(150 * time.Nanosecond)
select { select {
case <-single.quit: case <-time.NewTimer(10 * time.Millisecond).C:
t.Error("Timed out waiting for quit channel.")
case <-single.Quit():
} }
}() }()
err := single.Close(5 * time.Millisecond) err := single.Close(5 * time.Millisecond)
if err != nil { if err != nil {
t.Errorf("Close() returned an error: %v", err) t.Errorf("Close returned an error: %v", err)
} }
} }
// Tests that Single.Close() returns an error when the timeout is reached. // Error path: tests that Single.Close returns an error when the timeout is
// reached.
func TestSingle_Close_Error(t *testing.T) { func TestSingle_Close_Error(t *testing.T) {
single := NewSingle("name") single := NewSingle("threadName")
expectedErr := single.name + " failed to close" timeout := time.Millisecond
expectedErr := fmt.Sprintf(closeTimeoutErr, single.Name(), timeout)
go func() { go func() {
time.Sleep(3 * time.Millisecond) time.Sleep(5 * time.Millisecond)
select { <-single.Quit()
case <-single.quit:
}
}() }()
err := single.Close(2 * time.Millisecond) err := single.Close(timeout)
if err == nil { if err == nil || err.Error() != expectedErr {
t.Errorf("Close() did not return the expected error."+ t.Errorf("Close did not return the expected error."+
"\n\texpected: %v\n\treceived: %v", expectedErr, err) "\nexpected: %s\nreceived: %v", expectedErr, err)
} }
} }
...@@ -9,7 +9,12 @@ package stoppable ...@@ -9,7 +9,12 @@ package stoppable
import "time" import "time"
// Interface for stopping a goroutine. const (
stopped = 0
running = 1
)
// Stoppable interface for stopping a goroutine.
type Stoppable interface { type Stoppable interface {
Close(timeout time.Duration) error Close(timeout time.Duration) error
IsRunning() bool IsRunning() bool
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment