This commit is contained in:
109
async/closer.go
Normal file
109
async/closer.go
Normal 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()
|
||||
}
|
||||
70
async/closer_example_test.go
Normal file
70
async/closer_example_test.go
Normal 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
95
async/closer_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user