Add ratelimit task

This commit is contained in:
Fedor Korotkiy 2021-03-02 21:05:37 +03:00
parent a011860727
commit de5fdd1753
5 changed files with 69 additions and 68 deletions

1
go.mod
View file

@ -8,6 +8,7 @@ require (
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect
github.com/coreos/go-etcd v2.0.0+incompatible // indirect
github.com/cpuguy83/go-md2man v1.0.10 // indirect
github.com/davecgh/go-spew v1.1.1
github.com/go-redis/redis v6.15.7+incompatible
github.com/go-resty/resty/v2 v2.1.0
github.com/gofrs/uuid v3.2.0+incompatible

16
ratelimit/README.md Normal file
View file

@ -0,0 +1,16 @@
# ratelimit
Напишите примитив синхронизации, ограничивающий число вызовов на интервале времени.
```go
func NewLimiter(maxCount int, interval time.Duration) *Limiter
func (l *Limiter) Acquire(ctx context.Context) error
```
`Limiter` должен гарантировать, что на любом интервале времени `interval`, не больше `maxCount` вызовов
`Acquire` могут завершиться без ошибки.
Каждый вызов `Acquire` должен либо завершаться успешно, либо завершаться с ошибкой в случае если `ctx` отменили
во время ожидания.

View file

@ -1,36 +0,0 @@
package ratelimit
import (
"fmt"
"strconv"
"strings"
"time"
)
// ParseRate parses rate in form of "N/D", e.g "10/1s" or "100/1ms"
func ParseRate(rate string) (count int, interval time.Duration, err error) {
parts := strings.SplitN(rate, "/", 2)
if len(parts) != 2 {
err = fmt.Errorf("invalid rate format in %q: missing slash", rate)
return
}
count, err = strconv.Atoi(parts[0])
if err != nil {
err = fmt.Errorf("invalid rate format in %q: %v", rate, err)
return
}
interval, err = time.ParseDuration(parts[1])
if err != nil {
err = fmt.Errorf("invalid rate format in %q: %v", rate, err)
return
}
if interval < 0 {
err = fmt.Errorf("invalid rate format in %q: negative interval", rate)
return
}
return
}

View file

@ -1,32 +0,0 @@
package ratelimit
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestParseRate(t *testing.T) {
count, dt, err := ParseRate("10/1s")
require.NoError(t, err)
require.Equal(t, 10, count)
require.Equal(t, time.Second, dt)
count, dt, err = ParseRate("1/1ms")
require.NoError(t, err)
require.Equal(t, 1, count)
require.Equal(t, time.Millisecond, dt)
}
func TestInvalidRate(t *testing.T) {
for _, invalid := range []string{
"",
"1/2",
"/10m",
"abc",
} {
_, _, err := ParseRate(invalid)
require.Errorf(t, err, "rate %q is invalid", invalid)
}
}

View file

@ -2,6 +2,8 @@ package ratelimit
import (
"context"
"math/rand"
"sort"
"sync"
"testing"
"time"
@ -41,6 +43,56 @@ func TestSimpleLimitCancel(t *testing.T) {
require.Equal(t, context.DeadlineExceeded, err)
}
func TestTimeDistribution(t *testing.T) {
limit := NewLimiter(100, time.Second)
var lock sync.Mutex
okTimes := []time.Duration{}
start := time.Now()
var wg sync.WaitGroup
for i := 0; i < 500; i++ {
time.Sleep(time.Millisecond * 5)
wg.Add(1)
go func() {
defer wg.Done()
dt := time.Duration(rand.Float64() * float64(time.Second))
ctx, cancel := context.WithTimeout(context.Background(), dt)
defer cancel()
err := limit.Acquire(ctx)
if err != nil {
return
}
lock.Lock()
defer lock.Unlock()
okTimes = append(okTimes, time.Since(start))
}()
}
wg.Wait()
require.Greater(t, len(okTimes), 200, "At least 200 goroutines should succeed")
sort.Slice(okTimes, func(i, j int) bool {
return okTimes[i] < okTimes[j]
})
for i, dt := range okTimes {
j := sort.Search(len(okTimes)-i, func(j int) bool {
return okTimes[i+j] > dt+time.Second
})
require.Lessf(t, j, 130, "%d goroutines acquired semaphore on interval [%v, %v)", j, dt, dt+time.Second)
}
// Uncomment this line to see full distribution
// spew.Fdump(os.Stderr, okTimes)
}
func TestStressBlocking(t *testing.T) {
const (
N = 100