This commit is contained in:
25
.drone.yml
Normal file
25
.drone.yml
Normal file
@@ -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
|
||||
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -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/
|
||||
|
||||
46
.golangci.yml
Normal file
46
.golangci.yml
Normal file
@@ -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
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -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.
|
||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# closer
|
||||
|
||||
[](https://drone.gitoa.ru/go-4devs/closer)
|
||||
[](https://goreportcard.com/report/gitoa.ru/go-4devs/closer)
|
||||
[](http://godoc.org/gitoa.ru/go-4devs/closer)
|
||||
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)
|
||||
}
|
||||
53
closer.go
Normal file
53
closer.go
Normal file
@@ -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...)
|
||||
}
|
||||
125
closer_example_test.go
Normal file
125
closer_example_test.go
Normal file
@@ -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)
|
||||
}
|
||||
220
priority/closer.go
Normal file
220
priority/closer.go
Normal file
@@ -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] }
|
||||
112
priority/closer_example_test.go
Normal file
112
priority/closer_example_test.go
Normal file
@@ -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.
|
||||
}
|
||||
63
priority/closer_test.go
Normal file
63
priority/closer_test.go
Normal file
@@ -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)
|
||||
}
|
||||
19
routine/LICENSE
Normal file
19
routine/LICENSE
Normal file
@@ -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.
|
||||
3
routine/go.mod
Normal file
3
routine/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module gitoa.ru/go-4devs/closer/routine
|
||||
|
||||
go 1.16
|
||||
55
routine/runner.go
Normal file
55
routine/runner.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
31
routine/runner_examplte_test.go
Normal file
31
routine/runner_examplte_test.go
Normal file
@@ -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.
|
||||
}
|
||||
69
routine/runner_test.go
Normal file
69
routine/runner_test.go
Normal file
@@ -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"])
|
||||
}
|
||||
15
shutdown.go
Normal file
15
shutdown.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
60
test/closer.go
Normal file
60
test/closer.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user