initial import

This commit is contained in:
kevin
2020-07-26 17:09:05 +08:00
commit 7e3a369a8f
647 changed files with 54754 additions and 0 deletions

38
core/syncx/atomicbool.go Normal file
View File

@@ -0,0 +1,38 @@
package syncx
import "sync/atomic"
type AtomicBool uint32
func NewAtomicBool() *AtomicBool {
return new(AtomicBool)
}
func ForAtomicBool(val bool) *AtomicBool {
b := NewAtomicBool()
b.Set(val)
return b
}
func (b *AtomicBool) CompareAndSwap(old, val bool) bool {
var ov, nv uint32
if old {
ov = 1
}
if val {
nv = 1
}
return atomic.CompareAndSwapUint32((*uint32)(b), ov, nv)
}
func (b *AtomicBool) Set(v bool) {
if v {
atomic.StoreUint32((*uint32)(b), 1)
} else {
atomic.StoreUint32((*uint32)(b), 0)
}
}
func (b *AtomicBool) True() bool {
return atomic.LoadUint32((*uint32)(b)) == 1
}

View File

@@ -0,0 +1,27 @@
package syncx
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAtomicBool(t *testing.T) {
val := ForAtomicBool(true)
assert.True(t, val.True())
val.Set(false)
assert.False(t, val.True())
val.Set(true)
assert.True(t, val.True())
val.Set(false)
assert.False(t, val.True())
ok := val.CompareAndSwap(false, true)
assert.True(t, ok)
assert.True(t, val.True())
ok = val.CompareAndSwap(true, false)
assert.True(t, ok)
assert.False(t, val.True())
ok = val.CompareAndSwap(true, false)
assert.False(t, ok)
assert.False(t, val.True())
}

View File

@@ -0,0 +1,30 @@
package syncx
import (
"sync/atomic"
"time"
)
type AtomicDuration int64
func NewAtomicDuration() *AtomicDuration {
return new(AtomicDuration)
}
func ForAtomicDuration(val time.Duration) *AtomicDuration {
d := NewAtomicDuration()
d.Set(val)
return d
}
func (d *AtomicDuration) CompareAndSwap(old, val time.Duration) bool {
return atomic.CompareAndSwapInt64((*int64)(d), int64(old), int64(val))
}
func (d *AtomicDuration) Load() time.Duration {
return time.Duration(atomic.LoadInt64((*int64)(d)))
}
func (d *AtomicDuration) Set(val time.Duration) {
atomic.StoreInt64((*int64)(d), int64(val))
}

View File

@@ -0,0 +1,19 @@
package syncx
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestAtomicDuration(t *testing.T) {
d := ForAtomicDuration(time.Duration(100))
assert.Equal(t, time.Duration(100), d.Load())
d.Set(time.Duration(200))
assert.Equal(t, time.Duration(200), d.Load())
assert.True(t, d.CompareAndSwap(time.Duration(200), time.Duration(300)))
assert.Equal(t, time.Duration(300), d.Load())
assert.False(t, d.CompareAndSwap(time.Duration(200), time.Duration(400)))
assert.Equal(t, time.Duration(300), d.Load())
}

View File

@@ -0,0 +1,40 @@
package syncx
import (
"math"
"sync/atomic"
)
type AtomicFloat64 uint64
func NewAtomicFloat64() *AtomicFloat64 {
return new(AtomicFloat64)
}
func ForAtomicFloat64(val float64) *AtomicFloat64 {
f := NewAtomicFloat64()
f.Set(val)
return f
}
func (f *AtomicFloat64) Add(val float64) float64 {
for {
old := f.Load()
nv := old + val
if f.CompareAndSwap(old, nv) {
return nv
}
}
}
func (f *AtomicFloat64) CompareAndSwap(old, val float64) bool {
return atomic.CompareAndSwapUint64((*uint64)(f), math.Float64bits(old), math.Float64bits(val))
}
func (f *AtomicFloat64) Load() float64 {
return math.Float64frombits(atomic.LoadUint64((*uint64)(f)))
}
func (f *AtomicFloat64) Set(val float64) {
atomic.StoreUint64((*uint64)(f), math.Float64bits(val))
}

View File

@@ -0,0 +1,24 @@
package syncx
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAtomicFloat64(t *testing.T) {
f := ForAtomicFloat64(100)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
for i := 0; i < 100; i++ {
f.Add(1)
}
wg.Done()
}()
}
wg.Wait()
assert.Equal(t, float64(600), f.Load())
}

13
core/syncx/barrier.go Normal file
View File

@@ -0,0 +1,13 @@
package syncx
import "sync"
type Barrier struct {
lock sync.Mutex
}
func (b *Barrier) Guard(fn func()) {
b.lock.Lock()
defer b.lock.Unlock()
fn()
}

View File

@@ -0,0 +1,31 @@
package syncx
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBarrier_Guard(t *testing.T) {
const total = 10000
var barrier Barrier
var count int
for i := 0; i < total; i++ {
barrier.Guard(func() {
count++
})
}
assert.Equal(t, total, count)
}
func TestBarrierPtr_Guard(t *testing.T) {
const total = 10000
barrier := new(Barrier)
var count int
for i := 0; i < total; i++ {
barrier.Guard(func() {
count++
})
}
assert.Equal(t, total, count)
}

47
core/syncx/cond.go Normal file
View File

@@ -0,0 +1,47 @@
package syncx
import (
"time"
"zero/core/lang"
"zero/core/timex"
)
type Cond struct {
signal chan lang.PlaceholderType
}
func NewCond() *Cond {
return &Cond{
signal: make(chan lang.PlaceholderType),
}
}
// WaitWithTimeout wait for signal return remain wait time or timed out
func (cond *Cond) WaitWithTimeout(timeout time.Duration) (time.Duration, bool) {
timer := time.NewTimer(timeout)
defer timer.Stop()
begin := timex.Now()
select {
case <-cond.signal:
elapsed := timex.Since(begin)
remainTimeout := timeout - elapsed
return remainTimeout, true
case <-timer.C:
return 0, false
}
}
// Wait for signal
func (cond *Cond) Wait() {
<-cond.signal
}
// Signal wakes one goroutine waiting on c, if there is any.
func (cond *Cond) Signal() {
select {
case cond.signal <- lang.Placeholder:
default:
}
}

73
core/syncx/cond_test.go Normal file
View File

@@ -0,0 +1,73 @@
package syncx
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTimeoutCondWait(t *testing.T) {
var wait sync.WaitGroup
cond := NewCond()
wait.Add(2)
go func() {
cond.Wait()
wait.Done()
}()
time.Sleep(time.Duration(50) * time.Millisecond)
go func() {
cond.Signal()
wait.Done()
}()
wait.Wait()
}
func TestTimeoutCondWaitTimeout(t *testing.T) {
var wait sync.WaitGroup
cond := NewCond()
wait.Add(1)
go func() {
cond.WaitWithTimeout(time.Duration(500) * time.Millisecond)
wait.Done()
}()
wait.Wait()
}
func TestTimeoutCondWaitTimeoutRemain(t *testing.T) {
var wait sync.WaitGroup
cond := NewCond()
wait.Add(2)
ch := make(chan time.Duration, 1)
defer close(ch)
timeout := time.Duration(2000) * time.Millisecond
go func() {
remainTimeout, _ := cond.WaitWithTimeout(timeout)
ch <- remainTimeout
wait.Done()
}()
sleep(200)
go func() {
cond.Signal()
wait.Done()
}()
wait.Wait()
remainTimeout := <-ch
assert.True(t, remainTimeout < timeout, "expect remainTimeout %v < %v", remainTimeout, timeout)
assert.True(t, remainTimeout >= time.Duration(200)*time.Millisecond,
"expect remainTimeout %v >= 200 millisecond", remainTimeout)
}
func TestSignalNoWait(t *testing.T) {
cond := NewCond()
cond.Signal()
}
func sleep(millisecond int) {
time.Sleep(time.Duration(millisecond) * time.Millisecond)
}
func currentTimeMillis() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}

28
core/syncx/donechan.go Normal file
View File

@@ -0,0 +1,28 @@
package syncx
import (
"sync"
"zero/core/lang"
)
type DoneChan struct {
done chan lang.PlaceholderType
once sync.Once
}
func NewDoneChan() *DoneChan {
return &DoneChan{
done: make(chan lang.PlaceholderType),
}
}
func (dc *DoneChan) Close() {
dc.once.Do(func() {
close(dc.done)
})
}
func (dc *DoneChan) Done() chan lang.PlaceholderType {
return dc.done
}

View File

