commit b5f9c60818e2c17cb10c26b86c4de70183954c92 Author: andrey1s Date: Tue Apr 27 08:22:11 2021 +0300 first commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..aa3cf9c Binary files /dev/null and b/.DS_Store differ diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..85e235b --- /dev/null +++ b/.drone.yml @@ -0,0 +1,25 @@ +kind: pipeline +name: default + +steps: +- name: test + image: golang + commands: + - go test -parallel 10 --race ./... + +- name: golangci-lint + image: golangci/golangci-lint:v1.39 + commands: + - golangci-lint run + +- name: test routine + image: golang + commands: + - cd routine + - go test -parallel 10 --race ./... + +- name: golangci-lint routine + image: golangci/golangci-lint:v1.39 + commands: + - cd routine + - golangci-lint run diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4d432a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# ---> Go +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..23e5557 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,46 @@ +run: + timeout: 5m + +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + goconst: + min-len: 2 + min-occurrences: 2 + gocyclo: + min-complexity: 15 + golint: + min-confidence: 0 + govet: + check-shadowing: true + lll: + line-length: 140 + maligned: + suggest-new: true + misspell: + locale: US + +linters: + enable-all: true + disable: + - exhaustivestruct + - maligned + - interfacer + - scopelint + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - gomnd + - exhaustivestruct + - wrapcheck + - path: test/* + linters: + - gomnd + - exhaustivestruct + - wrapcheck diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f03940c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +MIT License Copyright (c) 2020 4devs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e596d8a --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# closer + +[![Build Status](https://drone.gitoa.ru/api/badges/go-4devs/closer/status.svg)](https://drone.gitoa.ru/go-4devs/closer) +[![Go Report Card](https://goreportcard.com/badge/gitoa.ru/go-4devs/closer)](https://goreportcard.com/report/gitoa.ru/go-4devs/closer) +[![GoDoc](https://godoc.org/gitoa.ru/go-4devs/closer?status.svg)](http://godoc.org/gitoa.ru/go-4devs/closer) diff --git a/async/closer.go b/async/closer.go new file mode 100644 index 0000000..3630627 --- /dev/null +++ b/async/closer.go @@ -0,0 +1,109 @@ +package async + +import ( + "context" + "os" + "os/signal" + "sync" +) + +// Option configure async closer. +type Option func(*Closer) + +// WithHandleError configure async error handler. +func WithHandleError(he func(error)) Option { + return func(async *Closer) { + async.handler = he + } +} + +// New create new closer with options. +func New(opts ...Option) *Closer { + a := &Closer{} + + for _, o := range opts { + o(a) + } + + return a +} + +// Closer closer. +type Closer struct { + sync.Mutex + once sync.Once + done chan struct{} + fnc []func() error + handler func(error) +} + +// Wait when done context or notify signals or close all. +func (c *Closer) Wait(ctx context.Context, sig ...os.Signal) { + go func() { + ch := make(chan os.Signal, 1) + + if len(sig) > 0 { + signal.Notify(ch, sig...) + defer signal.Stop(ch) + } + select { + case <-ch: + case <-ctx.Done(): + } + + _ = c.Close() + }() + <-c.wait() +} + +func (c *Closer) wait() chan struct{} { + c.Lock() + if c.done == nil { + c.done = make(chan struct{}) + } + c.Unlock() + + return c.done +} + +// Add close functions. +func (c *Closer) Add(f ...func() error) { + c.Lock() + c.fnc = append(c.fnc, f...) + c.Unlock() +} + +// Close close all closers async. +func (c *Closer) Close() error { + c.once.Do(func() { + defer close(c.wait()) + c.Lock() + eh := func(error) {} + if c.handler != nil { + eh = c.handler + } + funcs := c.fnc + c.fnc = nil + c.Unlock() + + errs := make(chan error, len(funcs)) + for _, f := range funcs { + go func(f func() error) { + errs <- f() + }(f) + } + for i := 0; i < cap(errs); i++ { + if err := <-errs; err != nil { + eh(err) + } + } + }) + + return nil +} + +func (c *Closer) SetErrHandler(e func(error)) { + c.Lock() + c.handler = e + c.Unlock() +} diff --git a/async/closer_example_test.go b/async/closer_example_test.go new file mode 100644 index 0000000..2a0a51e --- /dev/null +++ b/async/closer_example_test.go @@ -0,0 +1,70 @@ +package async_test + +import ( + "context" + "fmt" + "syscall" + "time" + + "gitoa.ru/go-4devs/closer/async" + "gitoa.ru/go-4devs/closer/test" +) + +func ExampleCloser_Close() { + cl := async.New() + + cl.Add(func() error { + fmt.Print("do some close") + + return nil + }) + + cl.Close() + // Output: do some close +} + +func ExampleCloser_Wait_cancelContext() { + cl := async.New() + + ctx, cancel := context.WithTimeout(context.TODO(), time.Microsecond) + defer cancel() + + cl.Add(func() error { + fmt.Print("do some close with cancel context") + + return nil + }) + + cl.Wait(ctx) + // Output: do some close with cancel context +} + +func ExampleWithHandleError() { + cl := async.New(async.WithHandleError(func(err error) { + fmt.Printf("logged err:%s", err) + })) + + cl.Add(func() error { + return test.ErrClose + }) + + cl.Close() + // Output: logged err:some error +} + +func ExampleCloser_Wait_syscall() { + time.AfterFunc(time.Millisecond, func() { + _ = syscall.Kill(syscall.Getpid(), syscall.SIGTERM) + }) + + cl := async.New() + + cl.Add(func() error { + fmt.Print("do some close with SIGTERM") + + return nil + }) + + cl.Wait(context.TODO(), syscall.SIGTERM) + // Output: do some close with SIGTERM +} diff --git a/async/closer_test.go b/async/closer_test.go new file mode 100644 index 0000000..6c84d2f --- /dev/null +++ b/async/closer_test.go @@ -0,0 +1,95 @@ +package async_test + +import ( + "context" + "errors" + "sync/atomic" + "syscall" + "testing" + "time" + + "gitoa.ru/go-4devs/closer/async" + "gitoa.ru/go-4devs/closer/test" +) + +func TestAsyncClose(t *testing.T) { + t.Parallel() + + c := async.Closer{} + closedFn := &test.Closed{} + c.Add(closedFn.CloseFnc("one", 0)) + test.RequireNil(t, c.Close()) + closedFn.TestLen(t, 1) + + c.Wait(context.Background()) + test.RequireNil(t, c.Close()) + closedFn.TestLen(t, 1) + + as := async.New(async.WithHandleError(func(e error) { + if !errors.Is(e, test.ErrClose) { + t.Fatalf("expect: %s, got:%s", test.ErrClose, e) + } + })) + as.Add(closedFn.CloseFnc("two", 0), func() error { + test.RequireNil(t, closedFn.CloseFnc("two", 0)()) + + return test.ErrClose + }) + test.RequireNil(t, as.Close()) + closedFn.TestLen(t, 3) + + c = async.Closer{} + closedFn = &test.Closed{} + c.Add( + closedFn.CloseFnc("one", time.Millisecond/2), + closedFn.CloseFnc("two", time.Microsecond), + closedFn.CloseFnc("three", time.Millisecond/5)) + test.RequireNil(t, c.Close()) + + closedFn.TestLen(t, 3) +} + +func TestAsyncWait_Timeout(t *testing.T) { + t.Parallel() + + c := &async.Closer{} + + ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond) + defer cancel() + + var cnt int32 + + go func() { + c.Wait(ctx) + atomic.AddInt32(&cnt, 1) + }() + + cl := func() error { + atomic.AddInt32(&cnt, 1) + + return nil + } + + c.Add(cl) + c.Wait(context.Background()) + + if atomic.LoadInt32(&cnt) != 1 { + t.Fail() + } +} + +func TestAsyncWait_Syscall(t *testing.T) { + t.Parallel() + + c := async.New() + cl := &test.Closed{} + + time.AfterFunc(time.Second, func() { + test.RequireNil(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM)) + }) + + c.Add(cl.CloseFnc("one", 0), cl.CloseFnc("one", 0)) + c.Wait(context.Background(), syscall.SIGTERM, syscall.SIGINT) + + cl.TestLen(t, 2) +} diff --git a/closer.go b/closer.go new file mode 100644 index 0000000..b21b751 --- /dev/null +++ b/closer.go @@ -0,0 +1,53 @@ +package closer + +import ( + "context" + "os" + "time" + + "gitoa.ru/go-4devs/closer/priority" +) + +// nolint: gochecknoglobals +var closer = &priority.Closer{} + +// SetTimeout before close func. +func SetTimeout(t time.Duration) { + closer.SetTimeout(t) +} + +// SetErrHandler before close func. +func SetErrHandler(eh func(error)) { + closer.SetErrHandler(eh) +} + +// Add add closed func. +func Add(f ...func() error) { + closer.Add(f...) +} + +// AddByPriority add close by priority 255 its close first 0 - last. +func AddByPriority(priority uint8, f ...func() error) { + closer.AddByPriority(priority, f...) +} + +// AddLast add closer which execute at the end. +func AddLast(f ...func() error) { + closer.AddLast(f...) +} + +// AddFirst add closer which execute at the begin. +func AddFirst(f ...func() error) { + closer.AddFirst(f...) +} + +// Close all func. +// nolint: wrapcheck +func Close() error { + return closer.Close() +} + +// Wait cancel ctx or signal. +func Wait(ctx context.Context, sig ...os.Signal) { + closer.Wait(ctx, sig...) +} diff --git a/closer_example_test.go b/closer_example_test.go new file mode 100644 index 0000000..d6f31be --- /dev/null +++ b/closer_example_test.go @@ -0,0 +1,125 @@ +package closer_test + +import ( + "context" + "fmt" + "log" + "syscall" + "time" + + "gitoa.ru/go-4devs/closer" + "gitoa.ru/go-4devs/closer/priority" + "gitoa.ru/go-4devs/closer/test" +) + +func ExampleWait() { + ctx, cancel := context.WithTimeout(context.TODO(), time.Microsecond) + defer cancel() + + closer.SetErrHandler(func(err error) { + fmt.Print("\nlogged err:", err.Error()) + }) + + closer.Add(func() error { + fmt.Print("do some close. ") + + return nil + }) + + closer.AddFirst(func() error { + fmt.Print("close http server for example. ") + + return nil + }) + + closer.AddLast(func() error { + fmt.Print("close db for example.") + + return nil + }) + + closer.AddByPriority(priority.Last-1, func() error { + return test.ErrClose + }) + + closer.Wait(ctx) + // Output: + // close http server for example. do some close. close db for example. + // logged err:some error +} + +func ExampleClose() { + closer.Add(func() error { + // normal stop. + + return nil + }, func() error { + time.Sleep(time.Millisecond) + // long normal stop. + + return nil + }) + + closer.AddFirst(func() error { + // first stop. + + return nil + }) + closer.AddLast(func() error { + // last stop. + + return nil + }) + + closer.AddByPriority(priority.First+1, func() error { + // run before first. + + return nil + }) + + closer.AddByPriority(priority.Normal-1, func() error { + // run after normal. + + return nil + }) + closer.Close() +} + +func ExampleWait_cancelContext() { + ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond) + defer cancel() + + closer.Add(func() error { + // do some close with cancel context + + return nil + }) + + closer.Wait(ctx) +} + +func ExampleSetErrHandler() { + closer.SetErrHandler(func(err error) { + log.Print("logged err:", err.Error()) + }) + + closer.Add(func() error { + return test.ErrClose + }) + + closer.Close() +} + +func ExampleWait_syscall() { + time.AfterFunc(time.Millisecond, func() { + _ = syscall.Kill(syscall.Getpid(), syscall.SIGTERM) + }) + + closer.Add(func() error { + // do some close with SIGTERM + + return nil + }) + + closer.Wait(context.TODO(), syscall.SIGTERM) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..90d5b73 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitoa.ru/go-4devs/closer + +go 1.16 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/priority/closer.go b/priority/closer.go new file mode 100644 index 0000000..6aaef47 --- /dev/null +++ b/priority/closer.go @@ -0,0 +1,220 @@ +package priority + +import ( + "context" + "log" + "os" + "os/signal" + "sort" + "sync" + "time" +) + +// defaults. +const ( + First = 250 + Normal = 100 + Last = 5 +) + +// Option configure priority closer. +type Option func(*Closer) + +// WithHandleError configure priority error handler. +func WithHandleError(he func(error)) Option { + return func(async *Closer) { + async.handler = he + } +} + +// WithTimeout configure priority error handler. +func WithTimeout(d time.Duration) Option { + return func(async *Closer) { + async.timeout = d + } +} + +// New create new closer with options. +func New(opts ...Option) *Closer { + a := &Closer{} + + for _, o := range opts { + o(a) + } + + return a +} + +// Closer close by priority. +type Closer struct { + sync.Mutex + fnc map[uint8][]func() error + priority prioritySlice + sumPriority uint + once sync.Once + handler func(error) + timeout time.Duration + len int + done chan struct{} +} + +// Add adds closed func by normal priority. +func (c *Closer) Add(f ...func() error) { + c.AddByPriority(Normal, f...) +} + +// AddLast add closer which execute at the end. +func (c *Closer) AddLast(f ...func() error) { + c.AddByPriority(Last, f...) +} + +// AddFirst add closer which execute at the begin. +func (c *Closer) AddFirst(f ...func() error) { + c.AddByPriority(First, f...) +} + +// AddByPriority add close by priority 255 its close first 0 - last. +func (c *Closer) AddByPriority(priority uint8, f ...func() error) { + if len(f) == 0 { + return + } + + c.Lock() + if c.fnc == nil { + c.fnc = make(map[uint8][]func() error) + } + + if len(c.fnc[priority]) == 0 { + c.priority = append(c.priority, priority) + sort.Sort(c.priority) + c.sumPriority += uint(priority) + } + + c.len += len(f) + c.fnc[priority] = append(c.fnc[priority], f...) + c.Unlock() +} + +// Wait wait signal or cancel context. +func (c *Closer) Wait(ctx context.Context, sig ...os.Signal) { + go func() { + ch := make(chan os.Signal, 1) + + if len(sig) > 0 { + signal.Notify(ch, sig...) + defer signal.Stop(ch) + } + select { + case <-ch: + case <-ctx.Done(): + } + + _ = c.Close() + }() + <-c.wait() +} + +// Close closes all func by priority. +func (c *Closer) Close() error { + c.once.Do(func() { + start := time.Now() + c.Lock() + eh := func(err error) { + log.Print(err) + } + + if c.handler != nil { + eh = c.handler + } + + w := &wait{ + timeout: c.timeout, + priority: c.sumPriority, + } + funcs := c.fnc + c.fnc = nil + c.Unlock() + errs := make(chan error, c.len) + go func() { + defer close(c.wait()) + for i := 0; i < cap(errs); i++ { + if err := <-errs; err != nil { + eh(err) + } + } + }() + for _, p := range c.priority { + var wg sync.WaitGroup + wg.Add(len(funcs[p])) + for _, f := range funcs[p] { + go func(f func() error) { + errs <- f() + wg.Done() + }(f) + } + w.done(start, uint(p), &wg) + } + <-c.wait() + }) + + return nil +} + +// SetTimeout before close func. +func (c *Closer) SetTimeout(t time.Duration) { + c.Lock() + c.timeout = t + c.Unlock() +} + +// SetErrHandler before close func. +func (c *Closer) SetErrHandler(e func(error)) { + c.Lock() + c.handler = e + c.Unlock() +} + +func (c *Closer) wait() chan struct{} { + c.Lock() + if c.done == nil { + c.done = make(chan struct{}) + } + c.Unlock() + + return c.done +} + +type wait struct { + timeout time.Duration + priority uint +} + +func (w *wait) done(start time.Time, priority uint, wg *sync.WaitGroup) { + done := make(chan struct{}) + + go func() { + defer close(done) + wg.Wait() + }() + select { + case <-w.after(start, priority): + case <-done: + } +} + +func (w *wait) after(start time.Time, priority uint) <-chan time.Time { + timeout := (w.timeout - time.Since(start)) / time.Duration(w.priority) * time.Duration(priority) + w.priority -= priority + + if timeout <= 0 { + return nil + } + + return time.After(timeout) +} + +type prioritySlice []uint8 + +func (p prioritySlice) Len() int { return len(p) } +func (p prioritySlice) Less(i, j int) bool { return p[i] > p[j] } +func (p prioritySlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/priority/closer_example_test.go b/priority/closer_example_test.go new file mode 100644 index 0000000..aa02fde --- /dev/null +++ b/priority/closer_example_test.go @@ -0,0 +1,112 @@ +package priority_test + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "syscall" + "time" + + "gitoa.ru/go-4devs/closer/priority" +) + +type closer struct { + msg string +} + +// nolint: forbidigo +func (c *closer) Close() error { + fmt.Print(c.msg) + + return nil +} + +func OpenDB() io.Closer { + return &closer{msg: "close db. "} +} + +func NewConsumer() io.Closer { + return &closer{msg: "close consumer. "} +} + +func ExampleCloser_Wait() { + time.AfterFunc(time.Second/2, func() { + _ = syscall.Kill(syscall.Getpid(), syscall.SIGTERM) + }) + + ctx := context.Background() + + cl := priority.New( + priority.WithTimeout(time.Second*5), + priority.WithHandleError(func(e error) { + fmt.Print(e) + }), + ) + db := OpenDB() + cl.AddLast(db.Close) + + // your consumer events + consumer := NewConsumer() + cl.AddFirst(consumer.Close) + + s := http.Server{} + // your http listeners + listener, _ := net.Listen("tcp", "127.0.0.1:0") + + go func() { + if err := s.Serve(listener); err != nil { + _ = cl.Close() + } + }() + + cl.Add(func() error { + fmt.Print("stop server. ") + + return s.Shutdown(ctx) + }) + + cl.Wait(ctx, syscall.SIGTERM, syscall.SIGINT) + // Output: close consumer. stop server. close db. +} + +func ExampleCloser_Close() { + cl := priority.New() + + cl.Add(func() error { + fmt.Print("normal stop. ") + + return nil + }, func() error { + time.Sleep(time.Millisecond) + fmt.Print("long normal stop. ") + + return nil + }) + + cl.AddFirst(func() error { + fmt.Print("first stop. ") + + return nil + }) + cl.AddLast(func() error { + fmt.Print("last stop. ") + + return nil + }) + + cl.AddByPriority(priority.First+1, func() error { + fmt.Print("run before first. ") + + return nil + }) + cl.AddByPriority(priority.Normal-1, func() error { + fmt.Print("run after normal. ") + + return nil + }) + + cl.Close() + // Output: run before first. first stop. normal stop. long normal stop. run after normal. last stop. +} diff --git a/priority/closer_test.go b/priority/closer_test.go new file mode 100644 index 0000000..4c57d09 --- /dev/null +++ b/priority/closer_test.go @@ -0,0 +1,63 @@ +package priority_test + +import ( + "context" + "syscall" + "testing" + "time" + + "gitoa.ru/go-4devs/closer/priority" + "gitoa.ru/go-4devs/closer/test" +) + +func TestPriority_Close(t *testing.T) { + t.Parallel() + + cl := priority.Closer{} + closed := &test.Closed{} + + go cl.Wait(context.Background()) + + cl.Add() + cl.Add(closed.CloseFnc("one", time.Microsecond)) + cl.AddLast(closed.CloseFnc("last", time.Microsecond)) + cl.AddFirst(closed.CloseFnc("first", time.Millisecond)) + cl.AddByPriority(priority.Normal, closed.CloseFnc("one", time.Microsecond), closed.CloseFnc("one", time.Microsecond)) + test.RequireNil(t, cl.Close()) + closed.TestEqual(t, []string{"first", "one", "one", "one", "last"}) +} + +func TestPriority_Wait_Timeout(t *testing.T) { + t.Parallel() + + closed := &test.Closed{} + + cl := priority.New(priority.WithTimeout(time.Second/5), priority.WithHandleError(func(error) {})) + cl.Add(closed.CloseFnc("one", 0)) + cl.AddByPriority(priority.First, closed.CloseFnc("first", time.Second)) + cl.AddByPriority(priority.Last, closed.CloseFnc("last", 0)) + cl.AddLast(func() error { + return test.ErrClose + }) + + time.AfterFunc(time.Second/3, func() { + test.RequireNil(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM)) + }) + cl.Wait(context.Background(), syscall.SIGTERM) + closed.TestEqual(t, []string{"one", "last", "first"}) +} + +func TestPriority_Wait(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + cl := priority.New(priority.WithHandleError(func(err error) { + test.RequireError(t, err, test.ErrClose) + })) + cl.AddLast(func() error { + return test.ErrClose + }) + cl.Wait(ctx) +} diff --git a/routine/LICENSE b/routine/LICENSE new file mode 100644 index 0000000..f03940c --- /dev/null +++ b/routine/LICENSE @@ -0,0 +1,19 @@ +MIT License Copyright (c) 2020 4devs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/routine/go.mod b/routine/go.mod new file mode 100644 index 0000000..d8b3f14 --- /dev/null +++ b/routine/go.mod @@ -0,0 +1,3 @@ +module gitoa.ru/go-4devs/closer/routine + +go 1.16 diff --git a/routine/runner.go b/routine/runner.go new file mode 100644 index 0000000..2502d32 --- /dev/null +++ b/routine/runner.go @@ -0,0 +1,55 @@ +package routine + +import "sync" + +// nolint: gochecknoglobals +var global = &WaitGroup{} + +// Go run routine and add wait group. +func Go(fnc func()) { + global.Go(fnc) +} + +// Run run routines and add wait group. +func Run(fnc ...func()) { + global.Run(fnc...) +} + +// Close global routines. +func Close() error { + return global.Close() +} + +// Wait wait all go routines. +func Wait() { + global.Wait() +} + +// WaitGroup run func and wait when done. +type WaitGroup struct { + sync.WaitGroup +} + +// Close wait all routines and implement Closer. +func (wg *WaitGroup) Close() error { + wg.Wait() + + return nil +} + +// Go add wait group to routines. +func (wg *WaitGroup) Go(fnc func()) { + wg.Run(fnc) +} + +// Run functions in routine and add wait group. +func (wg *WaitGroup) Run(fnc ...func()) { + wg.Add(len(fnc)) + + for i := range fnc { + go func(i int) { + defer wg.Done() + fnc[i]() + }(i) + } +} diff --git a/routine/runner_examplte_test.go b/routine/runner_examplte_test.go new file mode 100644 index 0000000..1004662 --- /dev/null +++ b/routine/runner_examplte_test.go @@ -0,0 +1,31 @@ +package routine_test + +import ( + "fmt" + "time" + + "gitoa.ru/go-4devs/closer/routine" +) + +func ExampleGo() { + defer routine.Close() + routine.Go(func() { + time.Sleep(time.Microsecond) + fmt.Print("do some job") + }) + + // Output: do some job +} + +func ExampleRun() { + defer routine.Close() + + routine.Run(func() { + time.Sleep(time.Microsecond) + fmt.Print("do some job. ") + }, func() { + fmt.Print("fast job in goroutine. ") + }) + + // Output: fast job in goroutine. do some job. +} diff --git a/routine/runner_test.go b/routine/runner_test.go new file mode 100644 index 0000000..bc4ced9 --- /dev/null +++ b/routine/runner_test.go @@ -0,0 +1,69 @@ +package routine_test + +import ( + "sync" + "testing" + "time" + + "gitoa.ru/go-4devs/closer/routine" +) + +func equal(t *testing.T, exp, res int) { + t.Helper() + + if exp != res { + t.Fail() + } +} + +func TestGo(t *testing.T) { + t.Parallel() + + dt := make(map[string]int) + + var mu sync.Mutex + + fnc := func(name string) func() { + return func() { + time.Sleep(time.Millisecond) + mu.Lock() + dt[name]++ + mu.Unlock() + } + } + + equal(t, 0, dt["once"]) + + routine.Go(fnc("once")) + routine.Run(fnc("twice"), fnc("twice")) + routine.Wait() + + equal(t, 1, dt["once"]) + equal(t, 2, dt["twice"]) +} + +func TestClose(t *testing.T) { + t.Parallel() + + dt := make(map[string]int) + + var mu sync.Mutex + + fnc := func(name string) func() { + return func() { + time.Sleep(time.Millisecond) + mu.Lock() + dt[name]++ + mu.Unlock() + } + } + routine.Go(fnc("once")) + routine.Run(fnc("twice"), fnc("twice")) + + if err := routine.Close(); err != nil { + t.Fail() + } + + equal(t, 1, dt["once"]) + equal(t, 2, dt["twice"]) +} diff --git a/shutdown.go b/shutdown.go new file mode 100644 index 0000000..6f07c21 --- /dev/null +++ b/shutdown.go @@ -0,0 +1,15 @@ +package closer + +import ( + "context" + "time" +) + +func Shutdown(fnc func(context.Context) error, timeout time.Duration) func() error { + return func() error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + return fnc(ctx) + } +} diff --git a/test/closer.go b/test/closer.go new file mode 100644 index 0000000..6a0e342 --- /dev/null +++ b/test/closer.go @@ -0,0 +1,60 @@ +package test + +import ( + "errors" + "sync" + "testing" + "time" +) + +var ErrClose = errors.New("some error") + +type Closed struct { + mu sync.Mutex + d []string +} + +func (c *Closed) TestLen(t *testing.T, exp int) { + if len(c.d) != exp { + t.Fail() + } +} + +func (c *Closed) TestEqual(t *testing.T, exp []string) { + if len(c.d) != len(exp) { + t.Fail() + } + + for i := range exp { + if exp[i] != c.d[i] { + t.Fail() + } + } +} + +func (c *Closed) CloseFnc(name string, sleep time.Duration) func() error { + return func() error { + time.Sleep(sleep) + c.mu.Lock() + c.d = append(c.d, name) + c.mu.Unlock() + + return nil + } +} + +func RequireNil(t *testing.T, exp interface{}) { + t.Helper() + + if exp != nil { + t.Fatalf("expected nil, got %v", exp) + } +} + +func RequireError(t *testing.T, exp, target error) { + t.Helper() + + if !errors.Is(exp, target) { + t.Fatalf("expected %s, got %s", exp, target) + } +}