From fd3e0bb37c0dcb97622635f583c3c23cbc2b9488 Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Sun, 11 Jan 2015 11:57:53 -0800 Subject: [PATCH] added temp-err-catcher --- Godeps/Godeps.json | 4 + .../jbenet/go-temp-err-catcher/.travis.yml | 9 + .../jbenet/go-temp-err-catcher/README.md | 78 ++++++++ .../jbenet/go-temp-err-catcher/doc.go | 62 +++++++ .../go-temp-err-catcher/example/example.go | 47 +++++ .../jbenet/go-temp-err-catcher/tec_test.go | 172 ++++++++++++++++++ .../go-temp-err-catcher/temp_err_catcher.go | 124 +++++++++++++ 7 files changed, 496 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/README.md create mode 100644 Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/doc.go create mode 100644 Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/example/example.go create mode 100644 Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/tec_test.go create mode 100644 Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/temp_err_catcher.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index ba905c64c..ed8405cef 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -150,6 +150,10 @@ "ImportPath": "github.com/jbenet/go-random", "Rev": "2e83344e7dc7898f94501665af34edd4aa95a013" }, + { + "ImportPath": "github.com/jbenet/go-temp-err-catcher", + "Rev": "c531232018e678b2a702cfb86b5c3f68d1c8beb8" + }, { "ImportPath": "github.com/jbenet/goprocess", "Rev": "162148a58668ca38b0b8f0459ccc6ca88e32f1f4" diff --git a/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/.travis.yml b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/.travis.yml new file mode 100644 index 000000000..60e3d02d8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/.travis.yml @@ -0,0 +1,9 @@ +language: go + +go: + - 1.3 + - release + - tip + +script: + - go test -v diff --git a/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/README.md b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/README.md new file mode 100644 index 000000000..acd4cf536 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/README.md @@ -0,0 +1,78 @@ +# go-temp-err-catcher + +This is a little package to use with your net.Listeners. + +Docs: https://godoc.org/github.com/jbenet/go-temp-err-catcher + +Get: + + go get github.com/jbenet/go-temp-err-catcher + +## Examples + +It is meant to be used with things like net.Lister.Accept: + +```go +import ( + tec "github.com/jbenet/go-temp-err-catcher" +) + +func listen(listener net.Listener) { + var c tec.TempErrCatcher + + for { + conn, err := listener.Accept() + if err != nil && c.IsTemporary(c) { + continue + } + return conn, err + } +} +``` + +You can make your errors implement `Temporary`: + +```go +type errTemp struct { + e error +} + +func (e errTemp) Temporary() bool { + return true +} + +func (e errTemp) Error() string { + return e.e.Error() +} + +err := errors.New("beep boop") +var c tec.TempErrCatcher +c.IsTemporary(err) // false +c.IsTemporary(errTemp{err}) // true +``` + +Or just use `ErrTemp`: + +```go +err := errors.New("beep boop") +var c tec.TempErrCatcher +c.IsTemporary(err) // false +c.IsTemporary(tec.ErrTemp{err}) // true +``` + + +You can also define an `IsTemp` function to classify errors: + +```go +var ErrSkip = errors.New("this should be skipped") +var ErrNotSkip = errors.New("this should not be skipped") + +var c tec.TempErrCatcher +c.IsTemp = func(e error) bool { + return e == ErrSkip +} + +c.IsTemporary(ErrSkip) // true +c.IsTemporary(ErrNotSkip) // false +c.IsTemporary(ErrTemp) // false! no longer accepts Temporary() +``` diff --git a/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/doc.go b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/doc.go new file mode 100644 index 000000000..766c8b6f7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/doc.go @@ -0,0 +1,62 @@ +// Package temperrcatcher provides a TempErrCatcher object, +// which implements simple error-retrying functionality. +// It is meant to be used with things like net.Lister.Accept: +// +// import ( +// tec "github.com/jbenet/go-temp-err-catcher" +// ) +// +// func listen(listener net.Listener) { +// var c tec.TempErrCatcher +// +// for { +// conn, err := listener.Accept() +// if err != nil && c.IsTemporary(c) { +// continue +// } +// return conn, err +// } +// } +// +// You can make your errors implement `Temporary`: +// +// type errTemp struct { +// e error +// } +// +// func (e errTemp) Temporary() bool { +// return true +// } +// +// func (e errTemp) Error() string { +// return e.e.Error() +// } +// +// err := errors.New("beep boop") +// var c tec.TempErrCatcher +// c.IsTemporary(err) // false +// c.IsTemporary(errTemp{err}) // true +// +// Or just use `ErrTemp`: +// +// err := errors.New("beep boop") +// var c tec.TempErrCatcher +// c.IsTemporary(err) // false +// c.IsTemporary(tec.ErrTemp{err}) // true +// +// +// You can also define an `IsTemp` function to classify errors: +// +// var ErrSkip = errors.New("this should be skipped") +// var ErrNotSkip = errors.New("this should not be skipped") +// +// var c tec.TempErrCatcher +// c.IsTemp = func(e error) bool { +// return e == ErrSkip +// } +// +// c.IsTemporary(ErrSkip) // true +// c.IsTemporary(ErrNotSkip) // false +// c.IsTemporary(ErrTemp) // false! no longer accepts Temporary() +// +package temperrcatcher diff --git a/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/example/example.go b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/example/example.go new file mode 100644 index 000000000..fc10f20d9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/example/example.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + + tec "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher" +) + +var ( + ErrTemp = tec.ErrTemporary{fmt.Errorf("ErrTemp")} + ErrSkip = fmt.Errorf("ErrSkip") + ErrOther = fmt.Errorf("ErrOther") +) + +func main() { + var normal tec.TempErrCatcher + var skipper tec.TempErrCatcher + skipper.IsTemp = func(e error) bool { + return e == ErrSkip + } + + fmt.Println("trying normal (uses Temporary interface)") + tryTec(normal) + fmt.Println("") + fmt.Println("trying skipper (uses our IsTemp function)") + tryTec(skipper) +} + +func tryTec(c tec.TempErrCatcher) { + errs := []error{ + ErrTemp, + ErrSkip, + ErrOther, + ErrTemp, + ErrSkip, + ErrOther, + } + + for _, e := range errs { + if c.IsTemporary(e) { + fmt.Printf("\tIsTemporary: true - skipped %s\n", e) + continue + } + + fmt.Printf("\tIsTemporary: false - not skipped %s\n", e) + } +} diff --git a/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/tec_test.go b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/tec_test.go new file mode 100644 index 000000000..71dd219eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/tec_test.go @@ -0,0 +1,172 @@ +package temperrcatcher + +import ( + "fmt" + "testing" + "time" +) + +var ( + ErrTemp = ErrTemporary{fmt.Errorf("ErrTemp")} + ErrSkip = fmt.Errorf("ErrSkip") + ErrOther = fmt.Errorf("ErrOther") +) + +func testTec(t *testing.T, c TempErrCatcher, errs map[error]bool) { + for e, expected := range errs { + if c.IsTemporary(e) != expected { + t.Error("expected %s to be %v", e, expected) + } + } +} + +func TestNil(t *testing.T) { + var c TempErrCatcher + testTec(t, c, map[error]bool{ + ErrTemp: true, + ErrSkip: false, + ErrOther: false, + }) +} + +func TestWait(t *testing.T) { + var c TempErrCatcher + worked := make(chan time.Duration, 3) + c.Wait = func(t time.Duration) { + worked <- t + } + testTec(t, c, map[error]bool{ + ErrTemp: true, + ErrSkip: false, + ErrOther: false, + }) + + // should've called it once + select { + case <-worked: + default: + t.Error("did not call our Wait func") + } + + // should've called it ONLY once + select { + case <-worked: + t.Error("called our Wait func more than once") + default: + } +} + +func TestTemporary(t *testing.T) { + var c TempErrCatcher + testTec(t, c, map[error]bool{ + ErrTemp: true, + ErrSkip: false, + ErrOther: false, + }) +} + +func TestDoubles(t *testing.T) { + last := time.Now() + diff := func() time.Duration { + now := time.Now() + diff := now.Sub(last) + last = now + return diff + } + + testDiff := func(low, hi time.Duration) { + d := diff() + grace := time.Duration(time.Microsecond) + if (d + grace) < low { + t.Error("time difference is smaller than", low, d) + } + if (d - grace) > hi { + t.Error("time difference is greater than", hi, d) + } + } + + var c TempErrCatcher + testDiff(0, c.Start) + c.IsTemporary(ErrTemp) + testDiff(c.Start, 2*c.Start) // first time. + c.IsTemporary(ErrTemp) + testDiff(2*c.Start, 4*c.Start) // second time. + c.IsTemporary(ErrTemp) + testDiff(4*c.Start, 8*c.Start) // third time. +} + +func TestDifferentStart(t *testing.T) { + last := time.Now() + diff := func() time.Duration { + now := time.Now() + diff := now.Sub(last) + last = now + return diff + } + + testDiff := func(low, hi time.Duration) { + d := diff() + grace := time.Duration(time.Microsecond) + if (d + grace) < low { + t.Error("time difference is smaller than", low, d) + } + if (d - grace) > hi { + t.Error("time difference is greater than", hi, d) + } + } + + var c TempErrCatcher + f := time.Millisecond + testDiff(0, f) + c.IsTemporary(ErrTemp) + testDiff(f, 2*f) // first time. + c.IsTemporary(ErrTemp) + testDiff(2*f, 4*f) // second time. + c.IsTemporary(ErrTemp) + testDiff(4*f, 8*f) // third time. + + c.Reset() + c.Start = 10 * time.Millisecond + f = c.Start + testDiff(0, f) + c.IsTemporary(ErrTemp) + testDiff(f, 2*f) // first time. + c.IsTemporary(ErrTemp) + testDiff(2*f, 4*f) // second time. + c.IsTemporary(ErrTemp) + testDiff(4*f, 8*f) // third time. +} + +func TestDifferentStreaks(t *testing.T) { + var c TempErrCatcher + // one streak + c.IsTemporary(ErrTemp) // 1 + c.IsTemporary(ErrTemp) // 2 + c.IsTemporary(ErrTemp) // 4 + expect := 4 * time.Millisecond + if c.delay != expect { + t.Error("delay should be:", expect, c.delay) + } + + <-time.After(c.delay * 10) + + // a different streak + c.IsTemporary(ErrTemp) // 1 + c.IsTemporary(ErrTemp) // 2 + c.IsTemporary(ErrTemp) // 4 + if c.delay != expect { + t.Error("delay should be:", expect, c.delay) + } +} + +func TestFunc(t *testing.T) { + var c TempErrCatcher + c.IsTemp = func(e error) bool { + return e == ErrSkip + } + testTec(t, c, map[error]bool{ + ErrTemp: false, + ErrSkip: true, + ErrOther: false, + }) +} diff --git a/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/temp_err_catcher.go b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/temp_err_catcher.go new file mode 100644 index 000000000..fe8e780c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jbenet/go-temp-err-catcher/temp_err_catcher.go @@ -0,0 +1,124 @@ +// Package temperrcatcher provides a TempErrCatcher object, +// which implements simple error-retrying functionality. +package temperrcatcher + +import ( + "time" +) + +// InitialDelay governs how long to wait the first time. +// This is defaulted to time.Millisecond, which makes sense +// for network listener failures. You may want a much smaller +// delay. You can configure this package wide, or in each +// TempErrCatcher +var InitialDelay = time.Millisecond + +// Temporary is an interface errors can implement to +// ensure they are correctly classified by the default +// TempErrCatcher classifier +type Temporary interface { + Temporary() bool +} + +// ErrIsTemporary returns whether an error is Temporary(), +// iff it implements the Temporary interface. +func ErrIsTemporary(e error) bool { + te, ok := e.(Temporary) + return ok && te.Temporary() +} + +// TempErrCatcher catches temporary errors for you. It then sleeps +// for a bit before returning (you should then try again). This may +// seem odd, but it's exactly what net/http does: +// http://golang.org/src/net/http/server.go?s=51504:51550#L1728 +// +// You can set a few options in TempErrCatcher. They all have defaults +// so a zero TempErrCatcher is ready to be used: +// +// var c tec.TempErrCatcher +// c.IsTemporary(tempErr) +// +type TempErrCatcher struct { + IsTemp func(error) bool // the classifier to use. default: ErrIsTemporary + Wait func(time.Duration) // the wait func to call. default: time.Sleep + Max time.Duration // the maximum time to wait. default: time.Second + Start time.Duration // the delay to start with. default: InitialDelay + delay time.Duration + last time.Time +} + +func (tec *TempErrCatcher) init() { + if tec.Max == 0 { + tec.Max = time.Second + } + if tec.IsTemp == nil { + tec.IsTemp = ErrIsTemporary + } + if tec.Wait == nil { + tec.Wait = time.Sleep + } + if tec.Start == 0 { + tec.Start = InitialDelay + } +} + +// IsTemporary checks whether an error is temporary. It will call +// tec.Wait before returning, with a delay. The delay is also +// doubled, so we do not constantly spin. This is the strategy +// net.Listener uses. +// +// Note: you will want to call Reset() if you get a success, +// so that the stored delay is brough back to 0. +func (tec *TempErrCatcher) IsTemporary(e error) bool { + tec.init() + if tec.IsTemp(e) { + now := time.Now() + if now.Sub(tec.last) > (tec.delay * 5) { + // this is a "new streak" of temp failures. reset. + tec.Reset() + } + + if tec.delay == 0 { // init case. + tec.delay = tec.Start + } else { + tec.delay *= 2 + } + + if tec.delay > tec.Max { + tec.delay = tec.Max + } + tec.Wait(tec.delay) + tec.last = now + return true + } + tec.Reset() // different failure. call reset + return false +} + +// Reset sets the internal delay counter to 0 +func (tec *TempErrCatcher) Reset() { + tec.delay = 0 +} + +// ErrTemporary wraps any error and implements Temporary function. +// +// err := errors.New("beep boop") +// var c tec.TempErrCatcher +// c.IsTemporary(err) // false +// c.IsTemporary(tec.ErrTemp{err}) // true +// +type ErrTemporary struct { + Err error +} + +func (e ErrTemporary) Temporary() bool { + return true +} + +func (e ErrTemporary) Error() string { + return e.Err.Error() +} + +func (e ErrTemporary) String() string { + return e.Error() +}