diff --git a/go.mod b/go.mod index 432497f..122bf4e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/ratelimit/README.md b/ratelimit/README.md new file mode 100644 index 0000000..3c5e84f --- /dev/null +++ b/ratelimit/README.md @@ -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` отменили +во время ожидания. diff --git a/ratelimit/rate.go b/ratelimit/rate.go deleted file mode 100644 index 6aebdce..0000000 --- a/ratelimit/rate.go +++ /dev/null @@ -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 -} diff --git a/ratelimit/rate_test.go b/ratelimit/rate_test.go deleted file mode 100644 index 297619a..0000000 --- a/ratelimit/rate_test.go +++ /dev/null @@ -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) - } -} diff --git a/ratelimit/ratelimit_test.go b/ratelimit/ratelimit_test.go index 0095944..3af4c27 100644 --- a/ratelimit/ratelimit_test.go +++ b/ratelimit/ratelimit_test.go @@ -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