@@ -0,0 +1,33 @@
package syncx
import (
"sync"
"testing"
)
func TestDoneChanClose(t *testing.T) {
doneChan := NewDoneChan()
for i := 0; i < 5; i++ {
doneChan.Close()
}
}
func TestDoneChanDone(t *testing.T) {
var waitGroup sync.WaitGroup
doneChan := NewDoneChan()
waitGroup.Add(1)
go func() {
select {
case <-doneChan.Done():
waitGroup.Done()
}
}()
for i := 0; i < 5; i++ {
doneChan.Close()
}
waitGroup.Wait()
}

View File

@@ -0,0 +1,77 @@
package syncx
import (
"sync"
"time"
"zero/core/timex"
)
const defaultRefreshInterval = time.Second
type (
ImmutableResourceOption func(resource *ImmutableResource)
ImmutableResource struct {
fetch func() (interface{}, error)
resource interface{}
err error
lock sync.RWMutex
refreshInterval time.Duration
lastTime *AtomicDuration
}
)
func NewImmutableResource(fn func() (interface{}, error), opts ...ImmutableResourceOption) *ImmutableResource {
// cannot use executors.LessExecutor because of cycle imports
ir := ImmutableResource{
fetch: fn,
refreshInterval: defaultRefreshInterval,
lastTime: NewAtomicDuration(),
}
for _, opt := range opts {
opt(&ir)
}
return &ir
}
func (ir *ImmutableResource) Get() (interface{}, error) {
ir.lock.RLock()
resource := ir.resource
ir.lock.RUnlock()
if resource != nil {
return resource, nil
}
ir.maybeRefresh(func() {
res, err := ir.fetch()
ir.lock.Lock()
if err != nil {
ir.err = err
} else {
ir.resource, ir.err = res, nil
}
ir.lock.Unlock()
})
ir.lock.RLock()
resource, err := ir.resource, ir.err
ir.lock.RUnlock()
return resource, err
}
func (ir *ImmutableResource) maybeRefresh(execute func()) {
now := timex.Now()
lastTime := ir.lastTime.Load()
if lastTime == 0 || lastTime+ir.refreshInterval < now {
ir.lastTime.Set(now)
execute()
}
}
// Set interval to 0 to enforce refresh every time if not succeeded. default is time.Second.
func WithRefreshIntervalOnFailure(interval time.Duration) ImmutableResourceOption {
return func(resource *ImmutableResource) {
resource.refreshInterval = interval
}
}

View File

@@ -0,0 +1,78 @@
package syncx
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestImmutableResource(t *testing.T) {
var count int
r := NewImmutableResource(func() (interface{}, error) {
count++
return "hello", nil
})
res, err := r.Get()
assert.Equal(t, "hello", res)
assert.Equal(t, 1, count)
assert.Nil(t, err)
// again
res, err = r.Get()
assert.Equal(t, "hello", res)
assert.Equal(t, 1, count)
assert.Nil(t, err)
}
func TestImmutableResourceError(t *testing.T) {
var count int
r := NewImmutableResource(func() (interface{}, error) {
count++
return nil, errors.New("any")
})
res, err := r.Get()
assert.Nil(t, res)
assert.NotNil(t, err)
assert.Equal(t, "any", err.Error())
assert.Equal(t, 1, count)
// again
res, err = r.Get()
assert.Nil(t, res)
assert.NotNil(t, err)
assert.Equal(t, "any", err.Error())
assert.Equal(t, 1, count)
r.refreshInterval = 0
time.Sleep(time.Millisecond)
res, err = r.Get()
assert.Nil(t, res)
assert.NotNil(t, err)
assert.Equal(t, "any", err.Error())
assert.Equal(t, 2, count)
}
func TestImmutableResourceErrorRefreshAlways(t *testing.T) {
var count int
r := NewImmutableResource(func() (interface{}, error) {
count++
return nil, errors.New("any")
}, WithRefreshIntervalOnFailure(0))
res, err := r.Get()
assert.Nil(t, res)
assert.NotNil(t, err)
assert.Equal(t, "any", err.Error())
assert.Equal(t, 1, count)
// again
res, err = r.Get()
assert.Nil(t, res)
assert.NotNil(t, err)
assert.Equal(t, "any", err.Error())
assert.Equal(t, 2, count)
}

42
core/syncx/limit.go Normal file
View File

