Add ratelimit task
This commit is contained in:
parent
a011860727
commit
de5fdd1753
5 changed files with 69 additions and 68 deletions
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible // indirect
|
github.com/coreos/go-etcd v2.0.0+incompatible // indirect
|
||||||
github.com/cpuguy83/go-md2man v1.0.10 // 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-redis/redis v6.15.7+incompatible
|
||||||
github.com/go-resty/resty/v2 v2.1.0
|
github.com/go-resty/resty/v2 v2.1.0
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible
|
github.com/gofrs/uuid v3.2.0+incompatible
|
||||||
|
|
16
ratelimit/README.md
Normal file
16
ratelimit/README.md
Normal 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` отменили
|
||||||
|
во время ожидания.
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,8 @@ package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -41,6 +43,56 @@ func TestSimpleLimitCancel(t *testing.T) {
|
||||||
require.Equal(t, context.DeadlineExceeded, err)
|
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) {
|
func TestStressBlocking(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
N = 100
|
N = 100
|
||||||
|
|
Loading…
Reference in a new issue