initial import
This commit is contained in:
38
core/syncx/atomicbool.go
Normal file
38
core/syncx/atomicbool.go
Normal 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
|
||||
}
|
||||
27
core/syncx/atomicbool_test.go
Normal file
27
core/syncx/atomicbool_test.go
Normal 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())
|
||||
}
|
||||
30
core/syncx/atomicduration.go
Normal file
30
core/syncx/atomicduration.go
Normal 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))
|
||||
}
|
||||
19
core/syncx/atomicduration_test.go
Normal file
19
core/syncx/atomicduration_test.go
Normal 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())
|
||||
}
|
||||
40
core/syncx/atomicfloat64.go
Normal file
40
core/syncx/atomicfloat64.go
Normal 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))
|
||||
}
|
||||
24
core/syncx/atomicfloat64_test.go
Normal file
24
core/syncx/atomicfloat64_test.go
Normal 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
13
core/syncx/barrier.go
Normal 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()
|
||||
}
|
||||
31
core/syncx/barrier_test.go
Normal file
31
core/syncx/barrier_test.go
Normal 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
47
core/syncx/cond.go
Normal 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
73
core/syncx/cond_test.go
Normal 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
28
core/syncx/donechan.go
Normal 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
|
||||
}
|
||||
33
core/syncx/donechan_test.go
Normal file
33
core/syncx/donechan_test.go
Normal 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()
|
||||
}
|
||||
77
core/syncx/immutableresource.go
Normal file
77
core/syncx/immutableresource.go
Normal 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
|
||||
}
|
||||
}
|
||||
78
core/syncx/immutableresource_test.go
Normal file
78
core/syncx/immutableresource_test.go
Normal 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
42
core/syncx/limit.go
Normal 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
17
core/syncx/limit_test.go
Normal 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
56
core/syncx/lockedcalls.go
Normal 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()
|
||||
}
|
||||
82
core/syncx/lockedcalls_test.go
Normal file
82
core/syncx/lockedcalls_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
core/syncx/managedresource.go
Normal file
44
core/syncx/managedresource.go
Normal 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
|
||||
}
|
||||
22
core/syncx/managedresource_test.go
Normal file
22
core/syncx/managedresource_test.go
Normal 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
10
core/syncx/once.go
Normal 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
20
core/syncx/once_test.go
Normal 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
15
core/syncx/onceguard.go
Normal 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)
|
||||
}
|
||||
17
core/syncx/onceguard_test.go
Normal file
17
core/syncx/onceguard_test.go
Normal 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
98
core/syncx/pool.go
Normal 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
111
core/syncx/pool_test.go
Normal 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
48
core/syncx/refresource.go
Normal 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()
|
||||
}
|
||||
}
|
||||
27
core/syncx/refresource_test.go
Normal file
27
core/syncx/refresource_test.go
Normal 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())
|
||||
}
|
||||
62
core/syncx/resourcemanager.go
Normal file
62
core/syncx/resourcemanager.go
Normal 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
|
||||
}
|
||||
46
core/syncx/resourcemanager_test.go
Normal file
46
core/syncx/resourcemanager_test.go
Normal 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
76
core/syncx/sharedcalls.go
Normal 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
|
||||
}
|
||||
108
core/syncx/sharedcalls_test.go
Normal file
108
core/syncx/sharedcalls_test.go
Normal 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
24
core/syncx/spinlock.go
Normal 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)
|
||||
}
|
||||
41
core/syncx/spinlock_test.go
Normal file
41
core/syncx/spinlock_test.go
Normal 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())
|
||||
}
|
||||
51
core/syncx/timeoutlimit.go
Normal file
51
core/syncx/timeoutlimit.go
Normal 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()
|
||||
}
|
||||
33
core/syncx/timeoutlimit_test.go
Normal file
33
core/syncx/timeoutlimit_test.go
Normal 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())
|
||||
}
|
||||
Reference in New Issue
Block a user