@@ -0,0 +1,42 @@
package syncx
import (
"errors"
"zero/core/lang"
)
var ErrReturn = errors.New("discarding limited token, resource pool is full, someone returned multiple times")
type Limit struct {
pool chan lang.PlaceholderType
}
func NewLimit(n int) Limit {
return Limit{
pool: make(chan lang.PlaceholderType, n),
}
}
func (l Limit) Borrow() {
l.pool <- lang.Placeholder
}
// Return returns the borrowed resource, returns error only if returned more than borrowed.
func (l Limit) Return() error {
select {
case <-l.pool:
return nil
default:
return ErrReturn
}
}
func (l Limit) TryBorrow() bool {
select {
case l.pool <- lang.Placeholder:
return true
default:
return false
}
}

17
core/syncx/limit_test.go Normal file
View File

@@ -0,0 +1,17 @@
package syncx
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLimit(t *testing.T) {
limit := NewLimit(2)
limit.Borrow()
assert.True(t, limit.TryBorrow())
assert.False(t, limit.TryBorrow())
assert.Nil(t, limit.Return())
assert.Nil(t, limit.Return())
assert.Equal(t, ErrReturn, limit.Return())
}

56
core/syncx/lockedcalls.go Normal file
View File

@@ -0,0 +1,56 @@
package syncx
import "sync"
type (
// LockedCalls makes sure the calls with the same key to be called sequentially.
// For example, A called F, before it's done, B called F, then B's call would not blocked,
// after A's call finished, B's call got executed.
// The calls with the same key are independent, not sharing the returned values.
// A ------->calls F with key and executes<------->returns
// B ------------------>calls F with key<--------->executes<---->returns
LockedCalls interface {
Do(key string, fn func() (interface{}, error)) (interface{}, error)
}
lockedGroup struct {
mu sync.Mutex
m map[string]*sync.WaitGroup
}
)
func NewLockedCalls() LockedCalls {
return &lockedGroup{
m: make(map[string]*sync.WaitGroup),
}
}
func (lg *lockedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
begin:
lg.mu.Lock()
if wg, ok := lg.m[key]; ok {
lg.mu.Unlock()
wg.Wait()
goto begin
}
return lg.makeCall(key, fn)
}
func (lg *lockedGroup) makeCall(key string, fn func() (interface{}, error)) (interface{}, error) {
var wg sync.WaitGroup
wg.Add(1)
lg.m[key] = &wg
lg.mu.Unlock()
defer func() {
// delete key first, done later. can't reverse the order, because if reverse,
// another Do call might wg.Wait() without get notified with wg.Done()
lg.mu.Lock()
delete(lg.m, key)
lg.mu.Unlock()
wg.Done()
}()
return fn()
}

View File

@@ -0,0 +1,82 @@
package syncx
import (
"errors"
"fmt"
"sync"
"testing"
"time"
)
func TestLockedCallDo(t *testing.T) {
g := NewLockedCalls()
v, err := g.Do("key", func() (interface{}, error) {
return "bar", nil
})
if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
t.Errorf("Do = %v; want %v", got, want)
}
if err != nil {
t.Errorf("Do error = %v", err)
}
}
func TestLockedCallDoErr(t *testing.T) {
g := NewLockedCalls()
someErr := errors.New("some error")
v, err := g.Do("key", func() (interface{}, error) {
return nil, someErr
})
if err != someErr {
t.Errorf("Do error = %v; want someErr", err)
}
if v != nil {
t.Errorf("unexpected non-nil value %#v", v)
}
}
func TestLockedCallDoDupSuppress(t *testing.T) {
g := NewLockedCalls()
c := make(chan string)
var calls int
fn := func() (interface{}, error) {
calls++
ret := calls
<-c
calls--
return ret, nil
}
const n = 10
var results []int
var lock sync.Mutex
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
v, err := g.Do("key", fn)
if err != nil {
t.Errorf("Do error: %v", err)
}
lock.Lock()
results = append(results, v.(int))
lock.Unlock()
wg.Done()
}()
}
time.Sleep(100 * time.Millisecond) // let goroutines above block
for i := 0; i < n; i++ {
c <- "bar"
}
wg.Wait()
lock.Lock()
defer lock.Unlock()
for _, item := range results {
if item != 1 {
t.Errorf("number of calls = %d; want 1", item)
}
}
}

View File

