Dupcall task
This commit is contained in:
parent
c6d2706ac9
commit
a1d5eee219
3 changed files with 185 additions and 0 deletions
18
dupcall/README.md
Normal file
18
dupcall/README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# dupcall
|
||||
|
||||
В этом задании нужно реализовать свою вариацию на тему singleflight.
|
||||
|
||||
Объект `dupcall.Call` должен дедублицировать вызовы дорогой функции, правильно обрабатывая отмену контекста.
|
||||
|
||||
Клиенты вызывают метод `Do` из разных горутин, передавая внутрь `cb` который они хотят запустить. `cb` запускается
|
||||
в отдельной горутине и в отдельном контексте.
|
||||
|
||||
В один момент времени должен быть запущен только один не отменённый `cb`. Клиент вызвавщий `Do` должен получить результатом
|
||||
пару `result interface{}, err error` от того `cb`, который был запущен в момент вызова `Do` (но не обязательно
|
||||
от своего `cb`).
|
||||
|
||||
При этом вызов `Do` может быть отменён через `ctx`. Отменённый вызов `Do` должен завершаться сразу. Бегущий внутри
|
||||
`cb` должен отменяться только в случае, если __все__ ждущие вызовы `Do` были отменены.
|
||||
|
||||
Например, если две горутины сделали вызов `Do`, а потом первый вызов был отменён, `cb` должен добежать успешно и вторая горутина
|
||||
должна получить его результат.
|
13
dupcall/dupcall.go
Normal file
13
dupcall/dupcall.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
// +build !solution
|
||||
|
||||
package dupcall
|
||||
|
||||
import "context"
|
||||
|
||||
type Call struct {
|
||||
}
|
||||
|
||||
func (o *Call) Do(
|
||||
ctx context.Context,
|
||||
cb func(context.Context) (interface{}, error),
|
||||
) (result interface{}, err error)
|
154
dupcall/dupcall_test.go
Normal file
154
dupcall/dupcall_test.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package dupcall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestCall_Simple(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
called := 0
|
||||
|
||||
var call Call
|
||||
result, err := call.Do(context.Background(), func(ctx context.Context) (interface{}, error) {
|
||||
called++
|
||||
return "ok", nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, result, "ok")
|
||||
require.Equal(t, called, 1)
|
||||
|
||||
errFailed := errors.New("failed")
|
||||
|
||||
result, err = call.Do(context.Background(), func(ctx context.Context) (interface{}, error) {
|
||||
called++
|
||||
return nil, errFailed
|
||||
})
|
||||
|
||||
require.Equal(t, err, errFailed)
|
||||
require.Nil(t, result)
|
||||
require.Equal(t, called, 2)
|
||||
}
|
||||
|
||||
func TestCall_Dedup(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
called := 0
|
||||
cb := func(ctx context.Context) (interface{}, error) {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
called++
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
var call Call
|
||||
for i := 0; i < 10; i++ {
|
||||
go call.Do(context.Background(), cb)
|
||||
}
|
||||
|
||||
result, err := call.Do(context.Background(), cb)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, result, "ok")
|
||||
require.Equal(t, called, 1)
|
||||
}
|
||||
|
||||
func TestCall_HalfCancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
called := 0
|
||||
cb := func(ctx context.Context) (interface{}, error) {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
called++
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
var call Call
|
||||
for i := 0; i < 10; i++ {
|
||||
go call.Do(context.Background(), cb)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go call.Do(ctx, cb)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
cancel()
|
||||
}
|
||||
|
||||
result, err := call.Do(context.Background(), cb)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, result, "ok")
|
||||
require.Equal(t, called, 1)
|
||||
}
|
||||
|
||||
func TestCall_FullCancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
cb := func(ctx context.Context) (interface{}, error) {
|
||||
<-ctx.Done()
|
||||
close(cancelled)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var call Call
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go call.Do(ctx, cb)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
cancel()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-cancelled:
|
||||
return
|
||||
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
t.Errorf("duplicate call not cancelled after 100ms")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCall_NonBlockingCancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
var call Call
|
||||
cb := func(ctx context.Context) (interface{}, error) {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
_, err := call.Do(ctx, cb)
|
||||
assert.Error(t, err)
|
||||
close(cancelled)
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-cancelled:
|
||||
return
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
t.Errorf("cancelled call blocked for more that 50ms")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue