first commit
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
andrey1s
2021-04-27 08:22:11 +03:00
commit b5f9c60818
23 changed files with 1214 additions and 0 deletions

109
async/closer.go Normal file
View File

@@ -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()
}

View File

@@ -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
}

95
async/closer_test.go Normal file
View File

@@ -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)
}