@@ -0,0 +1,44 @@
package syncx
import "sync"
type ManagedResource struct {
resource interface{}
lock sync.RWMutex
generate func() interface{}
equals func(a, b interface{}) bool
}
func NewManagedResource(generate func() interface{}, equals func(a, b interface{}) bool) *ManagedResource {
return &ManagedResource{
generate: generate,
equals: equals,
}
}
func (mr *ManagedResource) MarkBroken(resource interface{}) {
mr.lock.Lock()
defer mr.lock.Unlock()
if mr.equals(mr.resource, resource) {
mr.resource = nil
}
}
func (mr *ManagedResource) Take() interface{} {
mr.lock.RLock()
resource := mr.resource
mr.lock.RUnlock()
if resource != nil {
return resource
}
mr.lock.Lock()
defer mr.lock.Unlock()
// maybe another Take() call already generated the resource.
if mr.resource == nil {
mr.resource = mr.generate()
}
return mr.resource
}

View File

@@ -0,0 +1,22 @@
package syncx
import (
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
)
func TestManagedResource(t *testing.T) {
var count int32
resource := NewManagedResource(func() interface{} {
return atomic.AddInt32(&count, 1)
}, func(a, b interface{}) bool {
return a == b
})
assert.Equal(t, resource.Take(), resource.Take())
old := resource.Take()
resource.MarkBroken(old)
assert.NotEqual(t, old, resource.Take())
}

10
core/syncx/once.go Normal file
View File

@@ -0,0 +1,10 @@
package syncx
import "sync"
func Once(fn func()) func() {
once := new(sync.Once)
return func() {
once.Do(fn)
}
}

20
core/syncx/once_test.go Normal file
View File

@@ -0,0 +1,20 @@
package syncx
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOnce(t *testing.T) {
var v int
add := Once(func() {
v++
})
for i := 0; i < 5; i++ {
add()
}
assert.Equal(t, 1, v)
}

15
core/syncx/onceguard.go Normal file
View File

@@ -0,0 +1,15 @@
package syncx
import "sync/atomic"
type OnceGuard struct {
done uint32
}
func (og *OnceGuard) Taken() bool {
return atomic.LoadUint32(&og.done) == 1
}
func (og *OnceGuard) Take() bool {
return atomic.CompareAndSwapUint32(&og.done, 0, 1)
}

View File

@@ -0,0 +1,17 @@
package syncx
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOnceGuard(t *testing.T) {
var guard OnceGuard
assert.False(t, guard.Taken())
assert.True(t, guard.Take())
assert.True(t, guard.Taken())
assert.False(t, guard.Take())
assert.True(t, guard.Taken())
}

98
core/syncx/pool.go Normal file
View File

@@ -0,0 +1,98 @@
package syncx
import (
"sync"
"time"
"zero/core/timex"
)
type (
PoolOption func(*Pool)
node struct {
item interface{}
next *node
lastUsed time.Duration
}
Pool struct {
limit int
created int
maxAge time.Duration
lock sync.Locker
cond *sync.Cond
head *node
create func() interface{}
destroy func(interface{})
}
)
func NewPool(n int, create func() interface{}, destroy func(interface{}), opts ...PoolOption) *Pool {
if n <= 0 {
panic("pool size can't be negative or zero")
}
lock := new(sync.Mutex)
pool := &Pool{
limit: n,
lock: lock,
cond: sync.NewCond(lock),
create: create,
destroy: destroy,
}
for _, opt := range opts {
opt(pool)
}
return pool
}
func (p *Pool) Get() interface{} {
p.lock.Lock()
defer p.lock.Unlock()
for {
if p.head != nil {
head := p.head
p.head = head.next
if p.maxAge > 0 && head.lastUsed+p.maxAge < timex.Now() {
p.created--
p.destroy(head.item)
continue
} else {
return head.item
}
}
if p.created < p.limit {
p.created++
return p.create()
}
p.cond.Wait()
}
}
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
p.lock.Lock()
defer p.lock.Unlock()
p.head = &node{
item: x,
next: p.head,
lastUsed: timex.Now(),
}
p.cond.Signal()
}
func WithMaxAge(duration time.Duration) PoolOption {
return func(pool *Pool) {
pool.maxAge = duration
}
}

111
core/syncx/pool_test.go Normal file
View File

