├── README.md ├── go.mod ├── backoff ├── backoff_test.go └── backoff.go ├── .gitignore ├── .github └── workflows │ └── go.yml ├── fanout ├── fanout_test.go └── fanout.go ├── group ├── example_test.go ├── group.go └── group_test.go ├── retry ├── retry_test.go └── retry.go └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # exp 2 | Experimental packages. 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-kratos/exp 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /backoff/backoff_test.go: -------------------------------------------------------------------------------- 1 | package backoff 2 | 3 | import "testing" 4 | 5 | func TestBackoff(t *testing.T) { 6 | b := New() 7 | for i := 0; i < 10; i++ { 8 | t.Log(b.Backoff(i)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /fanout/fanout_test.go: -------------------------------------------------------------------------------- 1 | package fanout 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestFanout_Do(t *testing.T) { 10 | ca := New("cache", WithWorker(1), WithBuffer(1024)) 11 | var run bool 12 | ca.Do(context.Background(), func(c context.Context) { 13 | run = true 14 | panic("error") 15 | }) 16 | time.Sleep(time.Millisecond * 50) 17 | t.Log("not panic") 18 | if !run { 19 | t.Fatal("expect run be true") 20 | } 21 | } 22 | 23 | func TestFanout_Close(t *testing.T) { 24 | ca := New("cache", WithWorker(1), WithBuffer(1024)) 25 | ca.Close() 26 | err := ca.Do(context.Background(), func(c context.Context) {}) 27 | if err == nil { 28 | t.Fatal("expect get err") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /group/example_test.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import "fmt" 4 | 5 | type Counter struct { 6 | Value int 7 | } 8 | 9 | func (c *Counter) Incr() { 10 | c.Value++ 11 | } 12 | 13 | func ExampleGroup_Get() { 14 | group := NewGroup(func() *Counter { 15 | fmt.Println("Only Once") 16 | return &Counter{} 17 | }) 18 | 19 | // Create a new Counter 20 | group.Get("pass").Incr() 21 | 22 | // Get the created Counter again. 23 | group.Get("pass").Incr() 24 | // Output: 25 | // Only Once 26 | } 27 | 28 | func ExampleGroup_Reset() { 29 | group := NewGroup(func() *Counter { 30 | return &Counter{} 31 | }) 32 | 33 | // Reset the new function and clear all created objects. 34 | group.Reset(func() *Counter { 35 | fmt.Println("reset") 36 | return &Counter{} 37 | }) 38 | 39 | // Create a new Counter 40 | group.Get("pass").Incr() 41 | // Output:reset 42 | } 43 | -------------------------------------------------------------------------------- /retry/retry_test.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | ) 8 | 9 | func TestRetry(t *testing.T) { 10 | r := New() 11 | { 12 | if err := r.Do(context.Background(), func(ctx context.Context) error { 13 | t.Log("retry ok") 14 | return nil 15 | }); err != nil { 16 | t.Fatal(err) 17 | } 18 | } 19 | { 20 | n := 0 21 | err := r.Do(context.Background(), func(ctx context.Context) error { 22 | n++ 23 | t.Log("retry:", n) 24 | return errors.New("retry") 25 | }) 26 | if err == nil { 27 | t.Log("should return an error") 28 | } 29 | if n != r.attempts { 30 | t.Logf("should want %d but got %d", r.attempts, n) 31 | } 32 | } 33 | { 34 | n := 0 35 | nr := New(WithRetryable(func(err error) bool { 36 | return false 37 | })) 38 | err := nr.Do(context.Background(), func(ctx context.Context) error { 39 | n++ 40 | return errors.New("not retry") 41 | }) 42 | if err == nil { 43 | t.Log("should got an error") 44 | } 45 | if n != 1 { 46 | t.Log("should not retry") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kratos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /group/group.go: -------------------------------------------------------------------------------- 1 | // Package group provides a sample lazy load container. 2 | // The group only creating a new object not until the object is needed by user. 3 | // And it will cache all the objects to reduce the creation of object. 4 | package group 5 | 6 | import "sync" 7 | 8 | // Group is a lazy load container. 9 | type Group[T any] struct { 10 | new func() T 11 | vals map[string]T 12 | sync.RWMutex 13 | } 14 | 15 | // NewGroup news a group container. 16 | func NewGroup[T any](new func() T) *Group[T] { 17 | if new == nil { 18 | panic("container.group: can't assign a nil to the new function") 19 | } 20 | return &Group[T]{ 21 | new: new, 22 | vals: make(map[string]T), 23 | } 24 | } 25 | 26 | // Get gets the object by the given key. 27 | func (g *Group[T]) Get(key string) T { 28 | g.RLock() 29 | v, ok := g.vals[key] 30 | if ok { 31 | g.RUnlock() 32 | return v 33 | } 34 | g.RUnlock() 35 | 36 | // slowpath for group don`t have specified key value 37 | g.Lock() 38 | defer g.Unlock() 39 | v, ok = g.vals[key] 40 | if ok { 41 | return v 42 | } 43 | v = g.new() 44 | g.vals[key] = v 45 | return v 46 | } 47 | 48 | // Reset resets the new function and deletes all existing objects. 49 | func (g *Group[T]) Reset(new func() T) { 50 | if new == nil { 51 | panic("container.group: can't assign a nil to the new function") 52 | } 53 | g.Lock() 54 | g.new = new 55 | g.Unlock() 56 | g.Clear() 57 | } 58 | 59 | // Clear deletes all objects. 60 | func (g *Group[T]) Clear() { 61 | g.Lock() 62 | g.vals = make(map[string]T) 63 | g.Unlock() 64 | } 65 | -------------------------------------------------------------------------------- /group/group_test.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestGroupGet(t *testing.T) { 9 | count := 0 10 | g := NewGroup(func() int { 11 | count++ 12 | return count 13 | }) 14 | v := g.Get("key_0") 15 | if !reflect.DeepEqual(v, 1) { 16 | t.Errorf("expect 1, actual %v", v) 17 | } 18 | 19 | v = g.Get("key_1") 20 | if !reflect.DeepEqual(v, 2) { 21 | t.Errorf("expect 2, actual %v", v) 22 | } 23 | 24 | v = g.Get("key_0") 25 | if !reflect.DeepEqual(v, 1) { 26 | t.Errorf("expect 1, actual %v", v) 27 | } 28 | if !reflect.DeepEqual(count, 2) { 29 | t.Errorf("expect count 2, actual %v", count) 30 | } 31 | } 32 | 33 | func TestGroupReset(t *testing.T) { 34 | g := NewGroup(func() int { 35 | return 1 36 | }) 37 | g.Get("key") 38 | call := false 39 | g.Reset(func() int { 40 | call = true 41 | return 1 42 | }) 43 | 44 | length := 0 45 | for range g.vals { 46 | length++ 47 | } 48 | if !reflect.DeepEqual(length, 0) { 49 | t.Errorf("expect length 0, actual %v", length) 50 | } 51 | 52 | g.Get("key") 53 | if !reflect.DeepEqual(call, true) { 54 | t.Errorf("expect call true, actual %v", call) 55 | } 56 | } 57 | 58 | func TestGroupClear(t *testing.T) { 59 | g := NewGroup(func() int { 60 | return 1 61 | }) 62 | g.Get("key") 63 | length := 0 64 | for range g.vals { 65 | length++ 66 | } 67 | if !reflect.DeepEqual(length, 1) { 68 | t.Errorf("expect length 1, actual %v", length) 69 | } 70 | 71 | g.Clear() 72 | length = 0 73 | for range g.vals { 74 | length++ 75 | } 76 | if !reflect.DeepEqual(length, 0) { 77 | t.Errorf("expect length 0, actual %v", length) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /retry/retry.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-kratos/exp/backoff" 8 | ) 9 | 10 | // defaultRetry is a backoff configuration with the default values. 11 | var defaultRetry = New() 12 | 13 | // Option is retry option. 14 | type Option func(*Retry) 15 | 16 | // WithAttempts with attempts. 17 | func WithAttempts(n int) Option { 18 | return func(o *Retry) { 19 | o.attempts = n 20 | } 21 | } 22 | 23 | // WithRetryable with retryable. 24 | func WithRetryable(r Retryable) Option { 25 | return func(o *Retry) { 26 | o.retryable = r 27 | } 28 | } 29 | 30 | // WithBackoff with backoff. 31 | func WithBackoff(b backoff.Strategy) Option { 32 | return func(o *Retry) { 33 | o.backoff = b 34 | } 35 | } 36 | 37 | // Retryable is used to judge whether an error is retryable or not. 38 | type Retryable func(err error) bool 39 | 40 | // Retry config. 41 | type Retry struct { 42 | backoff backoff.Strategy 43 | retryable Retryable 44 | attempts int 45 | } 46 | 47 | // New new a retry with backoff. 48 | func New(opts ...Option) *Retry { 49 | r := &Retry{ 50 | attempts: 2, 51 | retryable: func(err error) bool { return true }, 52 | backoff: backoff.New(), 53 | } 54 | for _, o := range opts { 55 | o(r) 56 | } 57 | return r 58 | } 59 | 60 | // Do wraps func with a backoff to retry. 61 | func (r *Retry) Do(ctx context.Context, fn func(context.Context) error) error { 62 | var ( 63 | err error 64 | retries int 65 | ) 66 | for { 67 | if err = ctx.Err(); err != nil { 68 | break 69 | } 70 | if err = fn(ctx); err == nil { 71 | break 72 | } 73 | if err != nil && !r.retryable(err) { 74 | break 75 | } 76 | retries++ 77 | if r.attempts > 0 && retries >= r.attempts { 78 | break 79 | } 80 | time.Sleep(r.backoff.Backoff(retries)) 81 | } 82 | return err 83 | } 84 | 85 | // Do wraps func with a backoff to retry. 86 | func Do(ctx context.Context, fn func(context.Context) error) error { 87 | return defaultRetry.Do(ctx, fn) 88 | } 89 | 90 | // Infinite wraps func with a backoff to retry. 91 | func Infinite(ctx context.Context, fn func(context.Context) error) error { 92 | r := New(WithAttempts(-1)) 93 | return r.Do(ctx, fn) 94 | } 95 | -------------------------------------------------------------------------------- /fanout/fanout.go: -------------------------------------------------------------------------------- 1 | package fanout 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "runtime" 7 | "sync" 8 | ) 9 | 10 | type options struct { 11 | worker int 12 | buffer int 13 | } 14 | 15 | // Option is fanout option. 16 | type Option func(*options) 17 | 18 | // WithWorker with the worker of fanout. 19 | func WithWorker(n int) Option { 20 | return func(o *options) { 21 | o.worker = n 22 | } 23 | } 24 | 25 | // WithBuffer with the buffer of fanout. 26 | func WithBuffer(n int) Option { 27 | return func(o *options) { 28 | o.buffer = n 29 | } 30 | } 31 | 32 | type item struct { 33 | f func(c context.Context) 34 | ctx context.Context 35 | } 36 | 37 | // Fanout async consume data from chan. 38 | type Fanout struct { 39 | name string 40 | ch chan item 41 | options *options 42 | waiter sync.WaitGroup 43 | 44 | ctx context.Context 45 | cancel func() 46 | } 47 | 48 | // New new a fanout struct. 49 | func New(name string, opts ...Option) *Fanout { 50 | if name == "" { 51 | name = "anonymous" 52 | } 53 | o := &options{ 54 | worker: 1, 55 | buffer: 1000, 56 | } 57 | for _, op := range opts { 58 | op(o) 59 | } 60 | c := &Fanout{ 61 | ch: make(chan item, o.buffer), 62 | name: name, 63 | options: o, 64 | } 65 | c.ctx, c.cancel = context.WithCancel(context.Background()) 66 | c.waiter.Add(o.worker) 67 | for i := 0; i < o.worker; i++ { 68 | go c.proc() 69 | } 70 | return c 71 | } 72 | 73 | func (c *Fanout) proc() { 74 | defer c.waiter.Done() 75 | for { 76 | select { 77 | case t := <-c.ch: 78 | wrapFunc(t.f)(t.ctx) 79 | case <-c.ctx.Done(): 80 | return 81 | } 82 | } 83 | } 84 | 85 | func wrapFunc(f func(c context.Context)) (res func(context.Context)) { 86 | return func(ctx context.Context) { 87 | defer func() { 88 | if r := recover(); r != nil { 89 | buf := make([]byte, 64*1024) 90 | buf = buf[:runtime.Stack(buf, false)] 91 | log.Printf("panic in fanout proc, err: %s, stack: %s\n", r, buf) 92 | } 93 | }() 94 | f(ctx) 95 | } 96 | } 97 | 98 | // Do save a callback func. 99 | func (c *Fanout) Do(ctx context.Context, f func(ctx context.Context)) error { 100 | if f == nil || c.ctx.Err() != nil { 101 | return c.ctx.Err() 102 | } 103 | select { 104 | case c.ch <- item{f: f, ctx: ctx}: 105 | case <-ctx.Done(): 106 | return ctx.Err() 107 | } 108 | return nil 109 | } 110 | 111 | // Close close fanout. 112 | func (c *Fanout) Close() error { 113 | if err := c.ctx.Err(); err != nil { 114 | return err 115 | } 116 | c.cancel() 117 | c.waiter.Wait() 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /backoff/backoff.go: -------------------------------------------------------------------------------- 1 | package backoff 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // defaultBackoff is a backoff configuration with the default values. 9 | var defaultBackoff = New() 10 | 11 | // Option is backoff option. 12 | type Option func(*Exponential) 13 | 14 | // WithBaseDelay with base delay duration. 15 | func WithBaseDelay(d time.Duration) Option { 16 | return func(o *Exponential) { 17 | o.baseDelay = d 18 | } 19 | } 20 | 21 | // WithMaxDelay with max delay duratioin. 22 | func WithMaxDelay(d time.Duration) Option { 23 | return func(o *Exponential) { 24 | o.maxDelay = d 25 | } 26 | } 27 | 28 | // WithMultiplier with multiplier factor. 29 | func WithMultiplier(m float64) Option { 30 | return func(o *Exponential) { 31 | o.multiplier = m 32 | } 33 | } 34 | 35 | // WithJitter with jitter factor. 36 | func WithJitter(j float64) Option { 37 | return func(o *Exponential) { 38 | o.jitter = j 39 | } 40 | } 41 | 42 | // Strategy defines the methodology for backing off after a retry failure. 43 | type Strategy interface { 44 | // Backoff returns the amount of time to wait before the next retry given 45 | // the number of consecutive failures. 46 | Backoff(retries int) time.Duration 47 | } 48 | 49 | // Exponential implements exponential backoff algorithm as defined in 50 | // https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md. 51 | type Exponential struct { 52 | // baseDelay is the amount of time to backoff after the first failure. 53 | baseDelay time.Duration 54 | // multiplier is the factor with which to multiply backoffs after a 55 | // failed retry. Should ideally be greater than 1. 56 | multiplier float64 57 | // jitter is the factor with which backoffs are randomized. 58 | jitter float64 59 | // maxDelay is the upper bound of backoff delay. 60 | maxDelay time.Duration 61 | } 62 | 63 | // New new a Exponential backoff with default options. 64 | func New(opts ...Option) Strategy { 65 | ex := &Exponential{ 66 | baseDelay: 100 * time.Millisecond, 67 | maxDelay: 15 * time.Second, 68 | multiplier: 1.6, 69 | jitter: 0.2, 70 | } 71 | for _, o := range opts { 72 | o(ex) 73 | } 74 | return ex 75 | } 76 | 77 | // Backoff returns the amount of time to wait before the next retry given the 78 | // number of retries. 79 | func (bc Exponential) Backoff(retries int) time.Duration { 80 | if retries == 0 { 81 | return bc.baseDelay 82 | } 83 | backoff, max := float64(bc.baseDelay), float64(bc.maxDelay) 84 | for backoff < max && retries > 0 { 85 | backoff *= bc.multiplier 86 | retries-- 87 | } 88 | if backoff > max { 89 | backoff = max 90 | } 91 | // Randomize backoff delays so that if a cluster of requests start at 92 | // the same time, they won't operate in lockstep. 93 | backoff *= 1 + bc.jitter*(rand.Float64()*2-1) 94 | if backoff < 0 { 95 | return 0 96 | } 97 | return time.Duration(backoff) 98 | } 99 | 100 | // Backoff returns the amount of time to wait before the next retry given the 101 | // number of retries. 102 | func Backoff(retries int) time.Duration { 103 | return defaultBackoff.Backoff(retries) 104 | } 105 | --------------------------------------------------------------------------------