Skip to content

Commit

Permalink
✅ add internal/singleflight test
Browse files Browse the repository at this point in the history
Signed-off-by: vankichi <kyukawa315@gmail.com>
  • Loading branch information
vankichi authored and actions-user committed Jul 6, 2020
1 parent a76ef1b commit 7040107
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 55 deletions.
9 changes: 8 additions & 1 deletion internal/singleflight/singleflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package singleflight

import (
"context"
"fmt"
"sync"
)

Expand All @@ -29,6 +30,7 @@ type call struct {
dups int
}

// Group represents interface for zero time cache
type Group interface {
Do(ctx context.Context, key string, fn func() (interface{}, error)) (v interface{}, shared bool, err error)
}
Expand All @@ -38,6 +40,7 @@ type group struct {
m map[string]*call
}

// New returns Group imple
func New(size int) Group {
if size < 1 {
size = 1
Expand All @@ -47,11 +50,14 @@ func New(size int) Group {
}
}

// Do returns a set of the cache of the first return value from function as interface{}, shared flg as bool, and err as error when the function is called multiple times in an instant
func (g *group) Do(ctx context.Context, key string, fn func() (interface{}, error)) (v interface{}, shared bool, err error) {
g.mu.RLock()
fmt.Println(g.m[key])
if c, ok := g.m[key]; ok {
g.mu.RUnlock()
c.dups++
fmt.Println("waiting")
c.wg.Wait()
return c.val, true, c.err
}
Expand All @@ -65,11 +71,12 @@ func (g *group) Do(ctx context.Context, key string, fn func() (interface{}, erro
g.mu.Unlock()

c.val, c.err = fn()
fmt.Println("release")
c.wg.Done()

g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()

fmt.Println("delete")
return c.val, c.dups > 0, c.err
}
222 changes: 168 additions & 54 deletions internal/singleflight/singleflight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ package singleflight

import (
"context"
"fmt"
"reflect"
"sync"
"sync/atomic"
"testing"
"time"

"github.com/vdaas/vald/internal/errors"
"go.uber.org/goleak"
Expand Down Expand Up @@ -49,31 +52,36 @@ func TestNew(t *testing.T) {
return nil
}
tests := []test{
// TODO test cases
/*
{
name: "test_case_1",
args: args {
size: 0,
},
want: want{},
checkFunc: defaultCheckFunc,
},
*/

// TODO test cases
/*
func() test {
return test {
name: "test_case_2",
args: args {
size: 0,
},
want: want{},
checkFunc: defaultCheckFunc,
}
}(),
*/
{
name: "returns Group when size is 0",
want: want{
want: &group{
m: make(map[string]*call, 1),
},
},
},
{
name: "returns Group when size is 1",
args: args{
size: 1,
},
want: want{
want: &group{
m: make(map[string]*call, 1),
},
},
},
{
name: "returns Group when size is over than 1",
args: args{
size: 2,
},
want: want{
want: &group{
m: make(map[string]*call, 2),
},
},
},
}

for _, test := range tests {
Expand Down Expand Up @@ -105,8 +113,7 @@ func Test_group_Do(t *testing.T) {
fn func() (interface{}, error)
}
type fields struct {
mu sync.RWMutex
m map[string]*call
m map[string]*call
}
type want struct {
wantV interface{}
Expand All @@ -118,8 +125,11 @@ func Test_group_Do(t *testing.T) {
args args
fields fields
want want
cond *sync.Cond
mu *sync.Mutex
wg *sync.WaitGroup
checkFunc func(want, interface{}, bool, error) error
beforeFunc func(args)
beforeFunc func(Group, args)
afterFunc func(args)
}
defaultCheckFunc := func(w want, gotV interface{}, gotShared bool, err error) error {
Expand Down Expand Up @@ -153,45 +163,149 @@ func Test_group_Do(t *testing.T) {
},
*/

// TODO test cases
/*
func() test {
return test {
name: "test_case_2",
args: args {
ctx: nil,
key: "",
fn: nil,
},
fields: fields {
mu: sync.RWMutex{},
m: nil,
},
want: want{},
checkFunc: defaultCheckFunc,
}
}(),
*/
func() test {
mu := new(sync.Mutex)
cond := sync.NewCond(mu)
cnt := uint32(0)
wg := new(sync.WaitGroup)
return test{
mu: mu,
cond: cond,
wg: wg,
name: "returns (v, shared, nil) when Do is called with another key",
args: args{
ctx: context.Background(),
key: "req_1",
fn: func() (interface{}, error) {
atomic.AddUint32(&cnt, 1)
return "res_1", nil
},
},
fields: fields{
m: make(map[string]*call, 2),
},
want: want{
wantV: "res_1",
wantShared: false,
err: nil,
},
beforeFunc: func(g Group, args args) {
wg.Add(1)
go func() {
mu.Lock()
defer mu.Unlock()
cond.Wait()
g.Do(context.Background(), "req_2", func() (interface{}, error) {
defer wg.Done()
atomic.AddUint32(&cnt, 1)
return "res_2", nil
})
}()
},
checkFunc: func(want, interface{}, bool, error) error {
if got, want := int(atomic.LoadUint32(&cnt)), 2; got != want {
return errors.Errorf("cnt got = %d, want = %d", got, want)
}
return nil
},
}
}(),
func() test {
mu := new(sync.Mutex)
cond := sync.NewCond(mu)
cnt := uint32(0)
wg := new(sync.WaitGroup)
return test{
name: "returns (v, shared, nil) when Do is called with same key",
mu: mu,
cond: cond,
wg: wg,
args: args{
ctx: context.Background(),
key: "req_1",
fn: func() (interface{}, error) {
fmt.Println("args")
atomic.AddUint32(&cnt, 1)
return "res_1", nil
},
},
fields: fields{
m: make(map[string]*call, 2),
},
want: want{
wantV: "res_1",
wantShared: true,
err: nil,
},
beforeFunc: func(g Group, args args) {
wg.Add(1)
ch := make(chan struct{})
go func() {
g.Do(context.Background(), "req_1", func() (interface{}, error) {
ch <- struct{}{}
fmt.Println("test")
defer wg.Done()
time.Sleep(time.Second * 10)
return "res_1", nil
})
}()
<- ch
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
mu.Lock()
defer mu.Unlock()
cond.Wait()
defer wg.Done()
fmt.Println(i)
g.Do(context.Background(), "req_1", func() (interface{}, error) {
atomic.AddUint32(&cnt, 1)
return "res_1", nil
})
}(i)
}
},
checkFunc: func(want, interface{}, bool, error) error {
if got, want := int(atomic.LoadUint32(&cnt)), 1; got != want {
return errors.Errorf("cnt got = %d, want = %d", got, want)
}
return nil
},
}
}(),
}

for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
defer goleak.VerifyNone(t)
if test.beforeFunc != nil {
test.beforeFunc(test.args)
}
defer goleak.VerifyNone(tt)
if test.afterFunc != nil {
defer test.afterFunc(test.args)
}
if test.checkFunc == nil {
test.checkFunc = defaultCheckFunc
}
g := &group{
mu: test.fields.mu,
m: test.fields.m,
m: test.fields.m,
}

if test.beforeFunc != nil {
test.beforeFunc(g, test.args)
}

gotV, gotShared, err := g.Do(test.args.ctx, test.args.key, test.args.fn)
var gotV interface{}
var gotShared bool
var err error
test.wg.Add(1)
go func() {
test.mu.Lock()
defer test.mu.Unlock()
test.cond.Wait()
defer test.wg.Done()
gotV, gotShared, err = g.Do(test.args.ctx, test.args.key, test.args.fn)
}()
time.Sleep(time.Second)
test.cond.Broadcast()
test.wg.Wait()
if err := test.checkFunc(test.want, gotV, gotShared, err); err != nil {
tt.Errorf("error = %v", err)
}
Expand Down

0 comments on commit 7040107

Please sign in to comment.