@@ -0,0 +1,111 @@
package syncx
import (
"sync"
"sync/atomic"
"testing"
"time"
"zero/core/lang"
"github.com/stretchr/testify/assert"
)
const limit = 10
func TestPoolGet(t *testing.T) {
stack := NewPool(limit, create, destroy)
ch := make(chan lang.PlaceholderType)
for i := 0; i < limit; i++ {
go func() {
v := stack.Get()
if v.(int) != 1 {
t.Fatal("unmatch value")
}
ch <- lang.Placeholder
}()
select {
case <-ch:
case <-time.After(time.Second):
t.Fail()
}
}
}
func TestPoolPopTooMany(t *testing.T) {
stack := NewPool(limit, create, destroy)
ch := make(chan lang.PlaceholderType, 1)
for i := 0; i < limit; i++ {
var wait sync.WaitGroup
wait.Add(1)
go func() {
stack.Get()
ch <- lang.Placeholder
wait.Done()
}()
wait.Wait()
select {
case <-ch:
default:
t.Fail()
}
}
var waitGroup, pushWait sync.WaitGroup
waitGroup.Add(1)
pushWait.Add(1)
go func() {
pushWait.Done()
stack.Get()
waitGroup.Done()
}()
pushWait.Wait()
stack.Put(1)
waitGroup.Wait()
}
func TestPoolPopFirst(t *testing.T) {
var value int32
stack := NewPool(limit, func() interface{} {
return atomic.AddInt32(&value, 1)
}, destroy)
for i := 0; i < 100; i++ {
v := stack.Get().(int32)
assert.Equal(t, 1, int(v))
stack.Put(v)
}
}
func TestPoolWithMaxAge(t *testing.T) {
var value int32
stack := NewPool(limit, func() interface{} {
return atomic.AddInt32(&value, 1)
}, destroy, WithMaxAge(time.Millisecond))
v1 := stack.Get().(int32)
// put nil should not matter
stack.Put(nil)
stack.Put(v1)
time.Sleep(time.Millisecond * 10)
v2 := stack.Get().(int32)
assert.NotEqual(t, v1, v2)
}
func TestNewPoolPanics(t *testing.T) {
assert.Panics(t, func() {
NewPool(0, create, destroy)
})
}
func create() interface{} {
return 1
}
func destroy(_ interface{}) {
}

48
core/syncx/refresource.go Normal file
View File

@@ -0,0 +1,48 @@
package syncx
import (
"errors"
"sync"
)
var ErrUseOfCleaned = errors.New("using a cleaned resource")
type RefResource struct {
lock sync.Mutex
ref int32
cleaned bool
clean func()
}
func NewRefResource(clean func()) *RefResource {
return &RefResource{
clean: clean,
}
}
func (r *RefResource) Use() error {
r.lock.Lock()
defer r.lock.Unlock()
if r.cleaned {
return ErrUseOfCleaned
}
r.ref++
return nil
}
func (r *RefResource) Clean() {
r.lock.Lock()
defer r.lock.Unlock()
if r.cleaned {
return
}
r.ref--
if r.ref == 0 {
r.cleaned = true
r.clean()
}
}

View File

@@ -0,0 +1,27 @@
package syncx
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRefCleaner(t *testing.T) {
var count int
clean := func() {
count += 1
}
cleaner := NewRefResource(clean)
err := cleaner.Use()
assert.Nil(t, err)
err = cleaner.Use()
assert.Nil(t, err)
cleaner.Clean()
cleaner.Clean()
assert.Equal(t, 1, count)
cleaner.Clean()
cleaner.Clean()
assert.Equal(t, 1, count)
assert.Equal(t, ErrUseOfCleaned, cleaner.Use())
}

View File

@@ -0,0 +1,62 @@
package syncx
import (
"io"
"sync"
"zero/core/errorx"
)
type ResourceManager struct {
resources map[string]io.Closer
sharedCalls SharedCalls
lock sync.RWMutex
}
func NewResourceManager() *ResourceManager {
return &ResourceManager{
resources: make(map[string]io.Closer),
sharedCalls: NewSharedCalls(),
}
}
func (manager *ResourceManager) Close() error {
manager.lock.Lock()
defer manager.lock.Unlock()
var be errorx.BatchError
for _, resource := range manager.resources {
if err := resource.Close(); err != nil {
be.Add(err)
}
}
return be.Err()
}
func (manager *ResourceManager) GetResource(key string, create func() (io.Closer, error)) (io.Closer, error) {
val, err := manager.sharedCalls.Do(key, func() (interface{}, error) {
manager.lock.RLock()
resource, ok := manager.resources[key]
manager.lock.RUnlock()
if ok {
return resource, nil
}
resource, err := create()
if err != nil {
return nil, err
}
manager.lock.Lock()
manager.resources[key] = resource
manager.lock.Unlock()
return resource, nil
})
if err != nil {
return nil, err
}
return val.(io.Closer), nil
}

View File

@@ -0,0 +1,46 @@
package syncx
import (
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
type dummyResource struct {
age int
}
func (dr *dummyResource) Close() error {
return errors.New("close")
}
func TestResourceManager_GetResource(t *testing.T) {
manager := NewResourceManager()
defer manager.Close()
var age int
for i := 0; i < 10; i++ {
val, err := manager.GetResource("key", func() (io.Closer, error) {
age++
return &dummyResource{
age: age,
}, nil
})
assert.Nil(t, err)
assert.Equal(t, 1, val.(*dummyResource).age)
}
}
func TestResourceManager_GetResourceError(t *testing.T) {
manager := NewResourceManager()
defer manager.Close()
for i := 0; i < 10; i++ {
_, err := manager.GetResource("key", func() (io.Closer, error) {
return nil, errors.New("fail")
})
assert.NotNil(t, err)
}
}

76
core/syncx/sharedcalls.go Normal file
View File

@@ -0,0 +1,76 @@
package syncx
import "sync"
type (
// SharedCalls lets the concurrent calls with the same key to share the call result.
// For example, A called F, before it's done, B called F. Then B would not execute F,
// and shared the result returned by F which called by A.
// The calls with the same key are dependent, concurrent calls share the returned values.
// A ------->calls F with key<------------------->returns val
// B --------------------->calls F with key------>returns val
SharedCalls interface {
Do(key string, fn func() (interface{}, error)) (interface{}, error)
DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error)
}
call struct {
wg sync.WaitGroup
val interface{}
err error
}
sharedGroup struct {
calls map[string]*call
lock sync.Mutex
}
)
func NewSharedCalls() SharedCalls {
return &sharedGroup{
calls: make(map[string]*call),
}
}
func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.lock.Lock()
if c, ok := g.calls[key]; ok {
g.lock.Unlock()
c.wg.Wait()
return c.val, c.err
}
c := g.makeCall(key, fn)
return c.val, c.err
}
func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) {
g.lock.Lock()
if c, ok := g.calls[key]; ok {
g.lock.Unlock()
c.wg.Wait()
return c.val, false, c.err
}
c := g.makeCall(key, fn)
return c.val, true, c.err
}
func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call {
c := new(call)
c.wg.Add(1)
g.calls[key] = c
g.lock.Unlock()
defer func() {
// delete key first, done later. can't reverse the order, because if reverse,
// another Do call might wg.Wait() without get notified with wg.Done()
g.lock.Lock()
delete(g.calls, key)
g.lock.Unlock()
c.wg.Done()
}()
c.val, c.err = fn()
return c
}

View File

@@ -0,0 +1,108 @@
package syncx
import (
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestExclusiveCallDo(t *testing.T) {
g := NewSharedCalls()
v, err := g.Do("key", func() (interface{}, error) {
return "bar", nil
})
if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
t.Errorf("Do = %v; want %v", got, want)
}
if err != nil {
t.Errorf("Do error = %v", err)
}
}
func TestExclusiveCallDoErr(t *testing.T) {
g := NewSharedCalls()
someErr := errors.New("some error")
v, err := g.Do("key", func() (interface{}, error) {
return nil, someErr
})
if err != someErr {
t.Errorf("Do error = %v; want someErr", err)
}
if v != nil {
t.Errorf("unexpected non-nil value %#v", v)
}
}
func TestExclusiveCallDoDupSuppress(t *testing.T) {
g := NewSharedCalls()
c := make(chan string)
var calls int32
fn := func() (interface{}, error) {
atomic.AddInt32(&calls, 1)
return <-c, nil
}
const n = 10
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
v, err := g.Do("key", fn)
if err != nil {
t.Errorf("Do error: %v", err)
}
if v.(string) != "bar" {
t.Errorf("got %q; want %q", v, "bar")
}
wg.Done()
}()
}
time.Sleep(100 * time.Millisecond) // let goroutines above block
c <- "bar"
wg.Wait()
if got := atomic.LoadInt32(&calls); got != 1 {
t.Errorf("number of calls = %d; want 1", got)
}
}
func TestExclusiveCallDoExDupSuppress(t *testing.T) {
g := NewSharedCalls()
c := make(chan string)
var calls int32
fn := func() (interface{}, error) {
atomic.AddInt32(&calls, 1)
return <-c, nil
}
const n = 10
var wg sync.WaitGroup
var freshes int32
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
v, fresh, err := g.DoEx("key", fn)
if err != nil {
t.Errorf("Do error: %v", err)
}
if fresh {
atomic.AddInt32(&freshes, 1)
}
if v.(string) != "bar" {
t.Errorf("got %q; want %q", v, "bar")
}
wg.Done()
}()
}
time.Sleep(100 * time.Millisecond) // let goroutines above block
c <- "bar"
wg.Wait()
if got := atomic.LoadInt32(&calls); got != 1 {
t.Errorf("number of calls = %d; want 1", got)
}
if got := atomic.LoadInt32(&freshes); got != 1 {
t.Errorf("freshes = %d; want 1", got)
}
}

