Dupcall task

This commit is contained in:
Fedor Korotkiy 2021-03-18 17:39:31 +03:00
parent c6d2706ac9
commit a1d5eee219
3 changed files with 185 additions and 0 deletions

18
dupcall/README.md Normal file
View 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
View 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
View 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")
}
}