24
core/syncx/spinlock.go Normal file
View File

@@ -0,0 +1,24 @@
package syncx
import (
"runtime"
"sync/atomic"
)
type SpinLock struct {
lock uint32
}
func (sl *SpinLock) Lock() {
for !sl.TryLock() {
runtime.Gosched()
}
}
func (sl *SpinLock) TryLock() bool {
return atomic.CompareAndSwapUint32(&sl.lock, 0, 1)
}
func (sl *SpinLock) Unlock() {
atomic.StoreUint32(&sl.lock, 0)
}

View File

@@ -0,0 +1,41 @@
package syncx
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTryLock(t *testing.T) {
var lock SpinLock
assert.True(t, lock.TryLock())
assert.False(t, lock.TryLock())
lock.Unlock()
assert.True(t, lock.TryLock())
}
func TestSpinLock(t *testing.T) {
var lock SpinLock
lock.Lock()
assert.False(t, lock.TryLock())
lock.Unlock()
assert.True(t, lock.TryLock())
}
func TestSpinLockRace(t *testing.T) {
var lock SpinLock
lock.Lock()
var wait sync.WaitGroup
wait.Add(1)
go func() {
lock.Lock()
lock.Unlock()
wait.Done()
}()
time.Sleep(time.Millisecond * 100)
lock.Unlock()
wait.Wait()
assert.True(t, lock.TryLock())
}

View File

@@ -0,0 +1,51 @@
package syncx
import (
"errors"
"time"
)
var ErrTimeout = errors.New("borrow timeout")
type TimeoutLimit struct {
limit Limit
cond *Cond
}
func NewTimeoutLimit(n int) TimeoutLimit {
return TimeoutLimit{
limit: NewLimit(n),
cond: NewCond(),
}
}
func (l TimeoutLimit) Borrow(timeout time.Duration) error {
if l.TryBorrow() {
return nil
}
var ok bool
for {
timeout, ok = l.cond.WaitWithTimeout(timeout)
if ok && l.TryBorrow() {
return nil
}
if timeout <= 0 {
return ErrTimeout
}
}
}
func (l TimeoutLimit) Return() error {
if err := l.limit.Return(); err != nil {
return err
}
l.cond.Signal()
return nil
}
func (l TimeoutLimit) TryBorrow() bool {
return l.limit.TryBorrow()
}

View File

@@ -0,0 +1,33 @@
package syncx
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTimeoutLimit(t *testing.T) {
limit := NewTimeoutLimit(2)
assert.Nil(t, limit.Borrow(time.Millisecond*200))
assert.Nil(t, limit.Borrow(time.Millisecond*200))
var wait1, wait2, wait3 sync.WaitGroup
wait1.Add(1)
wait2.Add(1)
wait3.Add(1)
go func() {
wait1.Wait()
wait2.Done()
assert.Nil(t, limit.Return())
wait3.Done()
}()
wait1.Done()
wait2.Wait()
assert.Nil(t, limit.Borrow(time.Second))
wait3.Wait()
assert.Equal(t, ErrTimeout, limit.Borrow(time.Millisecond*100))
assert.Nil(t, limit.Return())
assert.Nil(t, limit.Return())
assert.Equal(t, ErrReturn, limit.Return())
}